How to manually validate a model in a controller in ASP.NET Core

By FoxLearn 2/4/2025 8:39:49 AM   32
Validating a model manually in ASP.NET Core can be approached in different ways, depending on your needs. You may want to validate a model object using its validation attributes, or you might need to apply custom validation logic.

In either case, you can handle validation errors by adding them to the ModelState.

Using TryValidateModel() for attribute-based validation

If you need to validate a model based on the validation attributes defined on its properties, you can use the TryValidateModel() method. This method will check the model's attributes and add any validation errors to the ModelState.

[ApiController]
[Route("[controller]")]
public class ProductController : ControllerBase
{
    [HttpPost]
    public IActionResult Post()
    {
        var product = GetProductFromRequest(Request.Body);

        if (!TryValidateModel(product))
        {
            return ValidationProblem();
        }

        // Process the valid product

        return Ok();
    }
    
    // Rest of controller logic
}

Internally, TryValidateModel() uses ObjectValidator.Validate() to validate the model and populate ModelState with any errors. If validation fails, the ValidationProblem() method returns a 400 Bad Request response with details about the validation errors, like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-8a8231ab7dfe2a6c67b1c61c8b62ba9b-5482fb5c38154b07-00",
    "errors": {
        "Price": [
            "The field Price must be greater than 0."
        ]
    }
}

Manually Adding Validation Errors to ModelState

If you want to manually perform validation logic, such as ensuring a property meets custom conditions, you can add errors directly to ModelState.

[HttpPost]
public IActionResult Post(Product product)
{
    // Step 1 - Manually validate the product fields
    if (product.Price <= 0)
    {
        ModelState.AddModelError(nameof(product.Price), "Price must be greater than zero.");
    }

    // Step 2 - Return validation problem if any error exists
    if (!ModelState.IsValid)
    {
        return ValidationProblem();
    }

    // Process the valid product

    return Ok();
}

In this example, if the Price field is invalid (less than or equal to zero), an error is added to ModelState using AddModelError(). The response returned by ValidationProblem() will look like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-dc315a729467fca0b85f92383956f67f-09f21b627acbd8e6-00",
    "errors": {
        "Price": [
            "Price must be greater than zero."
        ]
    }
}

Combining Attribute Validation and Custom Logic

You don't have to choose between using validation attributes and performing custom validation.

[HttpPost]
public IActionResult Post(Product product)
{
    // Use TryValidateModel for attribute-based validation
    if (!TryValidateModel(product))
    {
        return ValidationProblem();
    }

    // Apply custom validation logic
    if (product.ManufactureDate > DateTime.UtcNow)
    {
        ModelState.AddModelError(nameof(product.ManufactureDate), "Manufacture date cannot be in the future.");
    }

    // Return validation error if needed
    if (!ModelState.IsValid)
    {
        return ValidationProblem();
    }

    // Process the valid product

    return Ok();
}

Handling Automatic Validation

In ASP.NET Core, if you use the [ApiController] attribute, automatic model validation occurs, meaning you get automatic 400 responses for invalid models. However, you may wish to handle this manually in some cases, for example, to customize the error message or to modify the model before re-validating.

To disable automatic validation, you can configure it in Program.cs or Startup.cs like so:

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

Now you can manually check ModelState.IsValid and return a custom error response:

[HttpPost]
public IActionResult Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest("Invalid product data.");
    }

    // Rest of the method logic
}

If you want to attempt to fix the validation problem and re-validate the model, you can clear the ModelState and update the model before calling TryValidateModel() again:

[HttpPost]
public IActionResult Post(Product product)
{
    if (!ModelState.IsValid)
    {
        ModelState.Clear();

        // Example fix: Setting a default value for the missing field
        product.ManufactureDate = DateTime.UtcNow.AddYears(-1);

        if (!TryValidateModel(product))
        {
            return ValidationProblem();
        }
    }

    // Process the valid product

    return Ok();
}

With these techniques, you can have full control over your model validation in ASP.NET Core, whether you're using built-in validation attributes, implementing custom logic, or even combining both.