1

I am having issues with my WebApi and the ModelState. Whenever I send data to my API it throws ModelState errors on all navigation properties. This is my model:

public class Student
{
    public int StudentID { get; set; }
    public string StudentName { get; set; }
    public int StandardId { get; set; }
    public Standard Standard { get; set; }
}

public class Standard
{
    public int StandardId { get; set; }
    [Required]
    public string StandardName { get; set; }

    public ICollection<Student> Students { get; set; }
}

As you can see I did not assign the virtual keyword which should not be an issue since I don't want lazy loading.

This is my API:

[HttpPut, Route("updateStudent/{id:int}")]
public IHttpActionResult Put(int id, Student student)
{
    // ModelState throws an error here!!
    if (ModelState.IsValid && id == student.StudentId) {
    ...
    }
}

This is how my request looks:

{
   "StudenID": 0,
   "StudentName": "Tom",
   "StandardId": 1
}

When I inspect how the model looks like when it arrives in the api, I can see that all the data is populated and it basically replaces the Standard property with a new Standard instance. However, I don't want it to throw the validation errors of course.

Edit: It throws the error saying that the StandardName property is required. Obviously this is a proprty part of the navigation property. I don't want to checkthe navigation property for errors.

Tom el Safadi
  • 6,164
  • 5
  • 49
  • 102

3 Answers3

2

You should create a new model which should contain only those items that will be posted as input and communicate it with your Data Model in the controller action. You can create a ViewModel in your case like:

public class StudentViewModel
{
    public int StudentID { get; set; }
    public string StudentName { get; set; }
    public int StandardId { get; set; }
}

and accordingly change the action method parameter.

[HttpPut, Route("updateStudent/{id:int}")]
public IHttpActionResult Put(int id, StudentViewModel student)
{

    if (ModelState.IsValid && id == student.StudentId) {
    ...
    // map with your Student Entity here  as per your needs
    }
}

For a work around at the moment you could remove those Standard entity properties from ModelState:

public IHttpActionResult Put(int id, Student student)
{
     // ignore StandardName property
     ModelState.Remove(nameof(student.Standard.StandardName));

     if (ModelState.IsValid && id == student.StudentId) {
    ...
}
Ehsan Sajjad
  • 61,834
  • 16
  • 105
  • 160
  • 1
    So if I have about 50 models, I need to create another 50 viewmodels? How is that maintainable? – Tom el Safadi Mar 12 '19 at 06:41
  • @TomelSafadi that is the standard approach we don't expose the data models directly to controller actions (means normally it is not recommended to do so) – Ehsan Sajjad Mar 12 '19 at 06:43
  • @TomelSafadi updated the post with work around too, but as i said that is not recommended approach – Ehsan Sajjad Mar 12 '19 at 06:45
  • @TomelSafadi: "if I have about 50 models, I need to create another 50 viewmodels?" - No, you should create ViewModels for Views, not for Models. Your design process is wrong. – H H Mar 12 '19 at 07:03
  • 1
    Definetly makes sense but since every model contains a related API interface, I would need to create a ViewModel for every single Model. I understand the points mentioned, I am just wondering how I am going to maintain that – Tom el Safadi Mar 12 '19 at 07:12
  • maintenance in what perspective? – Ehsan Sajjad Mar 12 '19 at 07:14
  • 1
    If I need to add a property for example to a model or make changes, I would need to apply the changes to the ViewModel too. Then some Models contain about 30 properties, that means that I need to map all my properties from the ViewModel back into the model. – Tom el Safadi Mar 12 '19 at 07:15
  • it is not necessary for the view model to have all the properties as of data model, unless you need that property in View – Ehsan Sajjad Mar 12 '19 at 07:20
  • 1
    I am convinced about it and will change it accordingly! Thanks for the input of everyone. This article explained it quite well to me: https://stackoverflow.com/a/7777059/5203853 – Tom el Safadi Mar 12 '19 at 07:26
0

Adding a [Required] attribute to model property will subject it to validation. Removing it will solve the issue. However, if you cannot change it like if it comes from an imported class DLL which you cannot modify in your solution, try creating a separate model for your request model which the StandardName property has no [Required] attribute.

0

I found a workaround. I was having this issue with simple model classes.The Create CRUD page would always fail because Enrollments was always null.

public class Course
{
    public int ID { get; set; }
    public string Name { get; set; }

    public ICollection<Enrollment> Enrollments { get; set; }
}
public class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    [Display(Name = "Is Teacher?")]
    public bool IsTeacher { get; set; }

    public ICollection<Enrollment> Enrollments { get; set; }
}
public class Enrollment
{
    public int ID { get; set; }
    public int CourseID { get; set; }
    public int PersonID { get; set; }

    public Course Course { get; set; }
    public Person Person { get; set; }
}
  • For Course and Person, the Create CRUD page would always fail because Enrollments was always null. There was a simple workaround for this problem: changing
public ICollection<Enrollment> Enrollments { get; set; }

to

public ICollection<Enrollment> Enrollments { get; set; } = new List<Enrollment>();
  • The second issue I was having was with enrollments. I tried changing
public Course Course { get; set; }
public Person Person { get; set; }

to

public Course Course { get; set; } => new() { Name = "" };
public Person Person { get; set; } => new() { Name = "" };

That did not work because it would just add blank entries to the Courses and Person list. This told me it was using the Course and Person fields rather than CourseID and PersonID fields. The solution was to add the ID.

What ended up solving the problem was changing it to:

public Course Course { get => _course == null && CourseID != default ? new() { ID = CourseID, Name = "" } : _course; set => _course = value; }
public Person Person { get => _person == null && PersonID != default ? new() { ID = PersonID, Name = "" } : _person; set => _person = value; }

Course _course; Person _person;

This allowed both the Index and Create pages to work correctly.

The final code looked like:

public class Course
{
    public int ID { get; set; }
    public string Name { get; set; }

    public ICollection<Enrollment> Enrollments { get; set; } = new List<Enrollment>();
}
public class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    [Display(Name = "Is Teacher?")]
    public bool IsTeacher { get; set; }

    public ICollection<Enrollment> Enrollments { get; set; } = new List<Enrollment>();
}
public class Enrollment
{
    public int ID { get; set; }
    public int CourseID { get; set; }
    public int PersonID { get; set; }

    public Course Course { get => _course == null && CourseID != default ? new() { ID = CourseID, Name = "" } : _course; set => _course = value; }
    public Person Person { get => _person == null && PersonID != default ? new() { ID = PersonID, Name = "" } : _person; set => _person = value; }

    Course _course; Person _person;
}

As a summary, to make navigational properties, the following guides:

  • With a list, you can safely set it to be empty.

  • With a one-to-one relation, it is more complicated and should look something like this:

public int FieldID { get; set; }

public FieldType Field
{
    get => _field == null && FieldID != default ? new() { ID = FieldID, ... } : _field;
    set => _field = value;
}

FieldType _field;

where ... just sets all of the nullable fields to a non-null value to make sure ModelState.IsValid is true.

I personally disagree with the logic about model view classes. Following this logic every time I modify the functionality of the database (eg. a field name) I have to manually change all of the model view classes to match. I understand what was said about POST attacks but I am referring to situations where all of the fields of the relevant dataset can be modified. Usually, I would only give access to these CRUD models to administrators of the data in question.

E_net4
  • 27,810
  • 13
  • 101
  • 139
trinalbadger587
  • 1,905
  • 1
  • 18
  • 36