Custom validation based on other value

佐手、 提交于 2019-12-24 02:52:01

问题


I make a Booking form for restaurant, which asks for the name of the restaurant, the date of the meal and the number of person.

I have a booking class, which has an ID, an ID of the restaurant, a date and a number of people :

public class Booking
{
    public int Id { get; set; }
    public int IDRestaurant{ get; set; }
    [CustomPlaceValidator]
    public int Nbpeople { get; set; }
    [CustomDateValidator]
    public DateTime Date { get; set; }
}

As well as a Resto class, which has an ID, a name, phone number and a number of table :

public class Resto
{
    public int Id { get; set; }
    [Required(ErrorMessage = "Le nom du restaurant doit être saisi")]
    public string Nom { get; set; }
    [Display(Name = "Téléphone")]
    [RegularExpression(@"^0[0-9]{9}$", ErrorMessage = "Le numéro de téléphone est incorrect")]
    public string Telephone { get; set; }
    [Range(0, 9999)]
    public int Size { get; set; }
}

I would like to make a validation to check with each new reservation, that the restaurant is not full. To do this, when validating the "Number of persons" field of the Booking, I need the value of the "restaurant name" field and the value of the "date" field, and then retrieve all the bookings on this Restaurant at that date, and check whether the sum of the number of persons is much lower than the capacity of the restaurant.

public class CustomPlaceValidator : ValidationAttribute
{
    private IDal dal = new Dal();
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        int nb = 0;
        if (dal.GetAllBooking() != null)
        {
            foreach (var booking in dal.GetAllBooking())
                nb += booking.Nbpeople;
            if (nb ..... ) return ValidationResult.Success;
            return new ValidationResult("The restaurant is full for this date.");
        }
        return ValidationResult.Success;

    }

}

(It's a draft, the tests are not finished obviously)

How can I have the value of the other proprieties for my validation ?


回答1:


This is not appropriate for a validation attribute. First, a validation attribute should be independent, or at least self-contained. Since the logic here depends on two different properties (the number of people and the date of the booking) a validation attribute would require too much knowledge of the domain in order to perform the necessary validation. In other words, it's not reusable, and if it's not reusable, then there's no point in using an attribute.

Second, a validation attribute should not do something like make a database query. The controller alone should be responsible for working with your DAL. When you start littering database access across your application, you're going to start running into all sorts of issues in very short order. If you use a DI container to inject your DAL where it needs to go, it's less problematic to use it outside of the controller, but importantly, attributes really don't play well with dependency injection. You can make it work with some DI containers, but it's never easy and you're probably going to regret it later. So, again, this really shouldn't be something a validation attribute handles.

The best approach in my opinion is to simply create a private/protected method on your controller to handle this validation. Something like:

public void ValidateCapacity(Booking booking)
{
    var restaurant = dal.GetRestaurant(booking.IDRestaurant);
    var existingBookings = dal.GetBookings(booking.IDRestaurant, booking.Date);
    var available = restaurant.Size - existingBookings.Sum(b => b.Nbpeople);
    if (booking.Nbpeople > available)
    {
        ModelState.AddModelError("Nbpeople", "There is not enough capacity at the restaurant for this many people on the date you've selected");
    }
}

Then, in your post action for the booking, simply call this before checking ModelState.IsValid.




回答2:


I'm looking at this question: Group validation messages for multiple properties together into one message asp.net mvc

My guess is something like:

public class Booking
{
    public int Id { get; set; }
    public int IDRestaurant{ get; set; }
    [CustomPlace("IDRestaurant", "Date", ErrorMessage = "the restaurant is full")]
    public int Nbpeople { get; set; }
    [CustomDateValidator]
    public DateTime Date { get; set; }
}

and the custom validation:

public class CustomPlaceAttribute : ValidationAttribute
{
    private readonly string[] _others
    public CustomPlaceAttribute(params string[] others)
    {
        _others= others;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
           // TODO: validate the length of _others to ensure you have all required inputs
           var property = validationContext.ObjectType.GetProperty(_others[0]);
           if (property == null)
           {
               return new ValidationResult(
                string.Format("Unknown property: {0}", _others[0])
               );
           }
           // This is to get one of the other value information. 
           var otherValue = property.GetValue(validationContext.ObjectInstance, null);

           // TODO: get the other value again for the date -- and then apply your business logic of determining the capacity          
    }
}

However, it feels a bit messy to do a database call for the validationAttribute though




回答3:


What you are asking for is cross-property validation. If you are not strongly opposed to implementing an interface on your data objects you should take a look at the following:

https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.ivalidatableobject.aspx

A simple example implementation for a small rectangle class where we want its area not to exceed 37 (whatever that unit is).

public class SmallRectangle : IValidatableObject
{
    public uint Width { get; set; }
    public uint Height { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var area = Width * Height;
        if (area > 37)
        {
            yield return new ValidationResult($"The rectangle is too large.");
        }
    }
}

Alternatives

The the second parameter of the IsValid function in your ValidationAttribute provides you with the ValidationContext which has the property ObjectInstance which you can cast to your object type and access its other members. That, however, will make your validation attribute specific to your class. I would generally advise against that.

You could also opt to use a different validation approach altogether such as using a validation library such as FluentValidations, see: https://github.com/JeremySkinner/FluentValidation

A different perspective

Last but not least I would like to note that usually validation should be used to validate the integrity of the data. A booking request which requests more seats than available is not invalid. It can not be granted, but it is a valid request which will, unfortunately, be answered with a negative result. To give that negative result is, in my opinion not the responsibility of the validation, but the business logic.



来源:https://stackoverflow.com/questions/44848134/custom-validation-based-on-other-value

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!