I have an ASP.NET Core 9.0 MVC project where I use DTOs for input binding and FluentValidation 12.x for validation.
Entities exist separately and contain data annotations attributes like [Required] and [MaxLength], but those are only for EF Core and are not intended for MVC validation.
DTOs do not have any data annotations at all (intentionally).
Validators are placed in a Validators folder, each inheriting from AbstractValidator<T>.
I register validators in Program.cs like this:
builder.Services.AddValidatorsFromAssemblyContaining<CreateCustomerDtoValidator>();
builder.Services.AddControllersWithViews()
.AddViewLocalization()
.AddDataAnnotationsLocalization();
One example validator looks like this:
public class CreateCustomerDtoValidator : AbstractValidator<CreateCustomerDto>
{
public CreateCustomerDtoValidator(IStringLocalizer<ValidationResource> localizer)
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Test message")
.MinimumLength(2).WithMessage("Test message 2");
}
}
My CreateCustomerDto.cs file:
namespace ProjectCustora.Dtos.CustomerDto
{
public class CreateCustomerDto
{
public string Name { get; set; }
public string Surname { get; set; }
public string Tc { get; set; }
public string? Phone { get; set; }
public string Username { get; set; }
public string? Notes { get; set; }
public string? VatId { get; set; }
public DateTime LastContactDate { get; set; } = DateTime.Now;
public CreateAddressDto Address { get; set; } = new CreateAddressDto();
}
}
My CustomerController.cs file:
#region Add
[HttpGet]
public IActionResult AddCustomer()
{
var model = new CreateCustomerDto();
return View(model);
}
[HttpPost]
public async Task<IActionResult> AddCustomer(CreateCustomerDto createCustomerDto)
{
if (ModelState.IsValid)
{
OperationResult<CreateCustomerDto> result = await _customerService.CreateCustomerAsync(createCustomerDto);
if(result.IsSuccess)
{
TempData["SuccessMessage"] = result.Message;
return RedirectToAction("Index", "Customer");
}
else
{
TempData["ErrorMessage"] = result.Message;
return View(createCustomerDto);
}
}
return View(createCustomerDto);
}
#endregion
My CustomerService.cs file:
public async Task<OperationResult<CreateCustomerDto>> CreateCustomerAsync(CreateCustomerDto createCustomer)
{
try
{
var customer = _mapper.Map<Customer>(createCustomer);
await _unitOfWork.Customers.CreateAsync(customer);
await _unitOfWork.CompleteAsync();
return new OperationResult<CreateCustomerDto>
{
Data = createCustomer,
Message="Customer created successfully",
IsSuccess= true
};
}
catch(Exception ex)
{
_logger.LogError(ex, "An error occurred while creating a customer.");
return new OperationResult<CreateCustomerDto>
{
Data = createCustomer,
Message = "An error occurred while creating the customer",
IsSuccess = false
};
}
}
When I run the application and submit an empty form, I expect to see only the localized FluentValidation message "Name is required".
- I confirmed that DTOs do not contain
[Required], so no data annotations should be involved - Entities do contain data annotations, but I am binding only to DTOs in my controller, not entities
- FluentValidation works correctly when debugging —
ValidateAsyncreturns the expected messages - I'm using .NET 8.0 and Fluent Validation v12.0 (latest)
Expected behavior:
only the FluentValidation messages (e.g., "Test message") should appear in the view under asp-validation-for.
Actual behavior:
the view shows the default ASP.NET Core data annotations message:
The Name field is required
It seems ASP.NET Core is still triggering some data annotations validation, even though I only want FluentValidation to handle validation for DTOs.
CreateCustomerDtomodel?CreateCustomerDto.cscontroller and my service files.