Search code examples
asp.net-mvcentity-frameworkef-code-firstasp.net-mvc-5modelstate

ModelState.IsValid with DTO and Entity Framework code first


ASP.NET 4.5, MVC 5, EF6 code first

I'm a newbie and probably asking something long-known but I couldn't find solution on the web, probably because I don't know correct terminology to formulate this question.

To simplify things, let's say I have two model classes Teacher and Kid; One kid can be assigned only to one teacher, but one teacher can have many kids. As I'm using code first, my database is constructed from these model classes:

public class Kid
{
    [Required]
    public int Id { get; set; }
    [Required]
    public string Name { get; set; }
    [Required]
    public virtual Teacher { get; set; }    
}

public class Teacher
{
    [Required]
    public int Id { get; set; }
    [Required]
    public string Name { get; set; }
    [Required]
    public virtual ICollection<Kid> Kids { get; set; }
}

Now, I need to have a view for adding new kid with: Textbox for Kid's name; Dropdown with list of Teachers

So, I'm creating a data transfer object, specifically for that view:

public class AddNewKidViewDTO
{
    public IEnumerable<SelectListItem> Teachers { get; set; }
    public int SelectedTeacherId { get; set; }
    public Kid Kid { get; set; }
}

I also have a method for populating IEnumerable Teachers:

public AddNewKidViewDTO LoadTeachersForDropDownList()
{
    ... //get the list of Teachers
    AddNewKidViewDTO addNewKidViewDTO = new AddNewKidViewDTO();
    List<SelectListItem> selectListItems =  new List<SelectListItem>();
    foreach (teacher in Teachers)
    {
        selectListItems.Add (new SelectListItem
        {
            Text = teacher.Name.ToString(),
            Value = teacher.Id.ToString()
        });
    }
    addNewKidViewDTO.Teachers = selectListItems;
    return addNewKidViewDTO;
}

and in the view AddNewKid.cshtml

<form>
@Html.LabelFor(model => model.Kid.Name)
@Html.TextBoxFor(model => model.Kid.Name, new {id ="Name"}
<br/>
@Html.LabelFor(model => model.Kid.Teacher)
@Html.DropDownListFor(model => model.SelectedTeacherId, Model.Teachers)
</form>

Form gets submitted and in the controller I get my populated AddNewKidViewDTO model:

[HttpPost]
public ActionResult SaveNewKid (AddNewKidViewDTO addNewKidViewDTO)
{
    if (ModelState.IsValid)
    {
        //here is where the problem comes
    }
}

ModelState.IsValid in my case will always return false. Because when it starts validating AddNewKidViewDTO.Kid, Teacher is compulsory field but in my addNewKidViewDTO model it's null. I have the necessary teacher Id contained in addNewKidViewDTO.SelectedTeacherId only.

My question is, what is an elegant way to validate my model before passing to my inner business logic methods?

Any help is appreciated.


Solution

  • There are multiple possible solutions:

    1. Changing your AddNewKidViewDTO and decorating it with the DataAnnotaions for validation:

      public class AddNewKidViewDTO
      {
          public IEnumerable<SelectListItem> Teachers { get; set; }
      
          [Range(1, 2147483647)] //Int32 max value but you may change it
          public int SelectedTeacherId { get; set; }
      
          [Required]
          public string KidName { get; set; }
      }
      

      Then you can create Kid object manually in case that your model valid.

    UPDATE (to address your comment)

    If you use this approach your action will look like this:

        [HttpPost]
        public ActionResult SaveNewKid (AddNewKidViewDTO addNewKidViewDTO)
        {
            if (ModelState.IsValid)
            {
                using (var dbContext = new yourContext())
                {
                    var teacher = dbContext.Teachers.FirstOrDefault(t=>t.id == addNewKidViewDTO.SelectedTeacherId );
                    if(teacher == default(Teacher))
                    {
                        //return an error message or add validation error to model state
                    } 
    
                    //It is also common pattern to create a factory for models 
                    //if you have some logic involved, but in this case I simply
                    //want to demonstrate the approach
                    var kid = new Kid
                    {
                         Name = addNewKidViewDTO.KidName,
                         Teacher = teacher 
                    };
                    dbContext.SaveChanges();
                 }
             }
        }
    
    1. Write a custom model binder for AddNewKidViewDTO that will initialize Teacher property in Kid object so once you actually use Model.IsValid the property will be initialized.