FluentValidation.AutoValidation icon indicating copy to clipboard operation
FluentValidation.AutoValidation copied to clipboard

NativeAOT hiccups

Open devtekve opened this issue 5 months ago • 4 comments

I've been trying to use this project with a minimal API and I've been playing with making it NativeAOT. I found that there's a weird issue and got curious to figure out why.

This is the stack trace I got:

System.NotSupportedException: 'FluentValidation.IValidator`1[Microsoft.Extensions.Logging.Logger`1[Contoso.Runtime.IEndpoint]]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT. Inspect and fix trimming and AOT related warnings that were generated when the app was published. For more information see https://aka.ms/nativeaot-compatibility
   at System.Reflection.Runtime.General.TypeUnifier.WithVerifiedTypeHandle(RuntimeConstructedGenericTypeInfo, RuntimeTypeInfo[]) + 0x98
   at System.Reflection.Runtime.TypeInfos.RuntimeTypeInfo.MakeGenericType(Type[]) + 0x248
   at SharpGrip.FluentValidation.AutoValidation.Shared.Extensions.ServiceProviderExtensions.GetValidator(IServiceProvider, Type) + 0x6c
   at SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters.FluentValidationAutoValidationEndpointFilter.<InvokeAsync>d__0.MoveNext() + 0x2a0
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Http.Generated.<GeneratedRouteBuilderExtensions_g>F60C50E4107966576BFC90421C6592150AACC3F9288F7BF4F7F07549415ED8317__GeneratedRouteBuilderExtensionsCore.<>c__DisplayClass4_0.<<MapGet0>g__RequestHandlerFiltered|6>d.MoveNext() + 0x3d4
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<<Invoke>g__AwaitRequestTask|7_0>d.MoveNext() + 0x60
--- End of stack trace from previous location ---
   at Contoso.Runtime.ExceptionMiddleware.<InvokeAsync>d__3.MoveNext() + 0x534
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.<Invoke>d__14.MoveNext() + 0x94

this is the minimal API code (trimmed for simplicity)

{
...
    byDeviceSettingsGroup.MapGet("/", GetAvailableSettingsAsync).AddFluentValidationAutoValidation().ProducesProblem(StatusCodes.Status404NotFound);
...
}
private static async Task<IResult> GetAvailableSettingsAsync([FromServices] ILogger<IEndpoint> logger, string deviceId)
{
    logger.LogWarning("Entering {fn} for device {deviceId}", nameof(GetAvailableSettingsAsync), deviceId);
    return TypedResults.Ok();
}

So the error looked odd to me, why was it trying to find a Validator from a service injected, but it makes sense given the auto discovery nature... Anyway. I found that the main issue is the following:

https://github.com/SharpGrip/FluentValidation.AutoValidation/blob/a9c5045231430ba116d15dae7883d988ab3f66fe/FluentValidation.AutoValidation.Shared/src/Extensions/ServiceProviderExtensions.cs#L10 called from https://github.com/SharpGrip/FluentValidation.AutoValidation/blob/a9c5045231430ba116d15dae7883d988ab3f66fe/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs#L19

The code above works normally with the JIT because the type is known at runtime, and we get null after asking the service provider to give us the service (because of course, there's no validator registered for the logger). But on NativeAOT / trimmed assemblies it fails to evaluate the type to ask for, because the type itself doesn't exist. So we basically fail before the GetService can return null

I don't really expect this to be fixed, to my knowledge, nowhere on this library says it's meant to be trimmable or nativeAOT but I thought I'd share my experience as it was a good learning exercise.

devtekve avatar Jul 06 '25 07:07 devtekve

I’ve been experimenting with this and wanted to share what I found is that adding [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IValidator<>))] at startup (or in any appropriate location) prevents the NotSupportedException from being thrown.

What worked for me

Manually registering the validator

  1. Adding:
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IValidator<>))]
  1. Explicitly registering the validator:
services.TryAddScoped<IValidator<RequestModel>, RequestModelValidator>();

Declaring the validator as a dynamic dependency

  • Using both:

    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IValidator<>))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(RequestModelValidator))]
    
  • Then calling:

    builder.Services.AddValidatorsFromAssembly(...);
    

Recommendation

Always prefer:

[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IValidator<>))]

...instead of:

[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(IValidator<RequestModel>))]

The open generic form (IValidator<>) ensures that the runtime includes the necessary metadata for all generic instantiations of IValidator<T>, not just one specific type. This is especially important when your endpoint has dependencies like endpoint([FromServices] Service service, [FromBody] RequestModel model){...} otherwise, trimming or AOT compilation may strip required metadata and trigger a NotSupportedException at runtime.

devtekve avatar Jul 06 '25 09:07 devtekve

Hi @devtekve, very interesting. Haven't been doing a lot of AOT/trimming as of yet but if we can support it without too much changes I am all for it!

Do you think there is a way for the library to allow for AOT/trimming?

mvdgun avatar Jul 10 '25 22:07 mvdgun

@mvdgun I have a feeling it actually is, or at least for the most part. Since you are interested I can take some time during the weekend to fork it and see if I can do something on it so that it’s compatible out of the box instead of the user having to declare DynamicDependency.

But after adding what I added to my project the (basic) validation I have actually worked as expected

devtekve avatar Jul 11 '25 15:07 devtekve

@mvdgun sorry I've been busy so quality is not the highest, but this is what I have so far, I think it should be enough https://github.com/SharpGrip/FluentValidation.AutoValidation/pull/67

I will refine the PR with details if the direction is good with you

devtekve avatar Jul 13 '25 09:07 devtekve