Skip to content

Commit b7f65d6

Browse files
authored
Merge pull request #1492 from autofac/feature/aot-annotations
Annotate core for trimming and Native AOT compatibility
2 parents 1189581 + e2ce0a9 commit b7f65d6

71 files changed

Lines changed: 1299 additions & 74 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/aot.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Native AOT Verification
2+
3+
# Repo-local, additive workflow (the shared org CI in autofac/.github builds and
4+
# tests the solution). This job runs the VerifyAot target, which proves Autofac
5+
# stays Native-AOT/trim clean in two complementary ways:
6+
# 1. VerifyAotWarnings - builds a fixture that uses the [RequiresDynamicCode] /
7+
# [RequiresUnreferencedCode] APIs and asserts those diagnostics still fire,
8+
# guarding against silently losing an annotation.
9+
# 2. The native publish - publishes the smoke-test app with PublishAot=true (the
10+
# ILC trim/AOT analyzer fails on any IL2104/IL3053 since the project treats
11+
# warnings as errors) and executes the native binary, which exits non-zero on
12+
# any runtime failure of the AOT-safe resolve path (or if a dynamic-code
13+
# scenario unexpectedly stops throwing).
14+
15+
on:
16+
pull_request:
17+
branches:
18+
- develop
19+
- main
20+
push:
21+
branches:
22+
- develop
23+
- main
24+
- feature/*
25+
tags:
26+
- v[0-9]+.[0-9]+.[0-9]+
27+
28+
concurrency:
29+
group: ${{ github.workflow }}-${{ github.ref }}
30+
cancel-in-progress: true
31+
32+
jobs:
33+
verify-aot:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@v4
38+
39+
- name: Setup .NET SDK
40+
uses: actions/setup-dotnet@v4
41+
with:
42+
global-json-file: global.json
43+
44+
# Native AOT on Linux compiles and links native code, which requires a C
45+
# toolchain and zlib development headers.
46+
- name: Install Native AOT prerequisites
47+
run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev
48+
49+
- name: Verify Native AOT compatibility
50+
run: dotnet msbuild ./default.proj -t:VerifyAot -p:Configuration=Release

default.proj

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="Current">
33
<PropertyGroup>
44
<!-- Increment the overall semantic version here. -->
5-
<Version>9.2.0</Version>
5+
<Version>9.3.0</Version>
66
<SolutionName>Autofac</SolutionName>
77
<Configuration Condition="'$(Configuration)'==''">Release</Configuration>
88
<ArtifactDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts"))</ArtifactDirectory>
@@ -70,4 +70,84 @@
7070
<MakeDir Directories="$(LogDirectory)" />
7171
<Exec Command="dotnet test &quot;%(SolutionFile.FullPath)&quot; -c $(Configuration) --results-directory &quot;$(LogDirectory)&quot; --logger:trx /p:Version=$(Version) --collect:&quot;XPlat Code Coverage&quot; --settings &quot;$(CoverageRunSettings)&quot;" />
7272
</Target>
73+
<!--
74+
Native AOT verification. Publishes the AOT smoke-test app with PublishAot=true
75+
(which runs the ILC trim/AOT analyzer and would fail the build on any IL2104/
76+
IL3053 warning, since the project treats warnings as errors) and then executes
77+
the produced native binary, asserting a zero exit code. This proves both that
78+
Autofac is statically AOT-clean AND that the AOT-safe resolve path works at
79+
runtime. The project is intentionally excluded from Autofac.sln so the normal
80+
Compile/Test targets do not require the native toolchain.
81+
-->
82+
<PropertyGroup>
83+
<AotProjectDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),'test/Autofac.Test.Aot'))</AotProjectDirectory>
84+
<AotPublishDirectory>$([System.IO.Path]::Combine($(ArtifactDirectory),'aot'))</AotPublishDirectory>
85+
<AotExecutableName>Autofac.Test.Aot</AotExecutableName>
86+
<AotExecutableName Condition="'$(OS)' == 'Windows_NT'">Autofac.Test.Aot.exe</AotExecutableName>
87+
</PropertyGroup>
88+
<Target Name="VerifyAot" DependsOnTargets="VerifyAotWarnings">
89+
<Message Text="****************************************" Importance="high" />
90+
<Message Text="Verifying Native AOT compatibility" Importance="high" />
91+
<Message Text="****************************************" Importance="high" />
92+
<Exec Command="dotnet publish &quot;$(AotProjectDirectory)&quot; -c $(Configuration) --output &quot;$(AotPublishDirectory)&quot;" />
93+
<Exec Command="&quot;$([System.IO.Path]::Combine($(AotPublishDirectory),$(AotExecutableName)))&quot;" />
94+
</Target>
95+
<!--
96+
AOT/trim warning verification. Builds the warning fixture (which calls the APIs
97+
annotated [RequiresDynamicCode] / [RequiresUnreferencedCode]) and asserts the
98+
expected analyzer diagnostics are emitted. This guards against silently LOSING an
99+
annotation: if an attribute is dropped, the warning stops firing and this target
100+
fails. Unlike VerifyAot proper, this needs no native toolchain - a plain build
101+
surfaces the analyzer diagnostics - so it is cheap to run anywhere.
102+
103+
The check is PER CALL SITE, not just per diagnostic code: each ExpectedAotWarning
104+
item names both the code (IL2026/IL3050) and a distinctive substring of the
105+
annotated member's signature as it appears in the warning text. Multiple call
106+
sites share a code (e.g. RegisterGeneric and RegisterGenericDecorator are both
107+
IL3050), so asserting only "IL3050 appears somewhere" would not catch losing the
108+
annotation on just one of them. Matching the member signature catches each one.
109+
110+
Keep the ExpectedAotWarning items in sync with the calls in
111+
test/Autofac.Test.AotWarnings/Program.cs - one item per annotated call.
112+
-->
113+
<PropertyGroup>
114+
<AotWarningsProjectDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),'test/Autofac.Test.AotWarnings'))</AotWarningsProjectDirectory>
115+
</PropertyGroup>
116+
<ItemGroup>
117+
<!-- Identity = the IL code; Member = a substring uniquely identifying the API in the warning text. -->
118+
<ExpectedAotWarning Include="IL3050" Member="RegisterGeneric(ContainerBuilder, Type)" />
119+
<ExpectedAotWarning Include="IL3050" Member="RegisterGenericDecorator(ContainerBuilder, Type, Type" />
120+
<ExpectedAotWarning Include="IL2026" Member="RegisterAssemblyTypes(ContainerBuilder, params Assembly" />
121+
<ExpectedAotWarning Include="IL2026" Member="RegisterAssemblyModules(ContainerBuilder, params Assembly" />
122+
</ItemGroup>
123+
<Target Name="VerifyAotWarnings">
124+
<Message Text="****************************************" Importance="high" />
125+
<Message Text="Verifying AOT/trim warnings still fire" Importance="high" />
126+
<Message Text="****************************************" Importance="high" />
127+
<MakeDir Directories="$(LogDirectory)" />
128+
<PropertyGroup>
129+
<AotWarningsLog>$([System.IO.Path]::Combine($(LogDirectory),'aot-warnings-build.log'))</AotWarningsLog>
130+
</PropertyGroup>
131+
<!--
132+
Build the fixture (full recompile so analyzer diagnostics are always emitted)
133+
and redirect ALL output to a log file. This is deliberate: the fixture emits the
134+
IL2026/IL3050 warnings on purpose, and if they reached the CI job's stdout the
135+
.NET problem matcher would turn each one into a (misleading) PR annotation. By
136+
sending the build output only to a file and reading it back here, the warnings
137+
are verified without ever surfacing on the console. The log is written under the
138+
artifacts directory and inspected below.
139+
-->
140+
<Exec Command="dotnet build &quot;$(AotWarningsProjectDirectory)&quot; -c $(Configuration) --no-incremental &gt; &quot;$(AotWarningsLog)&quot; 2&gt;&amp;1"
141+
IgnoreExitCode="true" />
142+
<ReadLinesFromFile File="$(AotWarningsLog)">
143+
<Output TaskParameter="Lines" ItemName="AotWarningsBuildOutput" />
144+
</ReadLinesFromFile>
145+
<PropertyGroup>
146+
<AotWarningsBuildText>@(AotWarningsBuildOutput, '%0a')</AotWarningsBuildText>
147+
</PropertyGroup>
148+
<!-- Each expected warning must appear with BOTH its code and the specific member signature. -->
149+
<Error Condition="!($(AotWarningsBuildText.Contains('warning %(ExpectedAotWarning.Identity)')) and $(AotWarningsBuildText.Contains('%(ExpectedAotWarning.Member)')))"
150+
Text="Expected AOT/trim diagnostic %(ExpectedAotWarning.Identity) for '%(ExpectedAotWarning.Member)' was NOT emitted by Autofac.Test.AotWarnings. A [RequiresDynamicCode]/[RequiresUnreferencedCode] annotation may have been lost in core Autofac." />
151+
<Message Text="All expected AOT/trim warnings were emitted (@(ExpectedAotWarning->'%(Identity): %(Member)', '; '))." Importance="high" />
152+
</Target>
73153
</Project>

src/Autofac/Autofac.csproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@
4646
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
4747
<NoWarn>$(NoWarn);8600;8601;8602;8603;8604</NoWarn>
4848
</PropertyGroup>
49+
<!--
50+
Advertise the assembly as AOT/trim-compatible on the modern TFMs (net7.0+);
51+
netstandard targets cannot be AOT-analyzed. IsTargetFrameworkCompatible keeps
52+
this robust as TFMs are added/removed. IsAotCompatible implicitly enables the
53+
trim, AOT, and single-file analyzers, so combined with Release
54+
TreatWarningsAsErrors this is a permanent in-build gate: any new unannotated
55+
reflection/dynamic-code call site fails the build. APIs that are inherently
56+
incompatible (open generics, assembly scanning, generated factories, strongly
57+
typed metadata, etc.) are individually annotated [RequiresUnreferencedCode] /
58+
[RequiresDynamicCode] so the warning surfaces at the consumer's call site.
59+
-->
60+
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
61+
<IsAotCompatible>true</IsAotCompatible>
62+
</PropertyGroup>
4963
<ItemGroup>
5064
<None Include="..\..\build\icon.png" Pack="true" PackagePath="\" />
5165
<None Include="..\..\README.md" Pack="true" PackagePath="\" />

src/Autofac/Builder/ConcreteReflectionActivatorData.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Autofac.Core;
55
using Autofac.Core.Activators.Reflection;
6+
using Autofac.Util;
67

78
namespace Autofac.Builder;
89

@@ -15,7 +16,7 @@ public class ConcreteReflectionActivatorData : ReflectionActivatorData, IConcret
1516
/// Initializes a new instance of the <see cref="ConcreteReflectionActivatorData"/> class.
1617
/// </summary>
1718
/// <param name="implementer">Type that will be activated.</param>
18-
public ConcreteReflectionActivatorData(Type implementer)
19+
public ConcreteReflectionActivatorData([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type implementer)
1920
: base(implementer)
2021
{
2122
}

src/Autofac/Builder/ReflectionActivatorData.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Autofac.Core;
55
using Autofac.Core.Activators.Reflection;
6+
using Autofac.Util;
67

78
namespace Autofac.Builder;
89

@@ -14,6 +15,7 @@ public class ReflectionActivatorData
1415
private static readonly IConstructorFinder _defaultConstructorFinder = new DefaultConstructorFinder();
1516
private static readonly IConstructorSelector _defaultConstructorSelector = new MostParametersConstructorSelector();
1617

18+
[DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)]
1719
private Type _implementer = default!;
1820
private IConstructorFinder _constructorFinder;
1921
private IConstructorSelector _constructorSelector;
@@ -22,7 +24,7 @@ public class ReflectionActivatorData
2224
/// Initializes a new instance of the <see cref="ReflectionActivatorData"/> class.
2325
/// </summary>
2426
/// <param name="implementer">Type that will be activated.</param>
25-
public ReflectionActivatorData(Type implementer)
27+
public ReflectionActivatorData([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type implementer)
2628
{
2729
ImplementationType = implementer;
2830

@@ -33,6 +35,7 @@ public ReflectionActivatorData(Type implementer)
3335
/// <summary>
3436
/// Gets or sets the implementation type.
3537
/// </summary>
38+
[DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)]
3639
public Type ImplementationType
3740
{
3841
get

src/Autofac/Builder/RegistrationBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public static IRegistrationBuilder<object, SimpleActivatorData, SingleRegistrati
5959
/// </summary>
6060
/// <typeparam name="TImplementer">Implementation type to register.</typeparam>
6161
/// <returns>A registration builder.</returns>
62-
public static IRegistrationBuilder<TImplementer, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType<TImplementer>()
62+
public static IRegistrationBuilder<TImplementer, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType<[DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] TImplementer>()
6363
where TImplementer : notnull
6464
{
6565
// Open generics can't be generic type parameters so we don't have to check for that here.
@@ -79,7 +79,7 @@ public static IRegistrationBuilder<TImplementer, ConcreteReflectionActivatorData
7979
/// </summary>
8080
/// <param name="implementationType">Implementation type to register.</param>
8181
/// <returns>A registration builder.</returns>
82-
public static IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType(Type implementationType)
82+
public static IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type implementationType)
8383
{
8484
if (implementationType is null)
8585
{

src/Autofac/Builder/RegistrationExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static class RegistrationExtensions
2727
/// and this method is generally not required.</remarks>
2828
[Obsolete("Update your code to use the Func<T> implicit relationship or delegate factories. See https://autofac.readthedocs.io/en/latest/resolve/relationships.html and https://autofac.readthedocs.io/en/latest/advanced/delegate-factories.html for more information.")]
2929
public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
30-
RegisterGeneratedFactory(this ContainerBuilder builder, Type delegateType)
30+
RegisterGeneratedFactory(this ContainerBuilder builder, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type delegateType)
3131
{
3232
if (delegateType == null)
3333
{
@@ -51,7 +51,7 @@ public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, Sing
5151
/// this method is generally not required.</remarks>
5252
[Obsolete("Update your code to use the Func<T> implicit relationship or delegate factories. See https://autofac.readthedocs.io/en/latest/resolve/relationships.html and https://autofac.readthedocs.io/en/latest/advanced/delegate-factories.html for more information.")]
5353
public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
54-
RegisterGeneratedFactory(this ContainerBuilder builder, Type delegateType, Service service)
54+
RegisterGeneratedFactory(this ContainerBuilder builder, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type delegateType, Service service)
5555
{
5656
if (builder == null)
5757
{
@@ -72,7 +72,7 @@ public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, Sing
7272
/// and this method is generally not required.</remarks>
7373
[Obsolete("Update your code to use the Func<T> implicit relationship or delegate factories. See https://autofac.readthedocs.io/en/latest/resolve/relationships.html and https://autofac.readthedocs.io/en/latest/advanced/delegate-factories.html for more information.")]
7474
public static IRegistrationBuilder<TDelegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
75-
RegisterGeneratedFactory<TDelegate>(this ContainerBuilder builder, Service service)
75+
RegisterGeneratedFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TDelegate>(this ContainerBuilder builder, Service service)
7676
where TDelegate : class
7777
{
7878
if (builder == null)
@@ -93,7 +93,7 @@ public static IRegistrationBuilder<TDelegate, GeneratedFactoryActivatorData, Sin
9393
/// and this method is generally not required.</remarks>
9494
[Obsolete("Update your code to use the Func<T> implicit relationship or delegate factories. See https://autofac.readthedocs.io/en/latest/resolve/relationships.html and https://autofac.readthedocs.io/en/latest/advanced/delegate-factories.html for more information.")]
9595
public static IRegistrationBuilder<TDelegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
96-
RegisterGeneratedFactory<TDelegate>(this ContainerBuilder builder)
96+
RegisterGeneratedFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TDelegate>(this ContainerBuilder builder)
9797
where TDelegate : class
9898
{
9999
if (builder == null)

src/Autofac/ContainerBuilder.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ private void Build(IComponentRegistryBuilder componentRegistry, bool excludeDefa
244244
}
245245
}
246246

247+
[UnconditionalSuppressMessage(
248+
"AOT",
249+
"IL3050:RequiresDynamicCode",
250+
Justification = "The built-in KeyedServiceIndex<,> adapter is registered for every container. The IIndex<,> relationship is only ever constructed when a consumer actually resolves an IIndex<,>, so this default registration does not by itself force dynamic code; suppressing here avoids tainting the always-run Build() path. Consumers that resolve IIndex<,> over value-type keys take on the same dynamic-code requirement as any other open generic.")]
247251
private void RegisterDefaultAdapters(IComponentRegistryBuilder componentRegistry)
248252
{
249253
this.RegisterGeneric(typeof(KeyedServiceIndex<,>)).As(typeof(IIndex<,>)).InstancePerLifetimeScope();

src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ public static void InjectProperties(IComponentContext context, object instance,
109109
}
110110
}
111111

112+
[UnconditionalSuppressMessage(
113+
"Trimming",
114+
"IL2067:UnrecognizedReflectionPattern",
115+
Justification = "instanceType is the runtime type of an already-constructed instance being property-injected. Its public properties are preserved by the activation contract (ActivatorMemberTypes) for reflection-activated types; for externally-provided instances the caller that opted into property injection is responsible for preserving them.")]
112116
private static IEnumerable<PropertyInfo> GetInjectableProperties(Type instanceType)
113117
{
114118
foreach (var property in instanceType.GetRuntimeProperties())
@@ -169,6 +173,14 @@ private static bool IsUnsupportedPropertyType(Type propertyType)
169173
}
170174

171175
[SuppressMessage("S125", "S125", Justification = "Commented code explains the code generation output.")]
176+
[UnconditionalSuppressMessage(
177+
"AOT",
178+
"IL3050:RequiresDynamicCode",
179+
Justification = "Builds a strongly-typed property setter delegate via MakeGenericType/MakeGenericMethod over the property's declaring and value types. Reachable only when property injection is configured for a registration; the consumer that opted into property injection takes on the dynamic-code requirement for value-typed properties.")]
180+
[UnconditionalSuppressMessage(
181+
"Trimming",
182+
"IL2060:MakeGenericMethod",
183+
Justification = "The generic arguments are the property's declaring type and value type, both reachable from the property being injected. Preserving them is the responsibility of the consumer that opted into property injection.")]
172184
private static Action<object, object?> MakeFastPropertySetter(PropertyInfo propertyInfo)
173185
{
174186
// SetMethod will be non-null if we're trying to make a setter for it.

0 commit comments

Comments
 (0)