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:
- 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.
- These types are abstract, internal, or open generic -> the DI container cannot instantiate them. But they still get registered via ServiceDescriptor.
- 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:
- It only protects against types named exactly InlineValidator or AbstractValidator
- Any new type added by FluentValidation in a future version slips through the filter
- 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