diff --git a/Directory.Build.props b/Directory.Build.props index 2695885..c28e3a6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -66,8 +66,7 @@ - - $(NoWarn);CA1014;CA1031;CA1052;CA1515;CA1707;CA1724;CA1812;CA1848;CA1852;CA2007;RCS1102;IL2026;IL2067;IL2070;IL2075;IL3050;IL2104;IL3053 +$(NoWarn);CA1014;CA1031;CA1052;CA1515;CA1707;CA1724;CA1812;CA1848;CA1852;CA2007;RCS1102 diff --git a/Directory.Packages.props b/Directory.Packages.props index 0731f86..fb0f1b7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,9 @@ true + + diff --git a/kanban/done/002-fix-aot-trim-warnings.md b/kanban/done/002-fix-aot-trim-warnings.md new file mode 100644 index 0000000..1b2f28a --- /dev/null +++ b/kanban/done/002-fix-aot-trim-warnings.md @@ -0,0 +1,66 @@ +# Fix AOT and Trim Warnings + +## Summary + +Package produces IL2104/IL3053 trim and AOT analysis warnings when used in applications published with `PublishAot=true`. The package needs to be made fully AOT-compatible. + +**GitHub Issue:** #7 + +## Todo List + +- [x] Identify reflection usage causing trim warnings +- [x] Add `[DynamicallyAccessedMembers]` attributes where needed +- [x] Consider source generators for reflection-based binding +- [x] Add `true` to project +- [x] Add `true` to project +- [x] Test with AOT publish to verify zero warnings +- [ ] Update package version + +## Notes + +**Current warnings:** +``` +TimeWarp.OptionsValidation.dll : warning IL2104: Assembly 'TimeWarp.OptionsValidation' produced trim warnings. +TimeWarp.OptionsValidation.dll : warning IL3053: Assembly 'TimeWarp.OptionsValidation' produced AOT analysis warnings. +``` + +**Reproduction steps:** +1. Create a .NET 10 application using `TimeWarp.OptionsValidation` +2. Use `AddFluentValidatedOptions(config)` +3. Publish with AOT: `dotnet publish -p:PublishAot=true` + +**Package Version:** 1.0.0-beta.3 + +**Related:** TimeWarp.Nuru task 018-make-configuration-validation-sample-aot-compatible + +## Results + +### Changes Made: + +**1. `service-collection-extensions.cs`** +- Added `using System.Diagnostics.CodeAnalysis;` +- Added `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]` to `TOptions` on the configuration binding overload (required by `Bind()`) +- Added `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]` to `TValidator` on both overloads + +**2. `options-builder-extensions.cs`** +- Added `using System.Diagnostics.CodeAnalysis;` +- Added `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]` to `TValidator` (required by `TryAddSingleton()`) + +**3. `timewarp-options-validation.csproj`** +- Added `true` +- Added `true` + +**4. New AOT test project** (`tests/aot-test/`) +- Created test project that publishes with AOT to verify no trim/AOT warnings + +### Test Results: + +| Test | Result | +|------|--------| +| `dotnet build -warnaserror` | ✅ 0 warnings, 0 errors | +| `dotnet test` | ✅ Passed: 4, Skipped: 1 | +| `dotnet publish -c Release` (AOT) | ✅ 0 IL2104/IL3053 warnings | +| Run AOT binary | ✅ Output: "Name: Test, Value: 42" | + +### Decision: +Source generators were not needed - the `[DynamicallyAccessedMembers]` attributes were sufficient to eliminate all warnings while maintaining full AOT compatibility. diff --git a/source/Directory.Build.props b/source/Directory.Build.props index eb92710..9eeb8d1 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -4,7 +4,7 @@ - 1.0.0-beta.3 + 1.0.0-beta.4 Steven T. Cramer TimeWarp Enterprises TimeWarp.OptionsValidation diff --git a/source/timewarp-options-validation/extensions/options-builder-extensions.cs b/source/timewarp-options-validation/extensions/options-builder-extensions.cs index 1fa6f02..9a03653 100644 --- a/source/timewarp-options-validation/extensions/options-builder-extensions.cs +++ b/source/timewarp-options-validation/extensions/options-builder-extensions.cs @@ -1,5 +1,6 @@ namespace Microsoft.Extensions.Options; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using FluentValidation; @@ -26,7 +27,7 @@ public static class OptionsBuilderExtensions /// .ValidateOnStart(); /// /// - public static OptionsBuilder ValidateFluentValidation( + public static OptionsBuilder ValidateFluentValidation( this OptionsBuilder optionsBuilder) where TOptions : class where TValidator : AbstractValidator diff --git a/source/timewarp-options-validation/extensions/service-collection-extensions.cs b/source/timewarp-options-validation/extensions/service-collection-extensions.cs index 54321e0..e6620b5 100644 --- a/source/timewarp-options-validation/extensions/service-collection-extensions.cs +++ b/source/timewarp-options-validation/extensions/service-collection-extensions.cs @@ -1,5 +1,6 @@ -namespace Microsoft.Extensions.DependencyInjection; +namespace Microsoft.Extensions.DependencyInjection; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Options; public static class ServiceCollectionExtensions @@ -34,7 +35,9 @@ public static class ServiceCollectionExtensions /// .ValidateOnStart(); /// /// - public static OptionsBuilder AddFluentValidatedOptions( + [RequiresUnreferencedCode("TOptions's dependent types may have their members trimmed. Ensure all required members are preserved.")] + [RequiresDynamicCode("Binding strongly typed objects to configuration values may require generating dynamic code at runtime.")] + public static OptionsBuilder AddFluentValidatedOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TValidator>( this IServiceCollection services, IConfiguration configuration) where TOptions : class @@ -71,7 +74,7 @@ public static OptionsBuilder AddFluentValidatedOptions /// - public static OptionsBuilder AddFluentValidatedOptions( + public static OptionsBuilder AddFluentValidatedOptions( this IServiceCollection services, Action configureOptions) where TOptions : class diff --git a/source/timewarp-options-validation/timewarp-options-validation.csproj b/source/timewarp-options-validation/timewarp-options-validation.csproj index e1cddd4..965886d 100644 --- a/source/timewarp-options-validation/timewarp-options-validation.csproj +++ b/source/timewarp-options-validation/timewarp-options-validation.csproj @@ -3,6 +3,8 @@ TimeWarp.OptionsValidation TimeWarp.OptionsValidation + true + true diff --git a/tests/aot-test/aot-test.csproj b/tests/aot-test/aot-test.csproj new file mode 100644 index 0000000..af2ac9c --- /dev/null +++ b/tests/aot-test/aot-test.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + true + true + + + + + + + + + diff --git a/tests/aot-test/program.cs b/tests/aot-test/program.cs new file mode 100644 index 0000000..25e88c5 --- /dev/null +++ b/tests/aot-test/program.cs @@ -0,0 +1,48 @@ +using FluentValidation; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using TimeWarp.OptionsValidation; + +namespace AotTest; + +internal static class Program +{ + public static void Main() + { + // Create configuration + IConfigurationRoot configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["TestOptions:Name"] = "Test", + ["TestOptions:Value"] = "42" + }) + .Build(); + + // Setup DI with options validation + ServiceCollection services = new(); + services.AddFluentValidatedOptions(configuration) + .ValidateOnStart(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + IOptions options = serviceProvider.GetRequiredService>(); + + Console.WriteLine($"Name: {options.Value.Name}, Value: {options.Value.Value}"); + } +} + +[ConfigurationKey("TestOptions")] +internal sealed class TestOptions +{ + public string Name { get; set; } = string.Empty; + public int Value { get; set; } +} + +internal sealed class TestOptionsValidator : AbstractValidator +{ + public TestOptionsValidator() + { + RuleFor(x => x.Name).NotEmpty(); + RuleFor(x => x.Value).GreaterThan(0); + } +}