Advanced Model Validation Techniques in .NET Applications
The Evolution of Parameter Validation in API Development
In modern backend API development, proper parameter validation is crucial for maintaining data integrity and security. While many developers still implement manual validation checks, .NET provides robust validation frameworks that can significently simplify this process.
Consider a traditional validation approach:
public class UserProfile
{
public int UserId { get; set; }
public string DisplayName { get; set; }
public string EmailAddress { get; set; }
}
public ApiResponse ProcessUserData(UserProfile data)
{
if (data.UserId <= 0)
{
return ApiResponse.Error("Invalid user identifier");
}
if (string.IsNullOrWhiteSpace(data.DisplayName))
{
return ApiResponse.Error("Display name is required");
}
if (string.IsNullOrWhiteSpace(data.EmailAddress) || !data.EmailAddress.Contains("@"))
{
return ApiResponse.Error("Invalid email format");
}
// Business logic continues...
}
This approach becomes cumbersome as parameter count increases. An improved pattern might encapsulate validation within the model:
public class UserProfile
{
public int UserId { get; set; }
public string DisplayName { get; set; }
public string EmailAddress { get; set; }
public (bool IsValid, string ErrorMessage) ValidateModel()
{
if (UserId <= 0)
return (false, "User identifier must be positive");
if (string.IsNullOrWhiteSpace(DisplayName))
return (false, "Display name cannot be empty");
if (string.IsNullOrWhiteSpace(EmailAddress) || !EmailAddress.Contains("@"))
return (false, "Email address format is invalid");
return (true, null);
}
}
Leveraging Built-in Validation Attributes
Both .NET Framework and .NET Core support declarative validation through DataAnnotations. Add the namespace reference: using System.ComponentModel.DataAnnotations;
public class UserRegistrationModel
{
[Range(1, int.MaxValue, ErrorMessage = "User ID must be positive")]
public int AccountId { get; set; }
[Required(ErrorMessage = "Username is mandatory")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")]
public string LoginName { get; set; }
[EmailAddress(ErrorMessage = "Invalid email address format")]
public string ContactEmail { get; set; }
}
Implementing Custom Validation Logic
When standard attributes don't meet your requirements, implement IValidatableObject for complex validation scenarios:
public class ComplexOrderModel : IValidatableObject
{
public int OrderNumber { get; set; }
public decimal TotalAmount { get; set; }
public DateTime DeliveryDate { get; set; }
public List<OrderItem> Items { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (OrderNumber <= 0)
{
results.Add(new ValidationResult("Order number must be positive", new[] { nameof(OrderNumber) }));
}
if (TotalAmount <= 0)
{
results.Add(new ValidationResult("Order total must be greater than zero", new[] { nameof(TotalAmount) }));
}
if (DeliveryDate <= DateTime.Now)
{
results.Add(new ValidationResult("Delivery date must be in the future", new[] { nameof(DeliveryDate) }));
}
if (Items == null || !Items.Any())
{
results.Add(new ValidationResult("Order must contain at least one item", new[] { nameof(Items) }));
}
return results;
}
}
public class OrderItem
{
[Required]
public string ProductCode { get; set; }
[Range(1, 1000, ErrorMessage = "Quantity must be between 1 and 1000")]
public int Quantity { get; set; }
}
Centralized Validation in .NET Core Using Action Filters
Implement validation at the framework level to eliminate code duplication and maintain clean controllers:
public class ModelValidationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errorMessages = context.ModelState
.Where(x => x.Value.Errors.Count > 0)
.SelectMany(x => x.Value.Errors)
.Select(e => e.ErrorMessage)
.ToList();
var response = new ApiErrorResult
{
StatusCode = 400,
Message = "Validation failed",
Errors = errorMessages
};
context.Result = new BadRequestObjectResult(response);
}
base.OnActionExecuting(context);
}
}
// Apply globally or per-controller
[ModelValidationFilter]
public class OrdersController : ControllerBase
{
[HttpPost]
public IActionResult CreateOrder([FromBody] ComplexOrderModel order)
{
// No validation needed here - handled by filter
return Ok(new { Message = "Order processed successfully" });
}
}
Validation Interception in ASP.NET Web API
For .NET Framework applications, implement an action filter for Web API:
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
public class ValidationInterceptorAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.Any() && !actionContext.ModelState.IsValid)
{
var validationErrors = actionContext.ModelState
.Where(kvp => kvp.Value.Errors.Count > 0)
.SelectMany(kvp => kvp.Value.Errors)
.Select(err => err.ErrorMessage)
.FirstOrDefault();
var errorResponse = new
{
Success = false,
Message = validationErrors ?? "Request validation failed",
Timestamp = DateTime.UtcNow
};
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.BadRequest, errorResponse);
}
base.OnActionExecuting(actionContext);
}
}
By implementing these validation strategies, you achieve separation of concerns, reeduce code duplication, and ensure consistent validation across your API endpoints.