In modern application development, ensuring that data is valid before processing is crucial to maintain application integrity. Whether you're creating, updating, or deleting entities, it is important to verify that the data adheres to business rules. In this article, we will focus on how to add validation using Fluent Validation in a feature-based architecture, specifically within a command handler for creating events. Additionally, we’ll explore how to return custom responses and handle validation errors cleanly.
Why Not Use Data Annotations?
In many projects, data annotations are used for basic validation on entities. While this works for simple validation scenarios, it tightly couples validation logic to the domain, violating the persistence ignorance principle. Additionally, data annotations cannot handle more complex validation logic, such as cross-field validation or querying the database to check for uniqueness.
By using Fluent Validation, we keep the validation logic separate from the domain entities, making the system more maintainable, flexible, and easier to extend.
Step 1: Adding Fluent Validation NuGet Packages
Before we dive into the code, we need to add Fluent Validation to our project. This can be done by adding the following package references to your project file (.csproj
):
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
Alternatively, you can install the packages via the NuGet Package Manager in Visual Studio:
Install-Package FluentValidation
Install-Package FluentValidation.DependencyInjectionExtensions
These packages will allow us to define validation logic using Fluent Validation and register validators within the dependency injection container.
Step 2: Creating a Base Response Class
To standardize how our command handlers respond to requests, we’ll create a base response class that includes properties for success, a message, and a list of validation errors (if any). This will ensure that consumers of the API receive consistent responses regardless of the operation they are performing.
namespace GloboTicket.TicketManagement.Application.Responses
{
public class BaseResponse
{
public BaseResponse()
{
Success = true;
}
public BaseResponse(string message)
{
Success = true;
Message = message;
}
public BaseResponse(string message, bool success)
{
Success = success;
Message = message;
}
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public List<string>? ValidationErrors { get; set; }
}
}
Step 3: Adding Fluent Validation for Event Creation
Now let’s focus on validating the CreateEventCommand
. Fluent Validation allows us to define business rules in a clean, maintainable manner using lambda expressions. In this example, we'll validate that the name, price, and date of the event are provided and ensure the event name and date are unique.
using FluentValidation;
using GloboTicket.TicketManagement.Application.Contracts.Persistence;
namespace GloboTicket.TicketManagement.Application.Features.Events.Commands.CreateEvent
{
public class CreateEventCommandValidator : AbstractValidator<CreateEventCommand>
{
private readonly IEventRepository _eventRepository;
public CreateEventCommandValidator(IEventRepository eventRepository)
{
_eventRepository = eventRepository;
RuleFor(p => p.Name)
.NotEmpty().WithMessage("{PropertyName} is required.")
.NotNull()
.MaximumLength(50).WithMessage("{PropertyName} must not exceed 50 characters.");
RuleFor(p => p.Date)
.NotEmpty().WithMessage("{PropertyName} is required.")
.GreaterThan(DateTime.Now).WithMessage("{PropertyName} must be a future date.");
RuleFor(e => e)
.MustAsync(EventNameAndDateUnique)
.WithMessage("An event with the same name and date already exists.");
RuleFor(p => p.Price)
.NotEmpty().WithMessage("{PropertyName} is required.")
.GreaterThan(0).WithMessage("{PropertyName} must be greater than zero.");
}
private async Task<bool> EventNameAndDateUnique(CreateEventCommand e, CancellationToken token)
{
return !await _eventRepository.IsEventNameAndDateUnique(e.Name, e.Date);
}
}
}
Step 4: Handling Validation in the Command Handler
Now that we’ve defined our validation rules, we can use the CreateEventCommandValidator
in the command handler for creating events. The handler will validate the request before processing it, and if validation fails, it will return a response that includes the validation errors.
using AutoMapper;
using GloboTicket.TicketManagement.Application.Contracts.Persistence;
using GloboTicket.TicketManagement.Domain.Entities;
using MediatR;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace GloboTicket.TicketManagement.Application.Features.Events.Commands.CreateEvent
{
public class CreateEventCommandHandler : IRequestHandler<CreateEventCommand, CreateEventCommandResponse>
{
private readonly IEventRepository _eventRepository;
private readonly IMapper _mapper;
public CreateEventCommandHandler(IMapper mapper, IEventRepository eventRepository)
{
_mapper = mapper;
_eventRepository = eventRepository;
}
public async Task<CreateEventCommandResponse> Handle(CreateEventCommand request, CancellationToken cancellationToken)
{
var theEvent = _mapper.Map<Event>(request);
var createEventCommandResponse = new CreateEventCommandResponse();
var validator = new CreateEventCommandValidator(_eventRepository);
var validationResult = await validator.ValidateAsync(request);
if (validationResult.Errors.Count > 0)
{
createEventCommandResponse.Success = false;
createEventCommandResponse.ValidationErrors = new List<string>();
foreach (var error in validationResult.Errors)
{
createEventCommandResponse.ValidationErrors.Add(error.ErrorMessage);
}
}
if (createEventCommandResponse.Success)
{
theEvent = await _eventRepository.AddAsync(theEvent);
createEventCommandResponse.EventDto = _mapper.Map<CreateEventDto>(theEvent);
}
return createEventCommandResponse;
}
}
}
Step 5: Custom Response Objects
Instead of simply returning a success flag, we return a custom response object. This allows us to return detailed information to the consumer, such as the created event's details or a list of validation errors.
using GloboTicket.TicketManagement.Application.Responses;
namespace GloboTicket.TicketManagement.Application.Features.Events.Commands.CreateEvent
{
public class CreateEventCommandResponse : BaseResponse
{
public CreateEventCommandResponse() : base()
{
}
public CreateEventDto EventDto { get; set; } = default!;
}
}
Conclusion
By incorporating Fluent Validation into your application, you can maintain clean and flexible validation logic that is decoupled from your domain entities. Using custom exceptions and response objects, you can also handle validation errors elegantly and provide clear, structured feedback to the consumer.
With this approach, your application becomes more maintainable, modular, and flexible, ensuring that as your business rules evolve, your validation logic remains easy to manage.
For the complete source code, you can visit the GitHub repository: https://github.com/mohamedtayel1980/clean-architecture
Top comments (0)