Home > other >  ASP.NET Core Web API - How to map two entities to a single DTO for validation
ASP.NET Core Web API - How to map two entities to a single DTO for validation

Time:09-01

In ASP.NET Core-6, I am implementing auto-mapper and Fluent Validation.

I have two entities:

Students

public class Employee
{
    public Guid Id { get; set; }
    public string UserId { get; set; }
    public string Grade { get; set; }
    public string EmployeeCode { get; set; }
    public string CurrentStatus { get; set; }

    public virtual ApplicationUser User { get; set; }
}

ApplicationUser:

public class ApplicationUser : IdentityUser
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual Employee Employee { get; set; }
}

This is the DTO:

public class EmployeeDto
{
    public string Grade { get; set; }
    public string EmployeeCode { get; set; }
    public string CurrentStatus { get; set; }
    public string UserName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

Then using AutoMapper, I did this:

CreateMap<EmployeeDto, Employee>().ReverseMap();
CreateMap<EmployeeDto, ApplicationUser>().ReverseMap();

Also, I have this Fluent Validation:

public class EmployeeDtoValidator : AbstractValidator<EmployeeDto>
{
    private readonly ApplicationDbContext _dbContext;
    public EmployeeDtoValidator(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    public EmployeeDtoValidator()
    {
        RuleFor(user => user.UserName)
            .Must(BeUniqueName).WithMessage("The specified Username already exists.")
            .NotEmpty().WithMessage("User Name field is required. ERROR!")
            .NotNull().WithMessage("User Name cannot be null")
            .Matches(@"^[\S] $").WithMessage("No Space Required. ERROR!")
            .MinimumLength(2).WithMessage("User Name is limited to a minimum of 2 characters")
            .MaximumLength(25).WithMessage("User Name is limited to a maximum of 25 characters");

        RuleFor(user => user.Email)
            .MaximumLength(100).WithMessage("Email must not exceed 100 characters.")
            .Must(BeUniqueEmail).WithMessage("The specified Email Address already exists.")
            .EmailAddress().WithMessage("Kindly enter correct Email Address. ERROR!");

        RuleFor(user => user.EmployeeCode)
            .Must(BeUniqueEmployeeCode).WithMessage("The specified Employee Code already exists.")
            .NotEmpty().WithMessage("Employee Code field is required. ERROR!")
            .NotNull().WithMessage("Employee Code cannot be null")
            .Matches(@"^\d*[0-9](|.\d*[0-9])?$").WithMessage("Only numeric values are allowed. ERROR!")
            .MinimumLength(5).WithMessage("Employee Code is limited to a minimum of 5 characters")
            .MaximumLength(20).WithMessage("Employee Code is limited to a maximum of 20 characters");
    }
    private bool BeUniqueName(string name)
    {
        if (_dbContext.ApplicationUsers.SingleOrDefault(x => x.UserName.ToLower() == name.ToLower()) == null) return true;
        return false;
    }
    private bool BeUniqueEmployeeCode(string name)
    {
        if (_dbContext.Employees.SingleOrDefault(x => x.EmployeeCode.ToLower() == name.ToLower()) == null) return true;
        return false;
    }
    private bool BeUniqueEmail(string email)
    {
        if (_dbContext.ApplicationUsers.SingleOrDefault(x => x.Email.ToLower() == email.ToLower()) == null) return true;
        return false;
    }
}

DI:

public static void AddDependencyInjection(this IServiceCollection services)
{
   services.AddTransient<IValidator<EmployeeDto>, EmployeeDtoValidator>();
}

Program.cs:

builder.Services.AddDependencyInjection();
// Configure AutoMapper
builder.Services.ConfigureAutoMappers();

builder.Services.AddFluentValidation(conf =>
{
    conf.RegisterValidatorsFromAssembly(typeof(Program).Assembly);
    conf.AutomaticValidationEnabled = true;
});

var app = builder.Build();

Lastly, I have the service:

public async Task<Response<string>> RegisterAsync(EmployeeDto model)
{
    var response = new Response<string>();
    using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
            var user = _mapper.Map<ApplicationUser>(model);

            var employeePassword = "@Password";
            var result = await _userManager.CreateAsync(user, employeePassword);
            if (result.Succeeded)
            {
                var employee = _mapper.Map<Employee>(model);
                employee.UserId = user.Id;

                await _unitOfWork.Employees.InsertAsync(employee);
                await _unitOfWork.Save();
                response.StatusCode = (int)HttpStatusCode.Created;
                response.Successful = true;
                response.Message = "Employee created successfully!";
                transaction.Complete();
                return response;
            }
        response.StatusCode = (int)HttpStatusCode.BadRequest;
        response.Successful = false;
        return response;
    };
}

Controller:

EmployeeController : BaseApiController
{
    private readonly ILogger<AdminController> _logger;
    private IEmployeeService _employeeService;

    public EmployeeController(
        ILogger<EmployeeController> logger,
        IEmployeeService employeeService,
        )
    {
        _logger = logger;
        _employeeService = employeeService;
    }
    [HttpPost]
    [Route(ApiRoutes.Admin.RegisterEmployee)]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    public async Task<ActionResult<Response<string>>> RegisterEmployeeAsync([FromBody] EmployeeDto model)
    {
        _logger.LogInformation($"Registration Attempt for {model.UserName}");
        var result = await _employeeService.RegisterAsync(model);
        return StatusCode(result.StatusCode, result);
    }

}

When user submits, I want the Application to validate with the Fluent Validation.

To my surprise, it is not performing any validation.

But in the same project, all other validations with single entities are working.

How do I resolve this?

Thanks

CodePudding user response:

As @CodingMytra noticed you have disabled auto validation so either enable it:

builder.Services.AddFluentValidation(conf =>
{
    conf.RegisterValidatorsFromAssembly(typeof(Program).Assembly);
    conf.AutomaticValidationEnabled = true;
});

Or validate manually by injecting the EmployeeDtoValidator:

public class EmployeeController : BaseApiController
{
    private readonly ILogger<AdminController> _logger;
    private IEmployeeService _employeeService;
    private readonly IValidator<EmployeeDto> _employeeValidator;

    public EmployeeController(
        ILogger<EmployeeController> logger,
        IEmployeeService employeeService,
        IValidator<EmployeeDto> employeeValidator)
    {
        _logger = logger;
        _employeeService = employeeService;
        _employeeValidator = employeeValidator;
    }

    [HttpPost]
    [Route(ApiRoutes.Admin.RegisterEmployee)]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    public async Task<ActionResult<Response<string>>> RegisterEmployeeAsync([FromBody] EmployeeDto model)
    {
        _logger.LogInformation($"Registration Attempt for {model.UserName}");

        ValidationResult result = await _employeeValidator.ValidateAsync(model);
    
        if (!result.IsValid) 
        {
            // handle invalid model
        }

        var result = await _employeeService.RegisterAsync(model);
        return StatusCode(result.StatusCode, result);
    }
}

https://docs.fluentvalidation.net/en/latest/aspnet.html#manual-validation

  • Related