banneh
banneh

Reputation: 77

Validating DTOs with ModelState

I'm creating .NET Core WEB API project together with Entity Framework using Code First approach. I have troubles validating input from the requests, as ModelState validation is always true.

My application consists of 3 layers.

Example DataModel in DAL:

public class Group
{
    [Key]
    [Required]
    public long GroupId { get; set; }
    [Required]
    public string Name { get; set; }
    [Required(AllowEmptyStrings = false)]
    public string Description { get; set; }
    public DateTime CreationDate { get; set; }
    public bool IsActive { get; set; }
}

Corresponding DTO:

public class GroupDto
{
    public long GroupId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

Controller method:

[HttpPost]
public IActionResult Post([FromBody] GroupDto groupDto)
{
    Group group = _mapper.Map<Group>(groupDto);
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    _groupService.Add(group);

    groupDto = _mapper.Map<GroupDto>(group);

     return Ok(groupDto);
}

As far as I understand in current state ModelState.IsValid would always return true as GroupDto does not have any validations done via DataAnnotations.

How the DTOs should be validated? I'd like to avoid repeating the same validations in two places. Should additional custom DtoValidator be created or am I missing somtething and there is way to perform those validations.

Upvotes: 3

Views: 14892

Answers (2)

farizmamad
farizmamad

Reputation: 359

DTOs and models are different things, so the DataAnnotation attribute should be made in both of them. The subject checked by "ModelState.IsValid" is the input of the function you work on. Thus, in your code the Model being checked is the GroupDto.

[HttpPost]
public IActionResult Post([FromBody] GroupDto groupDto)
{
    Group group = _mapper.Map<Group>(groupDto);
    if (!ModelState.IsValid) // this line will check the validation inside GroupDto

Anyway, to make "model validation" clean in the controller I would suggest to use custom ValidateModelAttribute, like this.

using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace MyWeb.App.Filter
{
  public class ValidateModelAttribute : ActionFilterAttribute
  {
    public override void OnActionExecuting( HttpActionContext actionContext )
    {
      if( !actionContext.ModelState.IsValid )
      {
        actionContext.Response = actionContext.Request.CreateErrorResponse(
          HttpStatusCode.BadRequest, actionContext.ModelState ); 
        // Path to the error in request content will be sent to the client, so helpful!
      }
    }
  }
}

Then you can write the validation on controller like this.

[Route( "" )]
[HttpPost]
[ValidateModel] // here you attach the ValidateModelAttribute
[ResponseType( typeof( GroupDto ) )] // Make sure the model is written in Swagger UI
public IHttpActionResult Create( InputDto inputDto )
{
  _groupService.Add(group);

  groupDto = _mapper.Map<GroupDto>(group);

  return Ok(groupDto);
}

Hope this answers the question.

Upvotes: 0

Bryan Lewis
Bryan Lewis

Reputation: 5977

Model state validation will occur on the model being passed in, which in your case is GroupDto. Just because you eventually map to the Group class has no bearing on how the validation works. You will need to repeat the validation attributes in the DTO. This does duplicate code, but also allows you to customize the rules, since you may or may not want the exact same set in the DTO. An example of this is your primary key. For creation (POST) you don't necessarily want the GroupId to be a required field to pass in to the controller since the DB may be auto-generating that field (depending on your setup).

If you are using ASP.Net Core 2.1 or later, you can also now apply the [ApiController] attribute to the controller class and it will automatically apply the model validation rules. This eliminated the need to manually check for ModelState.IsValid. The system will automatically return a 400 Bad Request if the model is invalid. (https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-2.2#annotation-with-apicontroller-attribute)

Upvotes: 10

Related Questions