Skip to content

Commit 2dd1f8d

Browse files
feat: annotate trim/AOT requirements ([RequiresUnreferencedCode]/[RequiresDynamicCode]) (#6)
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 <noreply@anthropic.com>
1 parent dc34f4c commit 2dd1f8d

3 files changed

Lines changed: 42 additions & 0 deletions

File tree

‎Scrutor.Extensions.HttpClient/Extensions/HttpClientExtensions.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System;
22
using System.Linq;
33
using System.Reflection;
4+
#if NET8_0_OR_GREATER
5+
using System.Diagnostics.CodeAnalysis;
6+
#endif
47
using Scrutor;
58

69
namespace Microsoft.Extensions.DependencyInjection;
@@ -14,12 +17,24 @@ namespace Microsoft.Extensions.DependencyInjection;
1417
/// See https://github.com/khellang/Scrutor/issues/180.</remarks>
1518
public static class HttpClientExtensions
1619
{
20+
// Honest trim/AOT signal: registration runs Scrutor's runtime assembly scan and closes the
21+
// generic AddHttpClient<TClient,TImplementation> over the scanned types via reflection. Consumers
22+
// building trimmed/AOT get a warning at these entry points instead of silent breakage.
23+
private const string TrimMessage =
24+
"Registers types discovered by Scrutor's runtime assembly scan as typed HttpClients; the scanned client types may be removed by the trimmer.";
25+
private const string AotMessage =
26+
"Closes the generic AddHttpClient<TClient,TImplementation> over the scanned types via MakeGenericMethod, which requires runtime code generation.";
27+
1728
/// <summary>
1829
/// Registers each scanned service as a typed <see cref="System.Net.Http.HttpClient"/> bound to
1930
/// the named client <paramref name="name"/> (so they share that client's configuration).
2031
/// </summary>
2132
/// <param name="selector">The Scrutor service-type selector.</param>
2233
/// <param name="name">The name of a client registered via <c>AddHttpClient(name, ...)</c>.</param>
34+
#if NET8_0_OR_GREATER
35+
[RequiresUnreferencedCode(TrimMessage)]
36+
[RequiresDynamicCode(AotMessage)]
37+
#endif
2338
public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector selector, string name = "")
2439
=> selector.UsingRegistrationStrategy(new HttpClientRegistrationStrategy(name));
2540

@@ -28,6 +43,10 @@ public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector select
2843
/// its own default client named after the service type.
2944
/// </summary>
3045
/// <param name="selector">The Scrutor service-type selector.</param>
46+
#if NET8_0_OR_GREATER
47+
[RequiresUnreferencedCode(TrimMessage)]
48+
[RequiresDynamicCode(AotMessage)]
49+
#endif
3150
public static IServiceTypeSelector AsHttpClient(this IServiceTypeSelector selector)
3251
=> selector.UsingRegistrationStrategy(new HttpClientRegistrationStrategy(name: null));
3352

@@ -44,6 +63,13 @@ private sealed class HttpClientRegistrationStrategy(string? name) : Registration
4463
private static readonly MethodInfo NamedAddHttpClient = ResolveAddHttpClient(named: true);
4564
private static readonly MethodInfo UnnamedAddHttpClient = ResolveAddHttpClient(named: false);
4665

66+
// Apply overrides Scrutor's (un-annotated) RegistrationStrategy.Apply, so the RequiresDynamicCode
67+
// warning can't propagate here. It's already surfaced to consumers on the public AsHttpClient
68+
// entry points (the only way to reach this strategy), so suppress the internal MakeGenericMethod site.
69+
#if NET8_0_OR_GREATER
70+
[UnconditionalSuppressMessage("AOT", "IL3050",
71+
Justification = "AsHttpClient() entry points are marked [RequiresDynamicCode]; callers are warned. AddHttpClient<,> has no non-generic overload.")]
72+
#endif
4773
public override void Apply(IServiceCollection services, ServiceDescriptor descriptor)
4874
{
4975
var serviceType = descriptor.ServiceType;

‎Scrutor.Extensions.HttpClient/Scrutor.Extensions.HttpClient.csproj‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
<LangVersion>latest</LangVersion>
1111
</PropertyGroup>
1212

13+
<!-- Turn on the trim + AOT analyzers on the modern target so our own build validates the
14+
[RequiresUnreferencedCode]/[RequiresDynamicCode] annotations are well-formed and the
15+
intentional reflection is properly justified. (netstandard2.0 has no such analyzers.) -->
16+
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
17+
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
18+
<EnableAotAnalyzer>true</EnableAotAnalyzer>
19+
</PropertyGroup>
20+
1321
<PropertyGroup Label="Package">
1422
<Title>Scrutor HttpClient Extension</Title>
1523
<Authors>Chrison Simtian</Authors>

‎tests/Scrutor.Extensions.HttpClient.Specs/PublicApiSpecs.Public_api_surface_has_not_changed.verified.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ namespace Microsoft.Extensions.DependencyInjection
55
{
66
public static class HttpClientExtensions
77
{
8+
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Closes the generic AddHttpClient<TClient,TImplementation> over the scanned types " +
9+
"via MakeGenericMethod, which requires runtime code generation.")]
10+
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registers types discovered by Scrutor\'s runtime assembly scan as typed HttpClient" +
11+
"s; the scanned client types may be removed by the trimmer.")]
812
public static Scrutor.IServiceTypeSelector AsHttpClient(this Scrutor.IServiceTypeSelector selector) { }
13+
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Closes the generic AddHttpClient<TClient,TImplementation> over the scanned types " +
14+
"via MakeGenericMethod, which requires runtime code generation.")]
15+
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registers types discovered by Scrutor\'s runtime assembly scan as typed HttpClient" +
16+
"s; the scanned client types may be removed by the trimmer.")]
917
public static Scrutor.IServiceTypeSelector AsHttpClient(this Scrutor.IServiceTypeSelector selector, string name = "") { }
1018
}
1119
}

0 commit comments

Comments
 (0)