A request with an empty title should not need to reach the database before it can be rejected. Validating the input contract at the API boundary gives the user an actionable field error and keeps repetitive format checks out of the business workflow. FluentValidation fits that role without absorbing authorization and publication decisions into one validator.
State the accepted input explicitly
The archive contains API flows with distinct create, update and approval operations. This independent Minimal API example demonstrates only their input boundary. CreateDraft requires a nonempty title of at most 120 string units. A valid request is echoed; no draft is persisted.
using FluentValidation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
var builder=WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IValidator<CreateDraft>,DraftValidator>();
var app=builder.Build();
app.MapPost("/drafts",async(CreateDraft input,IValidator<CreateDraft> validator,CancellationToken token)=>{
var validation=await validator.ValidateAsync(input,token);
if(!validation.IsValid)return Results.ValidationProblem(validation.ToDictionary());
return Results.Ok(new {input.Title});
});
app.Run();
public record CreateDraft(string Title);
public class DraftValidator:AbstractValidator<CreateDraft>
{
public DraftValidator()=>RuleFor(x=>x.Title).NotEmpty().MaximumLength(120);
}
The validator is resolved as a scoped dependency and invoked through ValidateAsync with the request cancellation token. Results.ValidationProblem returns field-associated errors. The successful response demonstrates passing validation, not a committed database operation.
Separate field rules from business decisions
Required text, length and numeric ranges are field rules. Whether a draft may be published, whether the caller may edit it and whether an operation has already completed depend on business state. Those decisions belong in services and authorization boundaries. A validator should not quietly become an orchestration service issuing unrelated queries to every table.
A uniqueness lookup can improve feedback but cannot prevent two concurrent requests from both passing it. The database constraint and controlled conflict handling remain necessary. Likewise, a correctly formatted record ID does not establish access to that record.
Asynchronous rules need an asynchronous execution path
The current rules are synchronous, but ValidateAsync leaves a valid execution path for a later MustAsync rule. Do not assume a synchronous automatic model-validation integration can execute arbitrary asynchronous checks. Verify the selected integration; an explicit async endpoint or action filter can make the boundary easier to follow.
Pass cancellation into any cancellable validator I/O as well. A token in the method signature is not enough if the lower-level call ignores it.
Keep normalization visible
If the stored title must be trimmed, define where that transformation occurs. Mutating the request invisibly inside a validator makes the execution order surprising. Normalize deliberately and validate the accepted representation, including any limits that transformations could affect.
String length also needs an agreed meaning. .NET String.Length is not always the count of visible Unicode characters. Multilingual input, database limits and user-facing descriptions should agree; a browser maxlength attribute is only one part of the boundary.
Make the rejection useful in the form
A client should associate server field errors with its inputs rather than display only a generic failure. Show submission errors after an attempt instead of marking every field red on arrival. A disabled save button for missing input explains little; actionable validation does. Preventing duplicate submissions while a request is running is a separate concern.
Test empty input, whitespace, titles at 120 and 121 units, malformed JSON and a valid request. Invalid input should not reach the business handler. If persistence is added, authentication, authorization, transaction ownership and the durable result still need to be implemented at their own boundaries.
FluentValidation ASP.NET integration; Asynchronous validation rules.