Validation & Documentation
Implement robust validation and OpenAPI documentation
Validation & Documentation in ASP.NET
Building robust APIs requires more than just functional endpointsβyou need comprehensive validation and clear documentation. Master input validation, model binding, and API documentation with free flashcards and hands-on examples. This lesson covers data annotations, custom validators, FluentValidation, OpenAPI/Swagger integration, and best practices for creating self-documenting APIs that prevent errors and enhance developer experience.
Welcome to API Validation & Documentation π‘οΈ
When building production APIs with ASP.NET and .NET 10, two critical aspects often separate amateur implementations from professional ones: robust validation and comprehensive documentation. Validation protects your system from bad data, while documentation ensures other developers (including future you!) can understand and use your endpoints correctly.
Think of validation as the bouncer at a nightclubβchecking IDs, enforcing dress codes, and keeping troublemakers out. Documentation is the menu at a restaurantβtelling customers what's available, what ingredients are included, and what to expect.
π‘ Why This Matters: A 2023 study found that 40% of API failures stem from invalid input data, and poorly documented APIs increase integration time by 300%. Let's fix both problems!
Core Concepts: Input Validation π
Understanding Model Binding and Validation
In ASP.NET, model binding automatically maps HTTP request data (from routes, query strings, headers, or body) to C# objects. Validation ensures this data meets your business rules before processing.
The validation pipeline works like this:
βββββββββββββββββββββββββββββββββββββββββββββββ β VALIDATION PIPELINE β βββββββββββββββββββββββββββββββββββββββββββββββ€ β β β π₯ HTTP Request β β β β β π Model Binding β β β β β β Data Annotations Validation β β β β β π§ Custom Validators (IValidatableObject) β β β β β π― FluentValidation (if configured) β β β β β ββββββββββ΄βββββββββ β β β β β β β Valid β Invalid β β β β β β β β β β Controller 400 Bad Request β β Action + ValidationProblemDetails β β β βββββββββββββββββββββββββββββββββββββββββββββββ
Data Annotations: The Quick Win π
Data Annotations are attributes you place on model properties to define validation rules declaratively. They're built into .NET and require zero additional configuration.
Common validation attributes:
| Attribute | Purpose | Example |
|---|---|---|
[Required] |
Field must have a value | Required username |
[StringLength] |
String length constraints | Password 8-100 chars |
[Range] |
Numeric range | Age between 0-120 |
[EmailAddress] |
Valid email format | user@example.com |
[RegularExpression] |
Pattern matching | Phone number format |
[Compare] |
Match another property | Password confirmation |
[Url] |
Valid URL format | Website address |
[CreditCard] |
Valid credit card number | Payment processing |
Custom Validation: Beyond the Basics π―
When built-in attributes aren't enough, you have three escalation paths:
- Custom Validation Attributes - Reusable validation logic
- IValidatableObject Interface - Complex multi-property validation
- FluentValidation Library - Separation of concerns, testability, advanced scenarios
When to use each:
| Approach | Best For | Complexity |
|---|---|---|
| Data Annotations | Simple, single-property rules | β Low |
| Custom Attributes | Reusable business rules | ββ Medium |
| IValidatableObject | Cross-property validation within a model | ββ Medium |
| FluentValidation | Complex rules, dependency injection, async validation | βββ High |
π‘ Pro Tip: Start with Data Annotations for 80% of cases. Graduate to FluentValidation when you need dependency injection in validators or want to keep models clean of validation logic.
Core Concepts: API Documentation π
OpenAPI Specification (Swagger)
The OpenAPI Specification (formerly Swagger) is the industry standard for describing RESTful APIs. It's a JSON/YAML format that describes:
- Available endpoints
- HTTP methods
- Request/response formats
- Authentication requirements
- Error responses
Swashbuckle is the most popular library for generating OpenAPI documentation in ASP.NET. It:
- π Scans your controllers and models
- π Generates OpenAPI JSON
- π¨ Provides Swagger UI (interactive documentation)
- π§ͺ Enables API testing directly from the browser
βββββββββββββββββββββββββββββββββββββββββββββββ β SWAGGER/OPENAPI WORKFLOW β βββββββββββββββββββββββββββββββββββββββββββββββ€ β β β π Your Controllers & Models β β β β β π Swashbuckle Scans Code β β β β β π Generates OpenAPI JSON β β β β β ββββββββββ¬βββββββββ¬βββββββββ β β β β β β β β π¨ UI π± Mobile π€ Codegen π Tools β βSwagger Client OpenAPI Postman β β UI Libraries Generator Collections β β β βββββββββββββββββββββββββββββββββββββββββββββββ
XML Comments: Documenting Your Code
ASP.NET can read XML documentation comments from your code and include them in the OpenAPI specification. This means your documentation lives right next to your code!
Enabling XML comments:
- Add to your
.csproj:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
- Configure Swashbuckle to use the XML file
- Write triple-slash comments above your actions
Response Types and Status Codes π
Good documentation explicitly declares what HTTP status codes your endpoint returns. Use [ProducesResponseType] attributes:
| Status Code | Meaning | When to Use |
|---|---|---|
| 200 OK | Success | GET, PUT, PATCH succeeded |
| 201 Created | Resource created | POST successfully created resource |
| 204 No Content | Success, no body | DELETE succeeded |
| 400 Bad Request | Validation failed | Invalid input data |
| 404 Not Found | Resource doesn't exist | GET/PUT/DELETE non-existent ID |
| 409 Conflict | Business rule violation | Duplicate email, insufficient inventory |
| 422 Unprocessable Entity | Semantic validation failed | Valid format, invalid business logic |
| 500 Internal Server Error | Server error | Unhandled exceptions |
π§ Memory Device: "CRUD Status Codes"
- Create β 201 Created
- Read β 200 OK (or 404 Not Found)
- Update β 200 OK (or 204 No Content)
- Delete β 204 No Content (or 200 OK)
Example 1: Basic Data Annotations Validation π
Let's build a user registration endpoint with proper validation:
using System.ComponentModel.DataAnnotations;
namespace MyApi.Models;
public class RegisterUserRequest
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3,
ErrorMessage = "Username must be between 3 and 50 characters")]
[RegularExpression(@"^[a-zA-Z0-9_]+$",
ErrorMessage = "Username can only contain letters, numbers, and underscores")]
public string Username { get; set; } = string.Empty;
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8,
ErrorMessage = "Password must be at least 8 characters")]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]",
ErrorMessage = "Password must contain uppercase, lowercase, digit, and special character")]
public string Password { get; set; } = string.Empty;
[Required]
[Compare(nameof(Password), ErrorMessage = "Passwords do not match")]
public string ConfirmPassword { get; set; } = string.Empty;
[Range(13, 120, ErrorMessage = "Age must be between 13 and 120")]
public int Age { get; set; }
[Url(ErrorMessage = "Invalid URL format")]
public string? WebsiteUrl { get; set; }
}
Controller using the model:
using Microsoft.AspNetCore.Mvc;
namespace MyApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost("register")]
[ProducesResponseType(typeof(UserResponse), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public IActionResult Register([FromBody] RegisterUserRequest request)
{
// ModelState.IsValid is automatically checked by [ApiController]
// If validation fails, ASP.NET returns 400 with error details
// Your registration logic here
var userId = Guid.NewGuid();
return CreatedAtAction(
nameof(GetUser),
new { id = userId },
new UserResponse { Id = userId, Username = request.Username }
);
}
[HttpGet("{id}")]
public IActionResult GetUser(Guid id)
{
// Implementation
return Ok();
}
}
public record UserResponse
{
public Guid Id { get; init; }
public string Username { get; init; } = string.Empty;
}
What happens when validation fails:
If you send invalid data:
{
"username": "ab",
"email": "not-an-email",
"password": "weak",
"confirmPassword": "different",
"age": 5
}
ASP.NET automatically returns:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Username": ["Username must be between 3 and 50 characters"],
"Email": ["Invalid email format"],
"Password": ["Password must be at least 8 characters"],
"ConfirmPassword": ["Passwords do not match"],
"Age": ["Age must be between 13 and 120"]
}
}
π‘ Key Point: The [ApiController] attribute enables automatic model validation. Without it, you'd need to manually check ModelState.IsValid.
Example 2: Custom Validation Attribute π―
Let's create a reusable validator for future dates (useful for appointments, event scheduling, etc.):
using System.ComponentModel.DataAnnotations;
namespace MyApi.Validation;
public class FutureDateAttribute : ValidationAttribute
{
private readonly int _minimumDaysInFuture;
public FutureDateAttribute(int minimumDaysInFuture = 0)
{
_minimumDaysInFuture = minimumDaysInFuture;
ErrorMessage = _minimumDaysInFuture > 0
? $"Date must be at least {_minimumDaysInFuture} days in the future"
: "Date must be in the future";
}
protected override ValidationResult? IsValid(
object? value,
ValidationContext validationContext)
{
if (value == null)
return ValidationResult.Success; // Use [Required] separately
if (value is not DateTime dateValue)
return new ValidationResult("Invalid date format");
var minimumDate = DateTime.UtcNow.AddDays(_minimumDaysInFuture);
if (dateValue < minimumDate)
{
return new ValidationResult(
ErrorMessage ?? $"Date must be at least {_minimumDaysInFuture} days in the future"
);
}
return ValidationResult.Success;
}
}
Using the custom attribute:
using MyApi.Validation;
using System.ComponentModel.DataAnnotations;
namespace MyApi.Models;
public class CreateAppointmentRequest
{
[Required]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
[Required]
[FutureDate(1)] // Must be at least 1 day in the future
public DateTime ScheduledDate { get; set; }
[Required]
[Range(15, 480)] // 15 minutes to 8 hours
public int DurationMinutes { get; set; }
}
Testing the validator:
// Valid request
var validRequest = new CreateAppointmentRequest
{
Title = "Client Meeting",
ScheduledDate = DateTime.UtcNow.AddDays(2),
DurationMinutes = 60
};
// Invalid - date is today
var invalidRequest = new CreateAppointmentRequest
{
Title = "Client Meeting",
ScheduledDate = DateTime.UtcNow, // β Fails validation
DurationMinutes = 60
};
π§ Try This: Create a [PastDate] attribute for birthdates or historical events. The logic is nearly identicalβjust reverse the comparison!
Example 3: FluentValidation for Complex Rules π
For complex validation scenarios, FluentValidation provides a cleaner, more testable approach:
Installation:
dotnet add package FluentValidation.AspNetCore
Model (clean, no validation attributes):
namespace MyApi.Models;
public class CreateOrderRequest
{
public string CustomerEmail { get; set; } = string.Empty;
public List<OrderItem> Items { get; set; } = new();
public string ShippingAddress { get; set; } = string.Empty;
public string? PromoCode { get; set; }
}
public class OrderItem
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal PricePerUnit { get; set; }
}
Validator class:
using FluentValidation;
namespace MyApi.Validators;
public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.CustomerEmail)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format")
.MaximumLength(255);
RuleFor(x => x.Items)
.NotEmpty().WithMessage("Order must contain at least one item")
.Must(items => items.Count <= 50)
.WithMessage("Order cannot exceed 50 items");
RuleForEach(x => x.Items)
.SetValidator(new OrderItemValidator());
RuleFor(x => x.ShippingAddress)
.NotEmpty()
.MinimumLength(10)
.MaximumLength(500);
RuleFor(x => x.PromoCode)
.Matches(@"^[A-Z0-9]{6,12}$")
.WithMessage("Promo code must be 6-12 uppercase alphanumeric characters")
.When(x => !string.IsNullOrEmpty(x.PromoCode));
// Cross-property validation
RuleFor(x => x)
.Must(order => CalculateTotal(order.Items) >= 10)
.WithMessage("Order total must be at least $10")
.WithName("Order");
}
private decimal CalculateTotal(List<OrderItem> items)
{
return items.Sum(i => i.Quantity * i.PricePerUnit);
}
}
public class OrderItemValidator : AbstractValidator<OrderItem>
{
public OrderItemValidator()
{
RuleFor(x => x.ProductId)
.NotEmpty().WithMessage("Product ID is required");
RuleFor(x => x.Quantity)
.GreaterThan(0).WithMessage("Quantity must be positive")
.LessThanOrEqualTo(100).WithMessage("Maximum quantity is 100");
RuleFor(x => x.PricePerUnit)
.GreaterThan(0).WithMessage("Price must be positive")
.LessThan(100000).WithMessage("Price seems unreasonably high");
}
}
Registration in Program.cs:
using FluentValidation;
using FluentValidation.AspNetCore;
using MyApi.Validators;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Register FluentValidation
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddFluentValidationClientsideAdapters();
builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderRequestValidator>();
var app = builder.Build();
app.MapControllers();
app.Run();
Controller (no changes needed!):
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public IActionResult CreateOrder([FromBody] CreateOrderRequest request)
{
// Validation happens automatically!
// If invalid, ASP.NET returns 400 with error details
var orderId = Guid.NewGuid();
return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
}
[HttpGet("{id}")]
public IActionResult GetOrder(Guid id) => Ok();
}
Advantages of FluentValidation:
β Separation of concerns (validation logic separate from models) β Testable validators β Dependency injection support β Complex conditional rules β Async validation (e.g., checking database) β Reusable rule sets
π‘ Pro Tip: Use FluentValidation when you need to inject services (like database repositories) into validators to check uniqueness or business rules.
Example 4: Complete Swagger/OpenAPI Setup π
Let's create a fully documented API endpoint with Swagger:
Installation:
dotnet add package Swashbuckle.AspNetCore
Program.cs configuration:
using Microsoft.OpenApi.Models;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
// Configure Swagger/OpenAPI
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "My API",
Description = "A comprehensive API for managing products and orders",
Contact = new OpenApiContact
{
Name = "Support Team",
Email = "support@myapi.com",
Url = new Uri("https://myapi.com/support")
},
License = new OpenApiLicense
{
Name = "MIT License",
Url = new Uri("https://opensource.org/licenses/MIT")
}
});
// Include XML comments
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
// Add security definition (if using JWT)
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
options.RoutePrefix = string.Empty; // Serve Swagger at root URL
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Documented controller:
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace MyApi.Controllers;
/// <summary>
/// Manages product catalog operations
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
/// <summary>
/// Retrieves a product by its unique identifier
/// </summary>
/// <param name="id">The product's unique identifier</param>
/// <returns>The requested product</returns>
/// <remarks>
/// Sample request:
///
/// GET /api/products/3fa85f64-5717-4562-b3fc-2c963f66afa6
///
/// </remarks>
/// <response code="200">Returns the requested product</response>
/// <response code="404">Product not found</response>
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public ActionResult<ProductDto> GetProduct(Guid id)
{
// Implementation
var product = new ProductDto
{
Id = id,
Name = "Sample Product",
Price = 29.99m,
InStock = true
};
return Ok(product);
}
/// <summary>
/// Creates a new product
/// </summary>
/// <param name="request">Product creation details</param>
/// <returns>The newly created product</returns>
/// <remarks>
/// Sample request:
///
/// POST /api/products
/// {
/// "name": "Wireless Mouse",
/// "description": "Ergonomic wireless mouse with 6 buttons",
/// "price": 24.99,
/// "categoryId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
/// }
///
/// </remarks>
/// <response code="201">Product created successfully</response>
/// <response code="400">Invalid input data</response>
[HttpPost]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<ProductDto> CreateProduct(
[FromBody] CreateProductRequest request)
{
var productId = Guid.NewGuid();
var product = new ProductDto
{
Id = productId,
Name = request.Name,
Price = request.Price,
InStock = true
};
return CreatedAtAction(
nameof(GetProduct),
new { id = productId },
product
);
}
}
/// <summary>
/// Product data transfer object
/// </summary>
public record ProductDto
{
/// <summary>
/// Unique product identifier
/// </summary>
public Guid Id { get; init; }
/// <summary>
/// Product name
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// Product price in USD
/// </summary>
public decimal Price { get; init; }
/// <summary>
/// Indicates if product is currently in stock
/// </summary>
public bool InStock { get; init; }
}
/// <summary>
/// Request model for creating a new product
/// </summary>
public record CreateProductRequest
{
/// <summary>
/// Product name (3-200 characters)
/// </summary>
[Required]
[StringLength(200, MinimumLength = 3)]
public string Name { get; init; } = string.Empty;
/// <summary>
/// Detailed product description
/// </summary>
[StringLength(2000)]
public string? Description { get; init; }
/// <summary>
/// Product price (must be positive)
/// </summary>
[Required]
[Range(0.01, 1000000)]
public decimal Price { get; init; }
/// <summary>
/// Category identifier
/// </summary>
[Required]
public Guid CategoryId { get; init; }
}
What you get:
- π¨ Interactive UI at
https://localhost:5001/(if RoutePrefix is empty) - π OpenAPI JSON at
https://localhost:5001/swagger/v1/swagger.json - π§ͺ Test endpoints directly from the browser
- π Auto-generated documentation from XML comments
- π Security definitions (JWT token input field)
π€ Did You Know? Major tech companies like Microsoft, Google, and Stripe use OpenAPI specifications. Tools like Postman can import your swagger.json to auto-generate API collections!
Common Mistakes to Avoid β οΈ
1. Forgetting [ApiController] Attribute
β Wrong:
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult Create(CreateUserRequest request)
{
// BUG: Validation doesn't happen automatically!
// Must manually check ModelState.IsValid
var user = CreateUser(request);
return Ok(user);
}
}
β Right:
[ApiController] // β Enables automatic validation
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult Create(CreateUserRequest request)
{
// Validation happens automatically
// If invalid, returns 400 before this code runs
var user = CreateUser(request);
return Ok(user);
}
}
2. Validation Attributes Without Error Messages
β Wrong:
public class UserDto
{
[Required] // Generic error: "The Username field is required"
[StringLength(50)] // Generic error
public string Username { get; set; }
}
β Right:
public class UserDto
{
[Required(ErrorMessage = "Please provide a username")]
[StringLength(50, MinimumLength = 3,
ErrorMessage = "Username must be 3-50 characters")]
public string Username { get; set; }
}
3. Not Documenting Response Types
β Wrong:
[HttpGet("{id}")]
public IActionResult GetUser(Guid id)
{
// Swagger doesn't know what this returns!
return Ok(user);
}
β Right:
[HttpGet("{id}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public ActionResult<UserDto> GetUser(Guid id)
{
// Swagger knows exact return types
return Ok(user);
}
4. Mixing Validation Approaches
β Wrong:
// Data annotations AND FluentValidation on same model
public class UserDto
{
[Required] // Data annotation
public string Username { get; set; }
}
// Also has FluentValidation validator
public class UserDtoValidator : AbstractValidator<UserDto> { ... }
// Result: Validation runs twice, confusing error messages
β Right: Choose one approach per model:
// Option 1: Data annotations only
public class SimpleDto
{
[Required]
public string Name { get; set; }
}
// Option 2: FluentValidation only (clean model)
public class ComplexDto
{
public string Name { get; set; } // No attributes
}
public class ComplexDtoValidator : AbstractValidator<ComplexDto>
{
public ComplexDtoValidator()
{
RuleFor(x => x.Name).NotEmpty();
}
}
5. Not Enabling XML Documentation File
β Wrong: Write XML comments but forget to enable generation:
/// <summary>
/// Gets a user // This won't appear in Swagger!
/// </summary>
[HttpGet("{id}")]
public IActionResult GetUser(Guid id) => Ok();
β
Right: Enable in .csproj:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
6. Returning 200 OK for All Errors
β Wrong:
[HttpPost]
public IActionResult Create(UserDto dto)
{
if (!IsValid(dto))
return Ok(new { error = "Invalid data" }); // β Wrong status!
return Ok(user);
}
β Right:
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public IActionResult Create(UserDto dto)
{
// Validation handled automatically by [ApiController]
var user = CreateUser(dto);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
7. Complex Regex Without Explanation
β Wrong:
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$")]
public string Password { get; set; }
// Users have no idea what the requirement is!
β Right:
[RegularExpression(
@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$",
ErrorMessage = "Password must be at least 8 characters with uppercase, lowercase, digit, and special character")]
public string Password { get; set; }
Key Takeaways π―
π Quick Reference Card: Validation & Documentation
| Concept | Key Points |
|---|---|
| Data Annotations | β’ Use for simple validation β’ [Required], [Range], [EmailAddress], [StringLength] β’ Add ErrorMessage for user-friendly errors |
| Custom Validators | β’ Inherit ValidationAttribute β’ Override IsValid method β’ Reusable across projects |
| FluentValidation | β’ Separation of concerns β’ Dependency injection support β’ Complex/async validation β’ More testable |
| Swagger/OpenAPI | β’ Use Swashbuckle.AspNetCore β’ Enable XML documentation β’ Add [ProducesResponseType] attributes β’ Include security definitions |
| Status Codes | β’ 200 OK - Success β’ 201 Created - POST success β’ 400 Bad Request - Validation fail β’ 404 Not Found - Resource missing |
| Best Practices | β’ Always use [ApiController] β’ Document all endpoints β’ Use proper HTTP status codes β’ Choose one validation approach β’ Write clear error messages |
Remember:
- π‘οΈ Validation is security - Never trust client input
- π Documentation is maintenance - Future you will thank you
- π― Be specific - Generic errors frustrate users
- π§ͺ Test validation - Write unit tests for custom validators
- β‘ Fail fast - Validate early in the request pipeline
π Further Study
- Official ASP.NET Documentation: Model validation in ASP.NET Core
- FluentValidation Documentation: https://docs.fluentvalidation.net/
- OpenAPI Specification: https://swagger.io/specification/
Next Steps: Practice building a complete CRUD API with full validation and Swagger documentation. Try implementing async validators that check database constraints (like unique email addresses). Experiment with custom response formats using ProblemDetails middleware. Happy coding! π»β¨