From 91e6217e2c03f56f7a9fb1e83831409d3d76aff6 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Fri, 24 Jul 2026 15:07:31 +1200 Subject: [PATCH] feat: annotate trim/AOT requirements ([RequiresUnreferencedCode]/[RequiresDynamicCode]) Honest trim/AOT signal rather than a source-gen rewrite: the real blocker is Scrutor's runtime assembly scan (fundamentally not trim/AOT-safe), so removing our own reflection would gain nothing while staying a Scrutor extension. Instead, the public AsHttpClient entry points now carry [RequiresUnreferencedCode] + [RequiresDynamicCode] (net8.0+), so consumers building trimmed/AOT get accurate warnings at the call site instead of silent breakage. - Enable EnableTrimAnalyzer + EnableAotAnalyzer on the net8.0 target to self-validate the annotations. Only IL3050 (MakeGenericMethod) surfaced; suppressed at that site with [UnconditionalSuppressMessage] + justification (the override can't carry Requires*; the public entry points already warn callers). - Attributes/usings guarded by NET8_0_OR_GREATER (the attrs + analyzers don't exist on netstandard2.0, where trimming/AOT isn't a concern). - Public API baseline updated to include the new attributes. Build clean (0 warnings both TFMs); all 7 specs pass. Co-Authored-By: Claude Opus 4.8 --- .../Extensions/HttpClientExtensions.cs | 26 +++++++++++++++++++ .../Scrutor.Extensions.HttpClient.csproj | 8 ++++++ ...c_api_surface_has_not_changed.verified.txt | 8 ++++++ 3 files changed, 42 insertions(+) diff --git a/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs b/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs index 073f9f1..ff2e421 100644 --- a/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs +++ b/Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Linq; using System.Reflection; +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using Scrutor; namespace Microsoft.Extensions.DependencyInjection; @@ -14,12 +17,24 @@ namespace Microsoft.Extensions.DependencyInjection; /// See https://github.com/khellang/Scrutor/issues/180. public static class HttpClientExtensions { + // Honest trim/AOT signal: registration runs Scrutor's runtime assembly scan and closes the + // generic AddHttpClient over the scanned types via reflection. Consumers + // building trimmed/AOT get a warning at these entry points instead of silent breakage. + private const string TrimMessage = + "Registers types discovered by Scrutor's runtime assembly scan as typed HttpClients; the scanned client types may be removed by the trimmer."; + private const string AotMessage = + "Closes the generic AddHttpClient over the scanned types via MakeGenericMethod, which requires runtime code generation."; + /// /// Registers each scanned service as a typed bound to /// the named client (so they share that client's configuration). /// /// The Scrutor service-type selector. /// The name of a client registered via AddHttpClient(name, ...). +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode(TrimMessage)] + [RequiresDynamicCode(AotMessage)] +#endif public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector selector, string name = "") => selector.UsingRegistrationStrategy(new HttpClientRegistrationStrategy(name)); @@ -28,6 +43,10 @@ public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector select /// its own default client named after the service type. /// /// The Scrutor service-type selector. +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode(TrimMessage)] + [RequiresDynamicCode(AotMessage)] +#endif public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector selector) => selector.UsingRegistrationStrategy(new HttpClientRegistrationStrategy(name: null)); @@ -44,6 +63,13 @@ private sealed class HttpClientRegistrationStrategy(string? name) : Registration private static readonly MethodInfo NamedAddHttpClient = ResolveAddHttpClient(named: true); private static readonly MethodInfo UnnamedAddHttpClient = ResolveAddHttpClient(named: false); + // Apply overrides Scrutor's (un-annotated) RegistrationStrategy.Apply, so the RequiresDynamicCode + // warning can't propagate here. It's already surfaced to consumers on the public AsHttpClient + // entry points (the only way to reach this strategy), so suppress the internal MakeGenericMethod site. +#if NET8_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL3050", + Justification = "AsHttpClient() entry points are marked [RequiresDynamicCode]; callers are warned. AddHttpClient<,> has no non-generic overload.")] +#endif public override void Apply(IServiceCollection services, ServiceDescriptor descriptor) { var serviceType = descriptor.ServiceType; diff --git a/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj b/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj index 109eef2..34c3f38 100644 --- a/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj +++ b/Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj @@ -10,6 +10,14 @@ latest + + + true + true + + Scrutor HttpClient Extension Chrison Simtian diff --git a/tests/Scrutor.Extensions.HttpClient.Specs/PublicApiSpecs.Public_api_surface_has_not_changed.verified.txt b/tests/Scrutor.Extensions.HttpClient.Specs/PublicApiSpecs.Public_api_surface_has_not_changed.verified.txt index 60496c9..d7c71b0 100644 --- a/tests/Scrutor.Extensions.HttpClient.Specs/PublicApiSpecs.Public_api_surface_has_not_changed.verified.txt +++ b/tests/Scrutor.Extensions.HttpClient.Specs/PublicApiSpecs.Public_api_surface_has_not_changed.verified.txt @@ -5,7 +5,15 @@ namespace Microsoft.Extensions.DependencyInjection { public static class HttpClientExtensions { + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Closes the generic AddHttpClient over the scanned types " + + "via MakeGenericMethod, which requires runtime code generation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registers types discovered by Scrutor\'s runtime assembly scan as typed HttpClient" + + "s; the scanned client types may be removed by the trimmer.")] public static Scrutor.IServiceTypeSelector AsHttpClient(this Scrutor.IServiceTypeSelector selector) { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Closes the generic AddHttpClient over the scanned types " + + "via MakeGenericMethod, which requires runtime code generation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registers types discovered by Scrutor\'s runtime assembly scan as typed HttpClient" + + "s; the scanned client types may be removed by the trimmer.")] public static Scrutor.IServiceTypeSelector AsHttpClient(this Scrutor.IServiceTypeSelector selector, string name = "") { } } } \ No newline at end of file