15

Problem

I know there is a lot of ways of doing Model validation within MVC, and there is quite a lot of documentation regarding this topic. However, I'm not quite sure what's the best approach for validating properties of the Model which are "Sub Model" of same type.

Keep in mind the following

  • I still want to to take profit of the TryUpdateModel/TryValidateModel methods
  • Each of these "sub models" have strongly typed views
  • There is one strongly typed view for the MainModel class that renders the overall display view

It might sound a little confusing but i'll throw in some code to clarify. Take as example the following classes:

MainModel:

class MainModel{
    public SomeSubModel Prop1 { get; set; }
    public SomeSubModel Prop2 { get; set; }
}

SomeSubModel:

class SomeSubModel{
      public string Name { get; set; }
      public string Foo { get; set; }
      public int Number { get; set; }
}

MainModelController:

class MainModelController{

    public ActionResult MainDisplay(){
         var main = db.retrieveMainModel();
         return View(main); 
    }

    [HttpGet]
    public ActionResult EditProp1(){
         //hypothetical retrieve method to get MainModel from somewhere
         var main = db.retrieveMainModel();

         //return "submodel" to the strictly typed edit view for Prop1
         return View(main.Prop1);
    }

    [HttpPost]
    public ActionResult EditProp1(SomeSubModel model){

         if(TryValidateModel(model)){
              //hypothetical retrieve method to get MainModel from somewhere
              var main = db.retrieveMainModel();
              main.Prop1 = model;
              db.Save();

              //when succesfully saved return to main display page 
              return RedirectToAction("MainDisplay");
         }
         return View(main.Prop1);
    }

    //[...] similar thing for Prop2 
    //Prop1 and Prop2 could perhaps share same view as its strongly 
    //typed to the same class
}

I believe this code all make sense until now (correct me if it's not the case) because TryValidateModel() is validating against a model with no ValidationAttribute.

The problem lies here, where would be the best place, or what would be the best and most elegant way to have different validation constraints for Prop1 and Prop2 while still taking advantage of TryValidateModel() and not filling the Edit method with conditional statements and ModelState.AddModelError()

Usually you could have validation attributes in the SomeSubModel class, but it wouldn't work in this case, because there is different constraints for each property.

Other option is that there could be Custom validation attribute in the MainModel class, but it also wouldn't work in this case because the SomeSubModelobject is passed directly to the view and when validating has no reference to its MainModel object.

The only left option I can think about is a ValidationModel for each property, but I am not quite what would be the best approach for this.

Solution

Here's solution I implemented, based of @MrMindor's answer.

Base ValidationModel class:

public class ValidationModel<T> where T : new()
{
    protected ValidationModel() {
        this.Model = new T();
    }
    protected ValidationModel(T obj) { 
        this.Model = obj; 
    }

    public T Model { get; set; }
}

Validation Model for Prop1

public class Prop1ValidationModel:ValidationModel<SomeSubModel>
{
    [StringLength(15)]
    public string Name { get{ return base.Model.Name; } set { base.Model.Name = value; } }

    public Prop1ValidationModel(SomeSubModel ssm)
        : base(ssm) { }
}

Validation Model for Prop2

public class Prop2ValidationModel:ValidationModel<SomeSubModel>
{
    [StringLength(70)]
    public string Name { get{ return base.Model.Name; } set { base.Model.Name = value; } }

    public Prop2ValidationModel(SomeSubModel ssm)
        : base(ssm) { }
}

Action

[HttpPost]
public ActionResult EditProp1(SomeSubModel model){

     Prop1ValidationModel vModel = new Prop1ValidationModel(model);
     if(TryValidateModel(vModel)){

          //[...] persist data

          //when succesfully saved return to main display page 
          return RedirectToAction("MainDisplay");
     }
     return View(model);
}
Pierluc SS
  • 3,138
  • 7
  • 31
  • 44
  • Does each SomeSubModel validate just against itself, or does it rely on other information as well? – Mr.Mindor Oct 22 '12 at 14:34
  • 1
    You can this library http://fluentvalidation.codeplex.com/. I'm not sure whether this is what you want. – VJAI Oct 22 '12 at 15:27
  • @Mr.Mindor Technically each instance/properties of type SomeSubModel in MainModel could have different validation constraints. – Pierluc SS Oct 22 '12 at 15:48
  • @Mark fluentvalidation seems indeed like an interesting API. Thanks for sharing. However, I think it wouldn't solve my problem, as where I would relate the validator with the object. – Pierluc SS Oct 22 '12 at 15:52
  • @Burnzy do you trust the data in database? Can loading data from storage cause "models" to become *invalid*? – tereško Oct 23 '12 at 14:29
  • Can you create a custom ModelMetadataProvider that provide a different ModelMetadata for each property, and provide different validators (http://msdn.microsoft.com/en-us/library/system.web.mvc.modelmetadata.getvalidators(v=vs.108).aspx)? You still need to differentiate between Prop1 and Prop2 *somewhere*. – Simon Mourier Oct 23 '12 at 17:10
  • @tereško Yes, data from db will always be validated before getting in. – Pierluc SS Oct 23 '12 at 17:42
  • @SimonMourier seems like an interesting approach i'll read more on it, thank you. – Pierluc SS Oct 23 '12 at 17:43
  • @SimonMourier I read about writting a custom ModelMetadataProvider but I am not sure how you would handle it in the controller. Could you write up a sample? – Pierluc SS Oct 23 '12 at 18:47
  • @Burnzy , since your "models" (they are actually [domain object](http://c2.com/cgi/wiki?DomainObject), model is layer) can become invalid only when assign values to them, the most pragmatic way would be to the validation of each value in setter. – tereško Oct 23 '12 at 18:51
  • @tereško That could make sense, however, since those fields are user inputs, if the data is incorrect, I want to prompt it on the UI. If it's only done in the setter, I don't have any relation to the ModelMetadata from the actual object/model. – Pierluc SS Oct 23 '12 at 19:01
  • 2
    Since i am not an ASP.NET user, i might be wrong about this ... but .. such setter, when applied value from invalid user input (btw, model layer should not care about the source of input, it might as well come from mars rovers sensors), can rise an exception. Service, which contains that "model"/domain object, then handles the exception and acquires an error state. This error state then can be freely checked (by request or thought observers) in presentation layer. And if you are checking for data integrity errors instead (like "unique username"), those should be handled in DB abstraction. – tereško Oct 24 '12 at 00:12
  • Where do you want to implement the validation rules? In the controller? in the SomeSubModel class? else? Also,have you though about implementing IValidatableObject in the SomeSubModel class? – Simon Mourier Oct 24 '12 at 09:28
  • @SimonMourier None of these would work as, Prop1 and Prop2 have different validation constraint – Pierluc SS Oct 24 '12 at 11:39
  • @tereško You're approach makes perfect sense, however in the MVC .NET framework, there is a validation mechanism available for this kind of cases, and this is what i'd like to take advantage of. – Pierluc SS Oct 24 '12 at 11:42
  • When do you intend to code the constraint then? If the object itself does not know if it's valid? – Simon Mourier Oct 24 '12 at 12:08
  • @SimonMourier that's the point of my question :) – Pierluc SS Oct 24 '12 at 12:19
  • From an OOP perspective, as Prop1 and Prop2 have different validation rules, shouldn't they be different classes? Is there a reason they can't be? – Mr.Mindor Oct 24 '12 at 13:55

3 Answers3

4

We have a similar situation in one of our applications where each SomeSubModel represents a parameter setting for a job. As each type of job has a different number and types of parameters, our job model has a collection of these parameters instead of just having set properties.

We have a JobParameter that is subclassed into the different types available (StringParameter, BoolParameter, DoubleParameter, ...). These subclasses have their own sets of validation attributes.
A shared 'JobParameterModel' is used for passing the parameters to the view.
For Validation the returned Model is converted to its specific JobParameter.
ParameterTypes:

public enum ParameterType
{
    Empty = 0,
    Boolean = 1,
    Integer = 2,
    String = 3,
    DateTime = 4,
    ...
}

JobParameter:

class JobParameter
{ 
  [AValidationAttributeForAllParamters] 
  public string Name { get; set; }  
  public virtual string Foo { get; set; }  
  public int Number { get; set; }
  public ParameterType Type {get;set;}

  private static readonly IDictionary<ParameterType, Func<object>> ParameterTypeDictionary =
  new Dictionary<ParameterType, Func<object>>{
                {ParameterType.Empty, () => new EmptyParameter() },
                {ParameterType.String, ()=>new StringParameter()},
                {ParameterType.Password, ()=>new PasswordParameter()},
                ...
              };
    public static ScriptParameter Factory(ParameterType type)
    {
        return (ScriptParameter)ParameterTypeDictionary[type]();
    }
}  

BoolParameter:

[ABoolClassLevelValidationAttribute]
class BoolParameter:JobParameter
{
    [AValidationAttribute]
    public override string Foo {get;set;}
}

....

In our validation framework (which I am told is modeled very closely to MS's) the ViewModel is always converted back to its domain object for validation.
ParameterModel:

class ParameterModel: JobParameter
{
    public JobParameter ToDomain()
    {
        var domainObject = JobParameter.Factory(Type);
        Mapper.Map(this, domainObject);
        return domainObject;
    }
    public bool Validate()
    {
        var dom = ToDomain();
        return TryValidate(dom);
    }

}

Controller:

class Controller(){

    [HttpPost]                                
    public ActionResult SaveParameter(JobParameter model){                                

         if(TryValidateModel(model)){                                

              //persist stuff to db.

          //when succesfully saved return to main display page                                 
              return RedirectToAction("MainDisplay");                                
         }                                
         return View(main.Prop1);
    }                                
}                                

For the sake of your specific situation, you don't need to get quite this complicated (Or trust that the specifics of our validation framework will work for you).
Edit/Save Actions for each Prop:
Create a validation model for each prop. Prop1ValidationModel, Prop2ValidationModel

[HttpGet]
public ActionResult EditProp1()
{
    var main = db.retrieveMainModel();
    db.Prop1.SubmitUrl = Url.Action("SaveProp1","Controller");
    return View(main.Prop1);
}
[HttpPost]                                
public ActionResult SaveProp1(SomeSubModel model){                                
     var validationModel = new Prop1ValidationModel{
     ///copy properties                                   
         };
     if(TryValidateModel(validationModel)){                                

          var main = db.retrieveMainModel();                                
          main.Prop1 = model;                                
          db.Save();                                

          //when succesfully saved return to main display page                                 
          return RedirectToAction("MainDisplay");                                
     }                                
     return View(main.Prop1);
} 

With this you can use the same strongly typed view for both Prop1 and Prop2.

Mr.Mindor
  • 4,079
  • 2
  • 19
  • 25
2

If SomeSubModel has different validation attributes depending if it is applied either in Prop1 or Prop2...means that actually the two SomeSubModel of prop1 end prop2 are two different classes, because also if they have the same fields the meaning of this fields is different depending if they are attached to prop1 or prop2(that is why they have different validation attributes. Accordingly the best approach is defining two subclasses of SomeSubClass, say SomeSubClass1 and SomeSubClass2 that inherit from the Common SomeSubClass. Once inherited you must not add new properties but just new validation rules either by using fluent validation or by using the MetaDataTypeAttribute to specify Validation attributes out of the class definition. So you will have something like:

[MetaDataType(typeof(ValidationClass1)]
public class SomeSubClass1: SomeSubclass{}

and

[MetaDataType(typeof(ValidationClass2)]
public class SomeSubClass2: SomeSubclass{}
Francesco Abbruzzese
  • 4,139
  • 1
  • 17
  • 18
  • I'll up vote for the MetadataType attribute, which is pretty interesting, althought this might be a bit overwhelming for what I want to do. – Pierluc SS Oct 26 '12 at 13:27
0

My answer uses DataAnnotations to validate the submodel, although you will want to clear the ModelState errors and revalidate to ensure it is accurately validating your model.

I left in my code for making the submodel validate conditionally based on if a different public property's value equals a specified string. Feel free to strip this if you don't want it but I think it's really useful, since it should still prevent underposting. Same goes with the ErrorMessage being Json, I had to quickly come up with a way to bind the errors to the submodel's properties and it's what I came up with.

Validation Attribute

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class ValidateObjectIfOtherFieldInAttribute : ValidationAttribute
{
    private readonly string _otherProperty;
    private readonly string[] _otherValues;
    public ValidateObjectIfOtherFieldInAttribute(string otherProperty, string otherValues)
    {
        _otherProperty = otherProperty;
        _otherValues = otherValues.ToLower().Split(',');
    }
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();
        var context = new ValidationContext(value, null, null);
        var property = validationContext.ObjectType.GetProperty(_otherProperty);
        if (property == null)
        {
            return new ValidationResult(string.Format(
                CultureInfo.CurrentCulture,
                "Unknown property {0}",
                new[] { _otherProperty }
            ));
        }
        var otherPropertyValue = property.GetValue(validationContext.ObjectInstance, null);

        if (otherPropertyValue == null || _otherValues == null) return null;
        try
        {
            if (!_otherValues.Contains((otherPropertyValue as string).ToLower())) return null;
        }
        catch
        {
            if (!_otherValues.Contains(otherPropertyValue.ToString().ToLower())) return null;
        }

        Validator.TryValidateObject(value, context, results, true);

        if (results.Count != 0)
        {
            var compositeResults = new ValidationResult(JsonSerializer.Serialize(results.Select(x => new string[2] { x.MemberNames.First(), x.ErrorMessage })));
            return compositeResults;
        }

        return ValidationResult.Success;
    }
}

Then you have to add this data annotation to your model like you would with any other validation attribute.

Model

public class Model
{
    [Required]
    public primitiveType Prop1 { get; set; }
    [ValidateObjectIfOtherFieldIn("Prop1", "1,2,3")]
    public Submodel SubmodelProp { get; set; }
}
public class Submodel
{
    [Required] //example for ValidationAttribute
    public string Name { get; set; }
    public string Foo { get; set; }
}

Now when you get your model back in an Action on your controller, you need to immediately throw away the ModelState errors and revalidate.

Action in your controller

public ActionResult YourAction(YourModelClass model)
    {
        ModelState.Clear();
        if (!TryValidateModel(model))
        {
            //add in your invalid model logic here
            //if you want to keep the json for binding the errors to the properties of the submodel instead of the property itself, keep in this next section, otherwise remove it
            //json was unfortunately the easiest way I thought of for binding the errors to the submodel properties
            string[] keys = ModelState.Keys.ToArray();
            foreach (string key in keys)
            {
                if (ModelState[key]?.Errors?.Any() ?? false)
                {
                    for (int i = 0; i < ModelState[key].Errors.Count; i++)
                    {
                        try
                        {
                            Newtonsoft.Json.Linq.JToken.Parse(ModelState[key].Errors[i].ErrorMessage); //should throw error if not valid json
                            string[][] results = System.Text.Json.JsonSerializer.Deserialize<string[][]>(ModelState[key].Errors[i].ErrorMessage);
                            foreach (string[] result in results)
                            {
                                ModelState.AddModelError(key + "." + result[0], result[1]);
                            }
                            ModelState[key].Errors.RemoveAt(i);
                            i--;
                            if (ModelState[key]?.Errors?.Any() ?? true) ModelState[key].Errors.Clear();
                        }
                        catch { }
                    }
                }
            }
        }
    }
Brandon
  • 51
  • 4