NativeAOT Hiccups SharpGrip And FluentValidation AutoValidation
Introduction
In this article, we delve into a peculiar issue encountered while attempting to utilize the SharpGrip.FluentValidation.AutoValidation library within a minimal API setup, specifically when employing NativeAOT compilation. This exploration stemmed from a personal endeavor to make the application NativeAOT compatible, revealing an intriguing problem during the process. This article will discuss the encountered stack trace, the relevant code snippets, and the underlying cause of the issue.
Understanding NativeAOT and Trimming
Before diving into the specifics, let's briefly touch upon NativeAOT and trimming. NativeAOT (Ahead-of-Time compilation) is a compilation technique that converts .NET code into native code at build time. This approach offers several advantages, including faster startup times and reduced application size. However, it also introduces certain constraints, particularly concerning reflection and dynamic code generation.
Trimming, on the other hand, is a process that removes unused code from the application to further reduce its size. While beneficial for deployment, trimming can also lead to runtime issues if essential code or metadata is inadvertently removed.
The Stack Trace
When running the application compiled with NativeAOT, the following stack trace was observed:
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 stack trace indicates a System.NotSupportedException
, specifically stating that FluentValidation.IValidator'1[Microsoft.Extensions.Logging.Logger'1[Contoso.Runtime.IEndpoint]]
is missing native code or metadata. The exception message suggests that this issue may arise due to code incompatible with trimming or AOT, recommending an inspection of trimming and AOT-related warnings generated during the application's publishing process.
Analyzing the Error Message
The core of the problem lies in the inability to locate the validator for ILogger<IEndpoint>
. This is unexpected, as the validator should not be required for the logger itself. The exception suggests that the type FluentValidation.IValidator'1[Microsoft.Extensions.Logging.Logger'1[Contoso.Runtime.IEndpoint]]
is missing native code or metadata, indicating a potential issue with AOT compatibility or trimming.
Minimal API Code Snippet
The following code snippet represents a simplified version of the Minimal API endpoint that triggered the exception:
{
...
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();
}
In this snippet, the GetAvailableSettingsAsync
method is mapped to a GET request and configured with FluentValidation auto-validation. The method injects an ILogger<IEndpoint>
instance via dependency injection and logs a warning message. The unexpected part is that the auto-validation mechanism seems to be attempting to resolve a validator for the injected logger, which is not the intended behavior.
Identifying the Root Cause
The investigation led to the following code snippet within the SharpGrip.FluentValidation.AutoValidation library:
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
This code attempts to resolve a validator from the service provider based on the type of the injected dependency. In the case of the JIT (Just-In-Time) compiler, this process works as expected. The type is known at runtime, and the service provider is queried for a validator. If no validator is registered for the specific type (e.g., ILogger<IEndpoint>
), the GetService
method returns null
, and the execution continues without issues.
The NativeAOT Difference
However, with NativeAOT and trimmed assemblies, the behavior diverges. The issue arises because the type evaluation fails before the GetService
method can be invoked. In essence, the type information required to resolve the validator is not available at runtime due to AOT compilation and trimming. This leads to the NotSupportedException
being thrown before the service provider has a chance to return null
.
In the NativeAOT context, the key difference is that the application is compiled ahead of time, and unused code is aggressively trimmed to reduce the application's size. This trimming process can remove metadata required for runtime type resolution, especially for generic types and types used in reflection. In this specific scenario, the attempt to resolve FluentValidation.IValidator'1[Microsoft.Extensions.Logging.Logger'1[Contoso.Runtime.IEndpoint]]
fails because the necessary metadata for this generic type is not available at runtime.
Deeper Dive into the Code
Let's examine the relevant code snippets from the SharpGrip.FluentValidation.AutoValidation library to understand the issue better.
ServiceProviderExtensions.cs
public static IValidator GetValidator(this IServiceProvider serviceProvider, Type type)
{
var validatorType = typeof(IValidator<>).MakeGenericType(type);
return serviceProvider.GetService(validatorType) as IValidator;
}
This extension method attempts to create a generic IValidator<T>
type based on the input type
and then resolves it from the service provider. The MakeGenericType
method is used to construct the specific validator type at runtime. This is where the issue arises in NativeAOT, as the MakeGenericType
call can fail if the necessary metadata for the generic type is not available.
FluentValidationAutoValidationEndpointFilter.cs
public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var serviceProvider = context.HttpContext.RequestServices;
foreach (var argument in context.Arguments)
{
if (argument == null)
{
continue;
}
var validator = serviceProvider.GetValidator(argument.GetType());
if (validator != null)
{
// Validation logic
}
}
return await next(context);
}
}
This filter iterates through the arguments of the endpoint and attempts to resolve a validator for each argument type. The serviceProvider.GetValidator
call is where the NotSupportedException
is thrown in the NativeAOT context.
The Core Problem Summary
The fundamental issue is that the SharpGrip.FluentValidation.AutoValidation library uses runtime type construction (MakeGenericType
) to resolve validators. In a NativeAOT environment, this can fail if the necessary metadata for the constructed types is not available due to trimming or AOT compilation constraints. This is particularly problematic when dealing with generic types, such as IValidator<T>
, where the type parameter T
is determined at runtime.
Conclusion
While the SharpGrip.FluentValidation.AutoValidation library functions correctly under JIT compilation, it exhibits compatibility issues within NativeAOT environments due to its reliance on runtime type construction. The observed NotSupportedException
highlights the challenges of using reflection and dynamic type creation in AOT-compiled applications. This exploration serves as a valuable learning experience, emphasizing the importance of considering AOT compatibility when selecting and utilizing libraries within .NET applications. It's essential to evaluate whether a library is designed to be trimmable and AOT-compatible before incorporating it into a NativeAOT project. While a fix for this specific issue within the library is not anticipated, understanding the root cause aids in making informed decisions about library selection and application architecture in NativeAOT scenarios.
Lessons Learned
This experience underscores the following key takeaways:
- NativeAOT Limitations: NativeAOT imposes constraints on reflection and dynamic code generation. Libraries that heavily rely on these features may not be directly compatible.
- Trimming Considerations: Trimming can remove essential metadata, leading to runtime exceptions. It's crucial to carefully analyze trimming warnings and ensure that necessary code and metadata are preserved.
- Library Compatibility: Before adopting a library in a NativeAOT project, verify its compatibility with AOT and trimming. Consult the library's documentation or community forums for guidance.
- Alternative Approaches: If a library is not AOT-compatible, explore alternative approaches or libraries that provide similar functionality while adhering to AOT constraints.
Future Considerations
As NativeAOT adoption grows, it is anticipated that more libraries will strive to become AOT-compatible. However, until then, developers must carefully assess the compatibility of their dependencies and make informed choices to ensure their applications function correctly in NativeAOT environments.
This exploration into NativeAOT hiccups with SharpGrip and FluentValidation.AutoValidation provides valuable insights into the challenges and considerations when working with NativeAOT. By understanding these issues, developers can better navigate the complexities of AOT compilation and build robust, high-performance .NET applications.