Domain validators ensure that entities remain in a valid state according to business rules. The architecture uses FluentValidation to provide declarative, fluent validation rules that are automatically applied to entities.
Here’s how to create a validator for your domain entities, based on DummyEntityValidator from Domain/Validators/DummyEntityValidator.cs:13:
using Core.Domain.Validators;using Domain.Constants;using Domain.Entities;using FluentValidation;namespace Domain.Validators{ public class DummyEntityValidator : EntityValidator<DummyEntity> { public DummyEntityValidator() { // Define business rules in the constructor RuleFor(x => x.DummyPropertyOne) .NotNull() .NotEmpty() .WithMessage(DomainConstants.NOTNULL_OR_EMPTY); } }}
RuleFor(x => x.Name) .NotNull().WithMessage("Name is required") .NotEmpty().WithMessage("Name cannot be empty");
String Length
RuleFor(x => x.Description) .MinimumLength(10).WithMessage("Description must be at least 10 characters") .MaximumLength(500).WithMessage("Description cannot exceed 500 characters") .Length(10, 500).WithMessage("Description must be between 10 and 500 characters");
Numeric Ranges
RuleFor(x => x.Quantity) .GreaterThan(0).WithMessage("Quantity must be positive") .LessThanOrEqualTo(1000).WithMessage("Quantity cannot exceed 1000") .InclusiveBetween(1, 1000).WithMessage("Quantity must be between 1 and 1000");
RuleFor(x => x.CompanyName) .NotEmpty() .When(x => x.IsBusinessCustomer) .WithMessage("Company name is required for business customers");
Custom Validation
RuleFor(x => x.OrderDate) .Must(BeValidBusinessDay) .WithMessage("Orders can only be placed on business days");private bool BeValidBusinessDay(DateTime date){ return date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday;}
Collection Validation
RuleFor(x => x.Items) .NotEmpty().WithMessage("Order must contain at least one item") .Must(x => x.Count <= 50).WithMessage("Order cannot contain more than 50 items");RuleForEach(x => x.Items) .SetValidator(new OrderItemValidator());
public class ProductValidator : EntityValidator<Product>{ public ProductValidator() { // Configure to stop at first failure ClassLevelCascadeMode = CascadeMode.Stop; RuleFor(x => x.Name).NotEmpty(); RuleFor(x => x.Price).GreaterThan(0); }}
Validate properties only if previous rules passed:
public class OrderValidator : EntityValidator<Order>{ public OrderValidator() { RuleFor(x => x.CustomerId) .NotEmpty().WithMessage("Customer is required"); RuleFor(x => x.Total) .GreaterThan(0) .DependentRules(() => { RuleFor(x => x.PaymentMethod) .NotNull().WithMessage("Payment method required for orders with total > 0"); }); }}
var entity = new DummyEntity(null, DummyValues.value1);if (!entity.IsValid){ var errors = entity.GetErrors(); // Get all error messages var errorMessages = errors.Select(e => e.ErrorMessage).ToList(); // Get errors for specific property var propertyErrors = errors .Where(e => e.PropertyName == "DummyPropertyOne") .Select(e => e.ErrorMessage) .ToList(); // Format for logging var formattedErrors = string.Join(", ", errors.Select(e => $"{e.PropertyName}: {e.ErrorMessage}")); Console.WriteLine($"Validation failed: {formattedErrors}");}
using Xunit;using FluentValidation.TestHelper;public class DummyEntityValidatorTests{ private readonly DummyEntityValidator _validator; public DummyEntityValidatorTests() { _validator = new DummyEntityValidator(); } [Fact] public void Should_Have_Error_When_DummyPropertyOne_Is_Null() { var entity = new DummyEntity(null, DummyValues.value1); var result = _validator.TestValidate(entity); result.ShouldHaveValidationErrorFor(x => x.DummyPropertyOne); } [Fact] public void Should_Not_Have_Error_When_DummyPropertyOne_Is_Valid() { var entity = new DummyEntity("Valid Value", DummyValues.value1); var result = _validator.TestValidate(entity); result.ShouldNotHaveValidationErrorFor(x => x.DummyPropertyOne); }}