Validation & parameter Binding in web API

 

Validation in web API

Validation in Web API ensures that incoming data—whether in the request body, query string, or route—is accurate, complete, and secure before reaching your business logic

This "first line of defence" prevents database corruption, application crashes, and security vulnerabilities like SQL injection.

In modern frameworks like ASP.NET Core, validation typically follows these approaches:

1. Built-in Data Annotations

You can decorate model properties with attributes from the System.ComponentModel.DataAnnotations namespace.

  • [Required]: Ensures the field is not null or empty.
  • [StringLength]: Sets maximum and minimum character limits.
  • [Range]: Restricts numeric values between a minimum and maximum.
  • [RegularExpression]: Validates input against a specific pattern (e.g., phone numbers).

2. Automatic Validation with [ApiController]

When a controller is marked with the [ApiController] attribute, the framework automatically checks ModelState.IsValid before your action method runs.

  • Result: If validation fails, it automatically returns a 400 Bad Request with a standardized Problem Details response containing error details.

3. Fluent Validation (Third-Party)

Many developers prefer the FluentValidation library for its cleaner separation of concerns.

  • Pros: It allows for complex, asynchronous rules and keeps validation logic outside of your DTO (Data Transfer Object) classes.

  • Usage: You create a separate class inheriting from AbstractValidator to define rules using a fluent API.

  • Setup: Create a separate validator class inheriting from AbstractValidator<T>.

  • Registration: Add it to your service container in Program.cs using builder.Services.AddValidatorsFromAssemblyContaining<T>().

4. Custom Validation Logic

For unique business requirements, you have two primary options:

  • Custom Attributes: Create a class inheriting from ValidationAttribute and override the IsValid method.

  • IValidatableObject: Implement this interface on your model class to perform class-level validation (checking multiple properties against each other).

1. Custom Validation Attribute

This is the standard way to create a reusable rule (e.g., ensuring a string contains no special characters).

Step 1: Inherit from ValidationAttribute

Create a class that inherits from System.ComponentModel.DataAnnotations.ValidationAttribute.

Step 2: Override IsValid

Implement your logic by overriding the IsValid method.

public class ClassicMovieAttribute : ValidationAttribute
{
    private readonly int _releaseYear;
    public ClassicMovieAttribute(int releaseYear) => _releaseYear = releaseYear;

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is DateTime releaseDate && releaseDate.Year > _releaseYear)
        {
            return new ValidationResult($"Classic movies must be released before {_releaseYear}.");
        }
        return ValidationResult.Success;
    }
}

Step 3: Apply to Model

Decorate your model property with the new attribute.

public class Movie {
    [ClassicMovie(1960)]
    public DateTime ReleaseDate { get; set; }
}

2. IValidatableObject Interface

Use this approach for "self-validating" models where rules depend on multiple fields (e.g., EndDate must be after StartDate).

  • Implementation: Implement the IValidatableObject interface and its Validate method directly in your model.
public class Appointment : IValidatableObject
{
    public DateTime Start { get; set; }
    public DateTime End { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (End < Start)
        {
            yield return new ValidationResult("End date must be after start date.", new[] { nameof(End) });
        }
    }
}

5. Validation at the Gateway

For high-scale production systems, basic request validation can be performed at the API Gateway (e.g., AWS API Gateway). This catches malformed requests at the edge, reducing unnecessary load on your backend services.

Summary Table: Validation Types

TypeBest ForImplementation
SyntacticCorrect format and structureData Annotations, Regex, JSON Schema
SemanticBusiness context (e.g., End Date > Start Date)Custom Attributes, FluentValidation
SchemaComplex nested objects and type complianceJSON Schema, Pydantic (Python), Joi (Node.js)

Q: what is ModelState.AddModelError

Answer: ModelState.AddModelError is a method in ASP.NET Core used to manually inject validation errors into the request's state. It is essential for business logic checks that cannot be handled by simple attributes—like verifying if an email already exists in a database.

Key Usage

  • Property-Level Error: Ties an error to a specific field. ModelState.AddModelError("Email", "This email is already in use.");

  • Model-Level Error: Adds a general error not tied to any single property (use an empty string for the key). ModelState.AddModelError(string.Empty, "The overall form is invalid."); How it Works in an API Controller

1. Manual Check: You perform a check (e.g., database lookup). 2. Add Error: If the check fails, call AddModelError3. Invalidate State: Calling this method automatically sets ModelState.IsValid to false. 4. Return Response: You must manually return a BadRequest(ModelState) if you are inside an action, as the automatic [ApiController] check happens before your action runs.

[HttpPost]
public IActionResult Register(UserRegistration model)
{
    // Custom database check
    if (_userService.EmailExists(model.Email))
    {
        ModelState.AddModelError("Email", "Email is already taken.");
    }

    if (!ModelState.IsValid)
    {
        // Returns 400 Bad Request with a list of all errors
        return BadRequest(ModelState); 
    }

    // Process valid request...
    return Ok();
}

Important Notes

  • Overloads: You can pass either a string error message or a full Exception object.
  • Case Sensitivity: By default, the key should match your property name for the client to map it correctly.
  • Validation Format: For consistent API responses, you can wrap the state in ValidationProblemDetails.

Parameter Binding in ASP.NET Web API

Parameter binding in ASP.NET Web API is the process of mapping data from an HTTP request (like the URI or the request body) to the parameters of an action method in a controller. The framework uses a set of default rules and specific attributes to determine where to find the data.

1. Default Binding Rules By default, ASP.NET Web API uses the following conventions to bind parameters:

  • Simple Types: For simple types, such as int, bool, double, string, DateTime, Guid, and decimal, Web API attempts to get the value from the URI (route data and query string).

  • Complex Types: For complex types (custom classes or objects), Web API attempts to read the value from the HTTP request body using a media-type formatter (e.g., JSON or XML formatter).

2. Explicit Binding Attributes You can override the default behavior using specific attributes on the action method parameters:

  • [FromUri]: Forces Web API to read the parameter value (even a complex type) from the URI (query string and route data). This is useful for GET requests with multiple search parameters encapsulated in a single object.

  • [FromBody]: Forces Web API to read the parameter value (even a simple type) from the request body. Note that only one parameter per action method can use [FromBody], as the request body can only be read once.

  • [FromHeader]: (In ASP.NET Core) Binds a parameter to a specific HTTP header.

  • [FromRoute]: (In ASP.NET Core) Specifically binds to values in the route template.

Example Consider a ProductsController with a Post method:

public class ProductsController : ApiController
{
    // Default behavior: id from URI, product from body
    public HttpResponseMessage Post(int id, [FromBody] Product product) 
    {
        // ...
    }

    // Custom binding: location from URI even though it's complex
    public HttpResponseMessage Get([FromUri] GeoPoint location)
    {
        // ...
    }
}

For more advanced customization, you can implement custom TypeConverter classes or the IModelBinder interface to define specific binding logic for your custom types.

3. Customization Options

If standard attributes are insufficient, you can extend the binding logic:

  • i.) Type Converters: Create a custom TypeConverter to make Web API treat a class as a "simple type," allowing it to bind from a string in the URI (e.g., ?point=10,20).
    • Implementation: Inherit from TypeConverter and override CanConvertFrom and ConvertFrom.
    • Usage: Decorate your class with the [TypeConverter(typeof(MyConverter))] attribute.
    • Benefit: No need to use [FromUri] on action parameters anymore.
// The class to bind
[TypeConverter(typeof(GeoPointConverter))]
public class GeoPoint {
    public double Lat { get; set; }
    public double Lon { get; set; }
}

// The Converter
public class GeoPointConverter : TypeConverter {
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {
        return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {
        if (value is string s) {
            var parts = s.Split(',');
            return new GeoPoint { Lat = double.Parse(parts[0]), Lon = double.Parse(parts[1]) };
        }
        return base.ConvertFrom(context, culture, value);
    }
}

Usage: Web API now automatically binds public IHttpActionResult Get(GeoPoint point) from the URI.

  • ii) Model Binders: Implement the IModelBinder interface to define exactly how an object should be constructed from the request data, such as custom parsing of a comma-separated string.

    • Implementation: Implement the IModelBinder interface and its BindModel method.
    • Registration:
      • Attribute: Add [ModelBinder(typeof(MyBinder))] to the parameter or the type itself.
      • Global: Register a ModelBinderProvider in WebApiConfig.Register.
  • Value Providers: Implement IValueProvider to tell the model binder where to fetch data from (e.g., cookies or session state).

    • Step 1: Implement IValueProvider to define how to fetch the value.
    • Step 2: Implement ValueProviderFactory to instantiate your provider.
    • Registration: Add the factory to HttpConfiguration.Services in your startup class.
  • HttpParameterBinding: Create a custom binding class for advanced scenarios, such as binding custom objects directly from headers (e.g., ETags).

    • Implementation: Inherit from HttpParameterBinding and override ExecuteBindingAsync.
    • Usage: Create a custom attribute that inherits from ParameterBindingAttribute and returns your binding class.
    • Global Rules: You can also add rules to config.ParameterBindingRules to apply the binding based on parameter type or action name.

Summary of Options

**Option **ImplementationBest Use Case
Type ConverterTypeConverter classURI-to-Object (e.g., ?point=10,20 to GeoPoint)
Model BinderIModelBinder interfaceCustom mapping from Route/Query/Body
Value ProviderIValueProvider interfaceFetching data from non-standard sources (Cookies)
Parameter BindingHttpParameterBindingDirect manipulation of the binding process

PATCH method in ASP.NET Web API

https://jsonpatch.com/

In ASP.NET Web API, the PATCH method is used for partial updates to a resource, unlike PUT which typically replaces the entire object.

Implementation Steps (ASP.NET Core)

Implementing PATCH in ASP.NET Core Web API is supported out-of-the-box using the Microsoft.AspNetCore.JsonPatch library.

1.Install the NuGet Package: Install the appropriate package based on your project's JSON serializer (System.Text.Json or Newtonsoft.Json). or Install NuGet Package: Install Microsoft.AspNetCore.Mvc.NewtonsoftJson (for ASP.NET Core) or Marvin.JsonPatch (for legacy Web API 2).

dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson
# OR for Newtonsoft.Json (legacy in .NET 9+ but widely used)
dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson

2. Configure Program.cs: If you are using Newtonsoft.Json, you need to configure your application services to use it for JSON Patch requests.

var builder = WebApplication.CreateBuilder(args);

// Add controllers and configure Newtonsoft.Json if needed
builder.Services.AddControllers()
    .AddNewtonsoftJson(); // Required for Newtonsoft.Json support

var app = builder.Build();
// ... rest of your app configuration
app.Run();

3. Create the Controller Action: In your API controller, define an action method decorated with the [HttpPatch] attribute. This method accepts a JsonPatchDocument<T> as a parameter, where T is your model or DTO.

using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using System.Linq;

[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
    // Assume you have a data source context or service here
    private readonly AppDbContext _context; 

    public CustomersController(AppDbContext context)
    {
        _context = context;
    }

    // PATCH api/customers/{id}
    [HttpPatch("{id}")]
    public IActionResult Patch(int id, [FromBody] JsonPatchDocument<Customer> patchDoc)
    {
        if (patchDoc == null)
        {
            return BadRequest();
        }

        var customer = _context.Customers.FirstOrDefault(c => c.Id == id);
        if (customer == null)
        {
            return NotFound();
        }

        // Apply the patch operations to the retrieved entity
        patchDoc.ApplyTo(customer, ModelState);

        // Validate the model state after applying changes
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        _context.SaveChanges(); // Save changes to the database

        return NoContent(); // Or return the updated object
    }
}

4. Send the Request (Client-side): The client sends a JSON array with the specific property changes. Header: Content-Type: application/json-patch+json Body:

[
  { "op": "replace", "path": "/firstName", "value": "NewName" },
  { "op": "remove", "path": "/temporaryField" }
]

Summary of Operations

OperationDescription
replaceUpdates an existing property value.
addAdds a property or appends to an array.
removeDeletes a property or array element.
move/copyTransfers values from one path to another.
testChecks if a value exists before applying the rest of the patch.

Comments

Popular posts from this blog