Skip to main content

Ensuring data integrity and providing clear error messages are crucial for a robust and user-friendly experience. ASP.NET Core offers built-in support for model validation using data annotations and a standardized error response format through ProblemDetails. By leveraging these features, developers can create APIs that not only validate incoming data effectively but also return detailed and structured error information, making it easier for clients to understand and handle validation issues.

# .NET API ModelState Validation Using ProblemDetails

In modern web APIs, ensuring data integrity and providing clear error messages are crucial for a robust and user-friendly experience. ASP.NET Core offers built-in support for model validation using data annotations and a standardized error response format through ProblemDetails. By leveraging these features, developers can create APIs that not only validate incoming data effectively but also return detailed and structured error information, making it easier for clients to understand and handle validation issues.

## Step-by-Step Implementation

1. **Define the model with validation attributes**.
2. **Create the `ValidationProblemDetails` class**.
3. **Configure the API to use `ValidationProblemDetails`**.
4. **Update the controller to handle validation errors**.

## 1. Define the model with validation attributes

Create a file `Models/Item.cs`:

```csharp
using System.ComponentModel.DataAnnotations;

namespace Models;

public class Item
{
    public int Id { get; set; }

    [Required]
    [StringLength(100, MinimumLength = 3)]
    public string Name { get; set; }

    [StringLength(500)]
    public string Description { get; set; }
}
```

## 2. Create the `ValidationProblemDetails` class

Create a file `Models/ValidationProblemDetails.cs`:

```csharp
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace Models;

// A custom class for detailed validation error responses
public class ValidationProblemDetails : ProblemDetails
{
    public IDictionary<string, string[]> Errors { get; set; }
}

```

## 3. Configure the API to use `ValidationProblemDetails`

Update `Program.cs` to configure the API behavior:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Repositories;
using Services;
using Microsoft.AspNetCore.Mvc;
using Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        // Customize the response returned when model state validation fails
        options.InvalidModelStateResponseFactory = context =>
        {
            var problemDetails = new ValidationProblemDetails
            {
                Status = StatusCodes.Status400BadRequest,
                Title = "One or more validation errors occurred.",
                Errors = context.ModelState.ToDictionary(
                    kvp => kvp.Key,
                    kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray())
            };

            return new BadRequestObjectResult(problemDetails)
            {
                ContentTypes = { "application/problem+json" }
            };
        };
    });

builder.Services.AddScoped<IItemRepository, ItemRepository>();
builder.Services.AddScoped<IItemService, ItemService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## 4. Update the controller to handle validation errors

Update `Controllers/ItemsController.cs`:

```csharp
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Models;
using Services;

namespace Controllers;

[ApiController]
[Route("api/[controller]")]
public class ItemsController : ControllerBase
{
    private readonly IItemService _itemService;

    public ItemsController(IItemService itemService)
    {
        _itemService = itemService;
    }

    [HttpPut("{id}")]
    public IActionResult Put(int id, [FromBody] Item item)
    {
        if (id != item.Id)
        {
            // Add this error to the ProblemdDetails response
            ModelState.AddModelError("Id", "The ID in the URL does not match the ID in the body.");
        }

        if (!ModelState.IsValid)
        {
            // Generates a ValidationProblemDetails response from the ModelState
            return ValidationProblem(ModelState);
        }

        _itemService.UpdateItem(item);

        return NoContent();
    }
```