Search code examples
asp.net-core.net-coreblazorfluentvalidation

Where to put validation on domain object shared by blazor UI and ASP .Net Core controller


I'm trying to figure out where to put my data validation in a new app.

The app is composed of a Blazor UI (Mainly made of simple forms) and a few Controllers. Same action can be done via the Blazor UI and the controllers, they both call the same service.

They do not share the same model class. On the controller side, I expect a "xxxDto" class, on the blazor side, I work with a "xxxModel" class, and they are both automapped to a "xxxInput" class, which is the class expected by my service.

DTO and Model are slightly different (The model has default values, and some value are split for easier input)

My question is to know, where should I put my data validation (Using FluentValidation). I don't think it's a good idea to duplicate validation on the Dto and Model class. Would it be a good practice to perform the validation on the Input class, and add some "proxy" validation on both the Model and Dto class ? (I was thinking doing a validator which would automap the value to the Input class, and then use that validator. Would it work ? )

Right now, I only have the Model validation, and I'm trying to add the controller layer.


Solution

  • When you have different classes (Model and Dto) but need to validate the same data before passing it to a service, it's a good idea to centralize your validation logic to avoid duplication. Your approach of using an Input class for validation and then mapping to Model and Dto sounds reasonable. Here's a suggested approach:

    Step 1: Create an Input Class: Define a separate Input class that contains the combined and flattened representation of data expected by your service. This will be the class you validate before passing it to the service.

       public class MyEntityInput
    {
        public string Name { get; set; }
        // Other properties with flattened structure
    }
    

    Step 2: Use FluentValidation on the Input Class: validator will contain your validation rules.

    public class MyEntityInputValidator : AbstractValidator<MyEntityInput>
    {
        public MyEntityInputValidator()
        {
            RuleFor(x => x.Name).NotEmpty().MaximumLength(50);
            // Other validation rules
        }
    }
    

    Step 3: Proxy Validation for Model and Dto: Create a method that takes an instance of either Model or Dto, maps it to the Input class, and then validates using the MyEntityInputValidator. You can use this method before performing actions in both the Blazor UI and the Controllers.

    public class MyValidationService
    {
        private readonly IValidator<MyEntityInput> _inputValidator;
    
        public MyValidationService(IValidator<MyEntityInput> inputValidator)
        {
            _inputValidator = inputValidator;
        }
    
        public ValidationResult ValidateAndMapToInput(MyEntityModel model)
        {
            var input = Mapper.Map<MyEntityInput>(model);
            return _inputValidator.Validate(input);
        }
    
        public ValidationResult ValidateAndMapToInput(MyEntityDto dto)
        {
            var input = Mapper.Map<MyEntityInput>(dto);
            return _inputValidator.Validate(input);
        }
    }
    

    Step 4: Usage in Blazor UI and Controllers: Before calling your service in both the Blazor UI and Controllers, use the 'ValidateAndMapToInput' method to validate the data.

    public class MyBlazorComponent : ComponentBase
    {
        [Inject]
        public MyValidationService ValidationService { get; set; }
    
        public async Task HandleSubmit(MyEntityModel model)
        {
            var validationResult = ValidationService.ValidateAndMapToInput(model);
    
            if (validationResult.IsValid)
            {
                // Call the service
            }
            else
            {
                // Handle validation errors in the UI
            }
        }
    }
    
    [ApiController]
    [Route("api/[controller]")]
    public class MyController : ControllerBase
    {
        private readonly MyValidationService _validationService;
    
        public MyController(MyValidationService validationService)
        {
            _validationService = validationService;
        }
    
        [HttpPost]
        public IActionResult Post([FromBody] MyEntityDto dto)
        {
            var validationResult = _validationService.ValidateAndMapToInput(dto);
    
            if (validationResult.IsValid)
            {
                // Call the service
            }
            else
            {
                // Return validation errors
                return BadRequest(validationResult.Errors);
            }
        }
    }
    

    In above all step you centralize your validation logic in the Input class and avoid duplication. The MyValidationService encapsulates the validation and mapping logic, making it easy to use in both the Blazor UI and Controllers.