# Let&#8217;s combine Data Annotation Validation with FluentValidation

**Date:** 2025-04-06  
**Author:** Kees C. Bakker  
**Categories:** .NET / C#  
**Tags:** .NET Data Validation  
**Original:** https://keestalkstech.com/lets-combine-data-annotation-validation-with-fluentvalidation/

![Let&#8217;s combine Data Annotation Validation with FluentValidation](https://keestalkstech.com/wp-content/uploads/2025/04/chuttersnap-G8ioIHUDfNc-unsplash.jpg)

---

Ever since I wrote the blog [“Is One Of” and “Is Not One Of” validation attributes](https://keestalkstech.com/is-one-of-and-is-not-one-of-validation-attributes/), I've wondered if there is anything better for class validation. I mean: validating individual properties is fine, but usually the more interesting stuff is the validation of a *combination of members*.

Enter [FluentValidation](https://docs.fluentvalidation.net/en/latest/index.html#example):

```csharp
public class CustomerValidator : AbstractValidator<Customer>
{
  public CustomerValidator()
  {
    RuleFor(x => x.Surname).NotEmpty();
    RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
    RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
    RuleFor(x => x.Address).Length(20, 250);
    RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
  }

  private bool BeAValidPostcode(string postcode)
  {
    // custom postcode validating logic goes here
  }
}
```

Wow, this makes intent really really clear.

## Some observations

When I compare FluentValidation to validation by data annotation, I observe the following:

- **Discovery**: data annotations are very clear. Everything is part of the class that needs to be validated. With the example implementation, we loose that clarity. Sure, the validator is tied to the class, but from the perspective of the class we can't see it.
- **Validation** **mechanism**. It has a different validation mechanism, we need [to do something to hook it up in the ASP.NET API validation pipeline](https://docs.fluentvalidation.net/en/latest/aspnet.html#using-the-asp-net-validation-pipeline), but that is deprecated. We can do some [manual validation](https://docs.fluentvalidation.net/en/latest/aspnet.html#manual-validation) using an `IValidator`, but that's a bit verbose.
- **OpenAPI**. Data annotations like `Required` and `MinLength` are parsed into the OpenAPI specification. I would rather use `[Required]` than `RuleFor(x => x.SurName).NotEmpty()`.
- **Complex validation bases on other members**. FluentValidation really shines when multiple members and [conditions](https://docs.fluentvalidation.net/en/latest/conditions.html) are involved:

```csharp
RuleFor(x => x.EntryPoint)
  .NotEmpty()
  .When(
    x => x.Type == ApplicationType.ApplicationWithEntryPoint,
    ApplyConditionTo.CurrentValidator)
  .Empty()
  .When(
    x => x.Type == ApplicationType.Application,
    ApplyConditionTo.CurrentValidator);
```

So, what if I told you, that we can *connect* the FluentValidations to the data validation?

## IValidatableObject to the rescue

The `IValidatableObject` can be used to implement the validation of the entire class. Let's combine them with

```
public class SimpleApplication : IValidatableObject
{
    [Required, MinLength(5), ApplicationNameAvailable]
    public string Name { get; set; } = string.Empty;

    public ApplicationType Type { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string? EntryPoint { get; set; }

    public int MagicNumber { get; set; }

    [Label]
    public string Label { get; set; } = string.Empty;

    public IEnumerable Validate(ValidationContext validationContext)
    {
        var v = new InlineValidator();

        // conditional validation
        v.RuleFor(x => x.EntryPoint)
            .NotEmpty()
            .When(
                x => x.Type == ApplicationType.ApplicationWithEntryPoint,
                ApplyConditionTo.CurrentValidator)
            .Empty()
            .When(
                x => x.Type == ApplicationType.Application,
                ApplyConditionTo.CurrentValidator);

        // dependency injection
        var provider = validationContext.GetRequiredService();
        v.RuleFor(x => x.MagicNumber)
            .MustAsync(async (magicNumber, _) => magicNumber == await provider.GetMagicNumber())
            .WithMessage("Magic number is invalid.");

        var result = v.ValidateAsync(this).Result;
        var errors = result.Errors.Select(e => new ValidationResult(e.ErrorMessage, [e.PropertyName]));
        return errors;
    }
}
```

Here we see a combination attribute annotations (`Required`, `MinLength`, etc) combined with an `InlineValidator` that is triggered on the `Validate`. This works like a charm. Everything gets combined when we validate. Check the following test:

```
[Fact]
public void ValidateByValidatorWithServiceProvider()
{
    // arrange
    FluentValidationLanguageManager.SetGlobalOptions();

    var provider = new ServiceCollection()
        .AddSingleton()
        .AddSingleton()
        .AddSingleton()
        .AddSingleton(sp => new ProvisioningOptions
        {
            Labels = ["development", "production"]
        })
        .AddTransient(sp => Options.Create(sp.GetRequiredService()))
        .AddSingleton(sp => sp)
        .BuildServiceProvider();

    var obj = new SimpleApplication
    {
        Name = "My Application",
        Type = ApplicationType.Application,
        EntryPoint = "dotnet run kaas.is.lekker.dll",
        MagicNumber = 1337,
        Label = "development"
    };

    // act
    IList validationErrors = [];
    var context = new ValidationContext(obj, provider, null);
    var valid = Validator.TryValidateObject(obj, context, validationErrors, true);

    // assert
    valid.Should().BeFalse();
    validationErrors.Should().NotBeNullOrEmpty();
    validationErrors.Should().HaveCount(2);

    var messages = validationErrors.Select(e => e.ErrorMessage).ToList();
    messages.Should().Contain("EntryPoint must be empty.");
    messages.Should().Contain("Magic number is invalid.");
}
```

## A note on localization

The `Empty()` and `NotEmpty()` validations will generate messages with quoted property names. The `[Required`] will not do so. FluentValidation will also split the name of the property: *EntryPoint* becomes *Entry Point* in the message.

The `[Required]` will says something like *EntryPoint field is required.*; it will add the word *field*, which is also a difference.

To harmonize things, we should use a custom language manager.

```
public class FluentValidationLanguageManager : LanguageManager
{
    private FluentValidationLanguageManager()
    {
    }

    public override string? GetString(string key, CultureInfo? culture = null)
    {
        var message = base.GetString(key, culture);

        // Harmonize output with attribute validation
        return message?.Replace("'{PropertyName}'", "{PropertyName}");
    }

    public static void SetGlobalOptions()
    {
        ValidatorOptions.Global.LanguageManager = new FluentValidationLanguageManager();
        ValidatorOptions.Global.DisplayNameResolver = (type, member, expression) =>
        {
            return member?.Name;
        };
    }
}
```

I hate (!!!) doing this, as this fiddles around with static properties, which may be set and unset at will. To group things together I create a single static method, which makes it easier. Now we need to add `FluentValidationLanguageManager.SetGlobalOptions();` to the `ConfigureServices` of the API. You also need to trigger this in your tests.

## Drawback

The main (theoretical) drawback of this way of working, is the `InlineValidator`. It will be constructed every time we're validating the object. For small validations, that won't be a problem, but if you have huge trees of objects that need to be validated, it might increase your footprint a bit.

## Conclusion

I think I'm going to use FluentAssertions with the `InlineValidator` in my projects. I currently use my own [DataAnnotationsValidator](https://keestalkstech.com/data-annotation-validation-in-a-business-service/#dataannotationsvalidator-implementation) to inject the validator into my business services. Now, the FluentAssertions lib also provides some DI capabilities, so I will check that out in the future.

The code is on GitHub, so check it out: [code gallery / 14. validation](https://github.com/KeesCBakker/keestalkstech-code-gallery/tree/main/14.validation).

## Changelog

- 2025-05-27: renamed the object.
- 2025-05-17: added the [section on localization](#a-note-on-localization).
- 2025-04-06: initial article.
