POST request object validation with IValidatableObject in .NET core
Validate before reaching action method
In .NET Core, when you need to validate incoming data in an API, one of the common approaches is to use the IValidatableObject
interface. This interface provides a way to implement custom validation logic in your model class, which is then automatically invoked during model binding in your API endpoints.
In this blog, we'll walk through how to perform API POST request validation using IValidatableObject
in .NET Core with a practical example.
What is IValidatableObject
?
IValidatableObject
is an interface that allows you to implement custom validation logic within your model class. It provides a method called Validate
where you can define validation rules that are more complex than just data annotations.
Why Use IValidatableObject
?
It allows you to define validation logic that depends on the values of multiple properties in the model.
You can implement conditional validations that go beyond simple attribute-based validation (like
[Required]
,[StringLength]
, etc.).
Example: REST API POST Validation with IValidatableObject
We will create a simple Item API where you can submit a POST request to create an item. The item will be validated using IValidatableObject
.
Step 1: Define the Model Class
Let's create a model class Item
that implements the IValidatableObject
interface.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public class Item : IValidatableObject
{
[Required]
public string Name { get; set; }
[Range(0.01, double.MaxValue, ErrorMessage = "The price must be greater than 0.")]
public decimal Price { get; set; }
[Required]
public DateTime CreatedDate { get; set; }
public string Category { get; set; }
// Custom validation logic
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
// Rule: Category must be provided if price is greater than 100
if (Price > 100 && string.IsNullOrWhiteSpace(Category))
{
validationResults.Add(new ValidationResult(
"Category is required if the price is greater than 100.",
new[] { nameof(Category) }));
}
// Rule: CreatedDate cannot be in the future
if (CreatedDate > DateTime.Now)
{
validationResults.Add(new ValidationResult(
"The Created Date cannot be in the future.",
new[] { nameof(CreatedDate) }));
}
return validationResults;
}
}
In this example:
We use standard attributes like
[Required]
and[Range]
for basic validation.We implement
IValidatableObject.Validate
for more complex validation:If the
Price
is greater than 100,Category
must be specified.CreatedDate
cannot be set to a future date.
Step 2: Create the Controller
Now, let's create a controller for handling POST requests. The validation will automatically trigger when the model binding occurs.
using Microsoft.AspNetCore.Mvc;
using System.Linq;
[ApiController]
[Route("api/[controller]")]
public class ItemsController : ControllerBase
{
[HttpPost]
public IActionResult CreateItem([FromBody] Item item)
{
// Model state validation
if (!ModelState.IsValid)
{
// Return validation errors if the model is invalid
return BadRequest(ModelState);
}
// If validation passes, return a success response
return Ok(new { Message = "Item created successfully", Item = item });
}
}
Step 3: Test the API
You can test this API using tools like Postman or cURL.
Successful Request Example:
POST /api/items
Content-Type: application/json
{
"name": "Laptop",
"price": 150,
"createdDate": "2023-09-20T10:00:00",
"category": "Electronics"
}
This request passes all the validation rules, so you'll get a response:
{
"message": "Item created successfully",
"item": {
"name": "Laptop",
"price": 150,
"createdDate": "2023-09-20T10:00:00",
"category": "Electronics"
}
}
Invalid Request Example (price > 100, but no category):
POST /api/items
Content-Type: application/json
{
"name": "Laptop",
"price": 150,
"createdDate": "2023-09-20T10:00:00"
}
This request will fail validation because Category
is required if the price is greater than 100.
{
"category": [
"Category is required if the price is greater than 100."
]
}
Another Invalid Request (CreatedDate is in the future):
POST /api/items
Content-Type: application/json
{
"name": "Laptop",
"price": 50,
"createdDate": "2024-12-31T10:00:00"
}
This request will fail validation because CreatedDate
cannot be in the future:
{
"createdDate": [
"The Created Date cannot be in the future."
]
}
Step 4: Handling Validation in .NET Core
.NET Core will automatically validate the model and populate the ModelState
with any validation errors. If the model fails validation, you can easily check this in your controller using ModelState.IsValid
and return a BadRequest
response with validation errors.
Advantages of Using IValidatableObject
Centralized Validation: Validation logic is kept in the model, so it’s easier to manage and modify without needing to add it separately in controllers or services.
Complex Logic: You can validate based on multiple properties or complex rules, which isn’t possible with simple data annotations alone.
Conditional Validations: You can implement validations that only trigger under certain conditions (e.g., requiring a field based on another field’s value).
Conclusion
Using IValidatableObject
in .NET Core provides a powerful way to handle complex validation rules beyond the basic [Required]
and [Range]
attributes. This pattern allows you to maintain clean, reusable, and maintainable code by encapsulating validation logic within your model classes.
With the example above, you can now implement custom validation for POST requests in your API. This approach ensures that your data is validated properly before processing, preventing invalid data from being stored or causing errors in your application.