SHARE:

ASP.NET Core: Why I Couldn’t Upgrade FluentValidation Past 11.4 in My Calzolari.Grpc.AspNetCore.Validation Package (and How I Finally Fixed It)

Context

I’m the author of the Calzolari.Grpc.AspNetCore.Validation package, which provides request message validation for gRPC services using FluentValidation. For a long time, the package was pinned to FluentValidation 11.4.0, and I couldn’t upgrade, even to a minor version without the application crashing at startup. The full source code can be found here: https://github.com/AnthonyGiretti/grpc-aspnetcore-validator

Additionally If you’re a consumer of this package, you may have also hit this issue: since the package declares FluentValidation Version=”11.4.0″ in its .csproj, which NuGet interprets as ≥ 11.4.0 , simply installing a more recent version of FluentValidation in your own project is enough to trigger the crash. NuGet resolves to the highest compatible version, and the problem surfaces immediately.

The most confusing part? It compiles perfectly. The error only occurs at runtime.

The Symptom

When the application starts, an exception is thrown during service resolution by the dependency injection (DI) container. The application never starts.

Root Cause

What Changed in FluentValidation After Version 11.4

Starting with FluentValidation 11.5+, the IValidator<T> interface was modified to make the generic parameter contravariant:

That small in keyword has major consequences on reflection behavior at runtime.

The Problematic Code

The issue lies in the AddValidators() method of ServiceCollectionHelper.cs. This method scans all loaded assemblies at runtime to automatically discover validators:

Why It Compiles but Crashes

Assembly scanning via AppDomain.CurrentDomain.GetAssemblies() and GetTypes() is a purely runtime mechanism. The compiler has no way of knowing which types will be present in loaded assemblies at startup. Here’s what happens:

  1. New internal types implementing IValidator<> have been added in recent versions of FluentValidation (wrappers, internal validators, etc.). These types are not named InlineValidator or AbstractValidator -> the name-based filter doesn’t exclude them.
  2. These types are abstract, internal, or open generic -> the DI container cannot instantiate them. But they still get registered via ServiceDescriptor.
  3. At startup, when the DI container tries to resolve the service graph, it encounters these non-instantiable types -> runtime crash.

Why the Name-Based Filter Is Fragile

The original filter relies on a class name blacklist:

.Where(t => !t.Name.Contains("InlineValidator") && !t.Name.Contains("AbstractValidator"))

This is fragile because:

  1. It only protects against types named exactly InlineValidator or AbstractValidator
  2. Any new type added by FluentValidation in a future version slips through the filter
  3. It doesn’t verify whether the type is actually instantiable

The Fix

Here is the corrected version of AddValidators():

The 5 Key Fixes Explained

# Fix Why
1 t.Assembly != fluentValidationAssembly Excludes all FluentValidation internal types, present and future. This is the most important safeguard.
2 !t.IsAbstract Prevents registration of abstract classes that the DI container cannot instantiate.
3 !t.IsInterface && !t.IsGenericTypeDefinition Prevents registration of interfaces and open generic types (e.g., SomeValidator<T>).
4 try/catch on GetTypes() Some assemblies may throw a ReflectionTypeLoadException if not all their dependencies are loaded.
5 continue instead of throw An irrelevant type should not crash the entire application. It is silently skipped.

Conclusion

When performing assembly scanning via reflection, never rely solely on class names to filter. Instead:

  • Filter by source assembly (t.Assembly != …) to exclude types from dependencies
  • Check instantiability (!IsAbstract, !IsInterface, !IsGenericTypeDefinition)
  • Be defensive with try/catch on reflection operations
  • Don’t crash for an irrelevant type (continue rather than throw)

An innocuous change in a dependency, ike adding the in keyword to a generic parameter, can have cascading effects on reflection-based code, while remaining completely invisible at compile time.

If you are using the Calzolari.Grpc.AspNetCore.Validation package you can now download the last version which includes the fix: https://www.nuget.org/packages/Calzolari.Grpc.AspNetCore.Validation/10.1.0

Written by

anthonygiretti

Anthony is a specialist in Web technologies (14 years of experience), in particular Microsoft .NET and learns the Cloud Azure platform. He has received twice the Microsoft MVP award and he is also certified Microsoft MCSD and Azure Fundamentals.