Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/aot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Native AOT Verification

# Repo-local, additive workflow (the shared org CI in autofac/.github builds and
# tests the solution). This job runs the VerifyAot target, which proves Autofac
# stays Native-AOT/trim clean in two complementary ways:
# 1. VerifyAotWarnings - builds a fixture that uses the [RequiresDynamicCode] /
# [RequiresUnreferencedCode] APIs and asserts those diagnostics still fire,
# guarding against silently losing an annotation.
# 2. The native publish - publishes the smoke-test app with PublishAot=true (the
# ILC trim/AOT analyzer fails on any IL2104/IL3053 since the project treats
# warnings as errors) and executes the native binary, which exits non-zero on
# any runtime failure of the AOT-safe resolve path (or if a dynamic-code
# scenario unexpectedly stops throwing).

on:
pull_request:
branches:
- develop
- main
push:
branches:
- develop
- main
- feature/*
tags:
- v[0-9]+.[0-9]+.[0-9]+

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify-aot:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

# Native AOT on Linux compiles and links native code, which requires a C
# toolchain and zlib development headers.
- name: Install Native AOT prerequisites
run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev

- name: Verify Native AOT compatibility
run: dotnet msbuild ./default.proj -t:VerifyAot -p:Configuration=Release
82 changes: 81 additions & 1 deletion default.proj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="Current">
<PropertyGroup>
<!-- Increment the overall semantic version here. -->
<Version>9.2.0</Version>
<Version>9.3.0</Version>
<SolutionName>Autofac</SolutionName>
<Configuration Condition="'$(Configuration)'==''">Release</Configuration>
<ArtifactDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts"))</ArtifactDirectory>
Expand Down Expand Up @@ -70,4 +70,84 @@
<MakeDir Directories="$(LogDirectory)" />
<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;" />
</Target>
<!--
Native AOT verification. Publishes the AOT smoke-test app with PublishAot=true
(which runs the ILC trim/AOT analyzer and would fail the build on any IL2104/
IL3053 warning, since the project treats warnings as errors) and then executes
the produced native binary, asserting a zero exit code. This proves both that
Autofac is statically AOT-clean AND that the AOT-safe resolve path works at
runtime. The project is intentionally excluded from Autofac.sln so the normal
Compile/Test targets do not require the native toolchain.
-->
<PropertyGroup>
<AotProjectDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),'test/Autofac.Test.Aot'))</AotProjectDirectory>
<AotPublishDirectory>$([System.IO.Path]::Combine($(ArtifactDirectory),'aot'))</AotPublishDirectory>
<AotExecutableName>Autofac.Test.Aot</AotExecutableName>
<AotExecutableName Condition="'$(OS)' == 'Windows_NT'">Autofac.Test.Aot.exe</AotExecutableName>
</PropertyGroup>
<Target Name="VerifyAot" DependsOnTargets="VerifyAotWarnings">
<Message Text="****************************************" Importance="high" />
<Message Text="Verifying Native AOT compatibility" Importance="high" />
<Message Text="****************************************" Importance="high" />
<Exec Command="dotnet publish &quot;$(AotProjectDirectory)&quot; -c $(Configuration) --output &quot;$(AotPublishDirectory)&quot;" />
<Exec Command="&quot;$([System.IO.Path]::Combine($(AotPublishDirectory),$(AotExecutableName)))&quot;" />
</Target>
<!--
AOT/trim warning verification. Builds the warning fixture (which calls the APIs
annotated [RequiresDynamicCode] / [RequiresUnreferencedCode]) and asserts the
expected analyzer diagnostics are emitted. This guards against silently LOSING an
annotation: if an attribute is dropped, the warning stops firing and this target
fails. Unlike VerifyAot proper, this needs no native toolchain - a plain build
surfaces the analyzer diagnostics - so it is cheap to run anywhere.

The check is PER CALL SITE, not just per diagnostic code: each ExpectedAotWarning
item names both the code (IL2026/IL3050) and a distinctive substring of the
annotated member's signature as it appears in the warning text. Multiple call
sites share a code (e.g. RegisterGeneric and RegisterGenericDecorator are both
IL3050), so asserting only "IL3050 appears somewhere" would not catch losing the
annotation on just one of them. Matching the member signature catches each one.

Keep the ExpectedAotWarning items in sync with the calls in
test/Autofac.Test.AotWarnings/Program.cs - one item per annotated call.
-->
<PropertyGroup>
<AotWarningsProjectDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),'test/Autofac.Test.AotWarnings'))</AotWarningsProjectDirectory>
</PropertyGroup>
<ItemGroup>
<!-- Identity = the IL code; Member = a substring uniquely identifying the API in the warning text. -->
<ExpectedAotWarning Include="IL3050" Member="RegisterGeneric(ContainerBuilder, Type)" />
<ExpectedAotWarning Include="IL3050" Member="RegisterGenericDecorator(ContainerBuilder, Type, Type" />
<ExpectedAotWarning Include="IL2026" Member="RegisterAssemblyTypes(ContainerBuilder, params Assembly" />
<ExpectedAotWarning Include="IL2026" Member="RegisterAssemblyModules(ContainerBuilder, params Assembly" />
</ItemGroup>
<Target Name="VerifyAotWarnings">
<Message Text="****************************************" Importance="high" />
<Message Text="Verifying AOT/trim warnings still fire" Importance="high" />
<Message Text="****************************************" Importance="high" />
<MakeDir Directories="$(LogDirectory)" />
<PropertyGroup>
<AotWarningsLog>$([System.IO.Path]::Combine($(LogDirectory),'aot-warnings-build.log'))</AotWarningsLog>
</PropertyGroup>
<!--
Build the fixture (full recompile so analyzer diagnostics are always emitted)
and redirect ALL output to a log file. This is deliberate: the fixture emits the
IL2026/IL3050 warnings on purpose, and if they reached the CI job's stdout the
.NET problem matcher would turn each one into a (misleading) PR annotation. By
sending the build output only to a file and reading it back here, the warnings
are verified without ever surfacing on the console. The log is written under the
artifacts directory and inspected below.
-->
<Exec Command="dotnet build &quot;$(AotWarningsProjectDirectory)&quot; -c $(Configuration) --no-incremental &gt; &quot;$(AotWarningsLog)&quot; 2&gt;&amp;1"
IgnoreExitCode="true" />
<ReadLinesFromFile File="$(AotWarningsLog)">
<Output TaskParameter="Lines" ItemName="AotWarningsBuildOutput" />
</ReadLinesFromFile>
<PropertyGroup>
<AotWarningsBuildText>@(AotWarningsBuildOutput, '%0a')</AotWarningsBuildText>
</PropertyGroup>
<!-- Each expected warning must appear with BOTH its code and the specific member signature. -->
<Error Condition="!($(AotWarningsBuildText.Contains('warning %(ExpectedAotWarning.Identity)')) and $(AotWarningsBuildText.Contains('%(ExpectedAotWarning.Member)')))"
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." />
<Message Text="All expected AOT/trim warnings were emitted (@(ExpectedAotWarning->'%(Identity): %(Member)', '; '))." Importance="high" />
</Target>
</Project>
14 changes: 14 additions & 0 deletions src/Autofac/Autofac.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<NoWarn>$(NoWarn);8600;8601;8602;8603;8604</NoWarn>
</PropertyGroup>
<!--
Advertise the assembly as AOT/trim-compatible on the modern TFMs (net7.0+);
netstandard targets cannot be AOT-analyzed. IsTargetFrameworkCompatible keeps
this robust as TFMs are added/removed. IsAotCompatible implicitly enables the
trim, AOT, and single-file analyzers, so combined with Release
TreatWarningsAsErrors this is a permanent in-build gate: any new unannotated
reflection/dynamic-code call site fails the build. APIs that are inherently
incompatible (open generics, assembly scanning, generated factories, strongly
typed metadata, etc.) are individually annotated [RequiresUnreferencedCode] /
[RequiresDynamicCode] so the warning surfaces at the consumer's call site.
-->
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\build\icon.png" Pack="true" PackagePath="\" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
Expand Down
3 changes: 2 additions & 1 deletion src/Autofac/Builder/ConcreteReflectionActivatorData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Autofac.Core;
using Autofac.Core.Activators.Reflection;
using Autofac.Util;

namespace Autofac.Builder;

Expand All @@ -15,7 +16,7 @@ public class ConcreteReflectionActivatorData : ReflectionActivatorData, IConcret
/// Initializes a new instance of the <see cref="ConcreteReflectionActivatorData"/> class.
/// </summary>
/// <param name="implementer">Type that will be activated.</param>
public ConcreteReflectionActivatorData(Type implementer)
public ConcreteReflectionActivatorData([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type implementer)
: base(implementer)
{
}
Expand Down
5 changes: 4 additions & 1 deletion src/Autofac/Builder/ReflectionActivatorData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Autofac.Core;
using Autofac.Core.Activators.Reflection;
using Autofac.Util;

namespace Autofac.Builder;

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

[DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)]
private Type _implementer = default!;
private IConstructorFinder _constructorFinder;
private IConstructorSelector _constructorSelector;
Expand All @@ -22,7 +24,7 @@ public class ReflectionActivatorData
/// Initializes a new instance of the <see cref="ReflectionActivatorData"/> class.
/// </summary>
/// <param name="implementer">Type that will be activated.</param>
public ReflectionActivatorData(Type implementer)
public ReflectionActivatorData([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type implementer)
{
ImplementationType = implementer;

Expand All @@ -33,6 +35,7 @@ public ReflectionActivatorData(Type implementer)
/// <summary>
/// Gets or sets the implementation type.
/// </summary>
[DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)]
public Type ImplementationType
{
get
Expand Down
4 changes: 2 additions & 2 deletions src/Autofac/Builder/RegistrationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static IRegistrationBuilder<object, SimpleActivatorData, SingleRegistrati
/// </summary>
/// <typeparam name="TImplementer">Implementation type to register.</typeparam>
/// <returns>A registration builder.</returns>
public static IRegistrationBuilder<TImplementer, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType<TImplementer>()
public static IRegistrationBuilder<TImplementer, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType<[DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] TImplementer>()
where TImplementer : notnull
{
// Open generics can't be generic type parameters so we don't have to check for that here.
Expand All @@ -79,7 +79,7 @@ public static IRegistrationBuilder<TImplementer, ConcreteReflectionActivatorData
/// </summary>
/// <param name="implementationType">Implementation type to register.</param>
/// <returns>A registration builder.</returns>
public static IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType(Type implementationType)
public static IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle> ForType([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type implementationType)
{
if (implementationType is null)
{
Expand Down
8 changes: 4 additions & 4 deletions src/Autofac/Builder/RegistrationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static class RegistrationExtensions
/// and this method is generally not required.</remarks>
[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.")]
public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
RegisterGeneratedFactory(this ContainerBuilder builder, Type delegateType)
RegisterGeneratedFactory(this ContainerBuilder builder, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type delegateType)
{
if (delegateType == null)
{
Expand All @@ -51,7 +51,7 @@ public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, Sing
/// this method is generally not required.</remarks>
[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.")]
public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
RegisterGeneratedFactory(this ContainerBuilder builder, Type delegateType, Service service)
RegisterGeneratedFactory(this ContainerBuilder builder, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type delegateType, Service service)
{
if (builder == null)
{
Expand All @@ -72,7 +72,7 @@ public static IRegistrationBuilder<Delegate, GeneratedFactoryActivatorData, Sing
/// and this method is generally not required.</remarks>
[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.")]
public static IRegistrationBuilder<TDelegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
RegisterGeneratedFactory<TDelegate>(this ContainerBuilder builder, Service service)
RegisterGeneratedFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TDelegate>(this ContainerBuilder builder, Service service)
where TDelegate : class
{
if (builder == null)
Expand All @@ -93,7 +93,7 @@ public static IRegistrationBuilder<TDelegate, GeneratedFactoryActivatorData, Sin
/// and this method is generally not required.</remarks>
[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.")]
public static IRegistrationBuilder<TDelegate, GeneratedFactoryActivatorData, SingleRegistrationStyle>
RegisterGeneratedFactory<TDelegate>(this ContainerBuilder builder)
RegisterGeneratedFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TDelegate>(this ContainerBuilder builder)
where TDelegate : class
{
if (builder == null)
Expand Down
4 changes: 4 additions & 0 deletions src/Autofac/ContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ private void Build(IComponentRegistryBuilder componentRegistry, bool excludeDefa
}
}

[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
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.")]
private void RegisterDefaultAdapters(IComponentRegistryBuilder componentRegistry)
{
this.RegisterGeneric(typeof(KeyedServiceIndex<,>)).As(typeof(IIndex<,>)).InstancePerLifetimeScope();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ public static void InjectProperties(IComponentContext context, object instance,
}
}

[UnconditionalSuppressMessage(
"Trimming",
"IL2067:UnrecognizedReflectionPattern",
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.")]
private static IEnumerable<PropertyInfo> GetInjectableProperties(Type instanceType)
{
foreach (var property in instanceType.GetRuntimeProperties())
Expand Down Expand Up @@ -169,6 +173,14 @@ private static bool IsUnsupportedPropertyType(Type propertyType)
}

[SuppressMessage("S125", "S125", Justification = "Commented code explains the code generation output.")]
[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
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.")]
[UnconditionalSuppressMessage(
"Trimming",
"IL2060:MakeGenericMethod",
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.")]
private static Action<object, object?> MakeFastPropertySetter(PropertyInfo propertyInfo)
{
// SetMethod will be non-null if we're trying to make a setter for it.
Expand Down
Loading
Loading