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 a400 Bad Requestwith 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.csusingbuilder.Services.AddValidatorsFromAssemblyContaining<T>().
4. Custom Validation Logic
For unique business requirements, you have two primary options:
Custom Attributes: Create a class inheriting from
ValidationAttributeand override theIsValidmethod.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
IValidatableObjectinterface and itsValidatemethod 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
| Type | Best For | Implementation |
|---|---|---|
| Syntactic | Correct format and structure | Data Annotations, Regex, JSON Schema |
| Semantic | Business context (e.g., End Date > Start Date) | Custom Attributes, FluentValidation |
| Schema | Complex nested objects and type compliance | JSON 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 AddModelError. 3. 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,anddecimal, Web API attempts to get the value from theURI(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 bodyusing 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
GETrequests 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
TypeConverterto 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
TypeConverterand overrideCanConvertFromandConvertFrom. - Usage: Decorate your class with the
[TypeConverter(typeof(MyConverter))]attribute. - Benefit: No need to use
[FromUri]on action parameters anymore.
- Implementation: Inherit from
// 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
IModelBinderinterface 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
IModelBinderinterface and itsBindModelmethod. - Registration:
- Attribute: Add
[ModelBinder(typeof(MyBinder))]to the parameter or the type itself. - Global: Register a
ModelBinderProviderinWebApiConfig.Register.
- Attribute: Add
- Implementation: Implement the
Value Providers: Implement
IValueProviderto tell the model binder where to fetch data from (e.g., cookies or session state).- Step 1: Implement
IValueProviderto define how to fetch the value. - Step 2: Implement
ValueProviderFactoryto instantiate your provider. - Registration: Add the factory to
HttpConfiguration.Servicesin your startup class.
- Step 1: Implement
HttpParameterBinding: Create a custom binding class for advanced scenarios, such as binding custom objects directly from headers (e.g., ETags).
- Implementation: Inherit from
HttpParameterBindingand overrideExecuteBindingAsync. - Usage: Create a custom attribute that inherits from
ParameterBindingAttributeand returns your binding class. - Global Rules: You can also add rules to
config.ParameterBindingRulesto apply the binding based on parameter type or action name.
- Implementation: Inherit from
Summary of Options
| **Option ** | Implementation | Best Use Case |
|---|---|---|
| Type Converter | TypeConverter class | URI-to-Object (e.g., ?point=10,20 to GeoPoint) |
| Model Binder | IModelBinder interface | Custom mapping from Route/Query/Body |
| Value Provider | IValueProvider interface | Fetching data from non-standard sources (Cookies) |
| Parameter Binding | HttpParameterBinding | Direct manipulation of the binding process |
PATCH method in ASP.NET Web API
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
| Operation | Description |
|---|---|
| replace | Updates an existing property value. |
| add | Adds a property or appends to an array. |
| remove | Deletes a property or array element. |
| move/copy | Transfers values from one path to another. |
| test | Checks if a value exists before applying the rest of the patch. |
Comments
Post a Comment