diff --git a/.gitignore b/.gitignore
index 0e3c8cfe18..149ec04978 100644
--- a/.gitignore
+++ b/.gitignore
@@ -419,8 +419,8 @@ TUnit.TestProject/TestSession*.txt
.mcp.json
requirements
-TESTPROJECT_AOT
-TESTPROJECT_SINGLEFILE
+TESTPROJECT_AOT*
+TESTPROJECT_SINGLEFILE*
nul
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 24ea46faf3..11f44f281e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -83,9 +83,9 @@
-
-
-
+
+
+
diff --git a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs
index 29e833e178..9c3c88b637 100644
--- a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs
+++ b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs
@@ -60,6 +60,29 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}
GenerateIndividualPropertyInjectionSource(context, classData);
});
+
+ // Third pipeline: Generate InitializerPropertyRegistry metadata for IAsyncInitializer types
+ // that have properties returning other IAsyncInitializer types.
+ // This enables AOT-compatible nested initializer discovery.
+ var asyncInitializerTypes = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (node, _) => node is TypeDeclarationSyntax,
+ transform: (ctx, _) => GetAsyncInitializerWithInitializerProperties(ctx))
+ .Where(x => x != null)
+ .Select((x, _) => x!)
+ .Collect()
+ .SelectMany((types, _) => types.DistinctBy(t => t.TypeSymbol, SymbolEqualityComparer.Default))
+ .Combine(enabledProvider);
+
+ context.RegisterSourceOutput(asyncInitializerTypes, (ctx, data) =>
+ {
+ var (typeInfo, isEnabled) = data;
+ if (!isEnabled)
+ {
+ return;
+ }
+ GenerateInitializerPropertySource(ctx, typeInfo);
+ });
}
private static bool IsClassWithDataSourceProperties(SyntaxNode node)
@@ -635,6 +658,130 @@ private static string GetNonNullableTypeString(ITypeSymbol typeSymbol)
// Alias for consistency
private static string GetNonNullableTypeName(ITypeSymbol typeSymbol) => GetNonNullableTypeString(typeSymbol);
+
+ #region IAsyncInitializer Property Discovery (for nested initializer discovery)
+
+ ///
+ /// Finds types that implement IAsyncInitializer and have properties that return other IAsyncInitializer types.
+ /// Used for AOT-compatible nested initializer discovery during object graph traversal.
+ ///
+ private static AsyncInitializerTypeInfo? GetAsyncInitializerWithInitializerProperties(GeneratorSyntaxContext context)
+ {
+ var typeDecl = (TypeDeclarationSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+
+ if (semanticModel.GetDeclaredSymbol(typeDecl) is not INamedTypeSymbol typeSymbol)
+ {
+ return null;
+ }
+
+ // Skip non-public/internal types
+ if (!IsPubliclyAccessible(typeSymbol))
+ {
+ return null;
+ }
+
+ // Skip open generic types
+ if (typeSymbol.IsUnboundGenericType || typeSymbol.TypeParameters.Length > 0)
+ {
+ return null;
+ }
+
+ var asyncInitializerInterface = semanticModel.Compilation.GetTypeByMetadataName("TUnit.Core.Interfaces.IAsyncInitializer");
+ if (asyncInitializerInterface == null)
+ {
+ return null;
+ }
+
+ // Check if this type implements IAsyncInitializer
+ if (!typeSymbol.AllInterfaces.Contains(asyncInitializerInterface, SymbolEqualityComparer.Default))
+ {
+ return null;
+ }
+
+ // Find properties that return IAsyncInitializer types
+ var initializerProperties = new List();
+
+ var allProperties = typeSymbol.GetMembers()
+ .OfType()
+ .Where(p => p.GetMethod != null && !p.IsStatic && !p.IsIndexer)
+ .ToList();
+
+ foreach (var property in allProperties)
+ {
+ // Check if the property type implements IAsyncInitializer
+ if (property.Type is INamedTypeSymbol propertyType)
+ {
+ if (propertyType.AllInterfaces.Contains(asyncInitializerInterface, SymbolEqualityComparer.Default) ||
+ SymbolEqualityComparer.Default.Equals(propertyType, asyncInitializerInterface))
+ {
+ initializerProperties.Add(new InitializerPropertyMetadata
+ {
+ Property = property
+ });
+ }
+ }
+ }
+
+ if (initializerProperties.Count == 0)
+ {
+ return null;
+ }
+
+ return new AsyncInitializerTypeInfo
+ {
+ TypeSymbol = typeSymbol,
+ Properties = initializerProperties.ToImmutableArray()
+ };
+ }
+
+ ///
+ /// Generates source code that registers IAsyncInitializer property metadata with InitializerPropertyRegistry.
+ ///
+ private static void GenerateInitializerPropertySource(SourceProductionContext context, AsyncInitializerTypeInfo typeInfo)
+ {
+ var typeSymbol = typeInfo.TypeSymbol;
+ var safeName = GetSafeClassName(typeSymbol);
+ var fileName = $"{safeName}_InitializerProperties.g.cs";
+
+ var sourceBuilder = new StringBuilder();
+
+ sourceBuilder.AppendLine("using System;");
+ sourceBuilder.AppendLine("using TUnit.Core.Discovery;");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine("namespace TUnit.Generated;");
+ sourceBuilder.AppendLine();
+
+ // Generate module initializer
+ sourceBuilder.AppendLine($"internal static class {safeName}_InitializerPropertiesInitializer");
+ sourceBuilder.AppendLine("{");
+ sourceBuilder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]");
+ sourceBuilder.AppendLine(" public static void Initialize()");
+ sourceBuilder.AppendLine(" {");
+ sourceBuilder.AppendLine($" InitializerPropertyRegistry.Register(typeof({typeSymbol.GloballyQualified()}), new InitializerPropertyInfo[]");
+ sourceBuilder.AppendLine(" {");
+
+ foreach (var propInfo in typeInfo.Properties)
+ {
+ var property = propInfo.Property;
+ var propertyTypeName = property.Type.GloballyQualified();
+
+ sourceBuilder.AppendLine(" new InitializerPropertyInfo");
+ sourceBuilder.AppendLine(" {");
+ sourceBuilder.AppendLine($" PropertyName = \"{property.Name}\",");
+ sourceBuilder.AppendLine($" PropertyType = typeof({propertyTypeName}),");
+ sourceBuilder.AppendLine($" GetValue = static obj => (({typeSymbol.GloballyQualified()})obj).{property.Name}");
+ sourceBuilder.AppendLine(" },");
+ }
+
+ sourceBuilder.AppendLine(" });");
+ sourceBuilder.AppendLine(" }");
+ sourceBuilder.AppendLine("}");
+
+ context.AddSource(fileName, sourceBuilder.ToString());
+ }
+
+ #endregion
}
internal sealed class ClassWithDataSourceProperties
@@ -665,3 +812,21 @@ public int GetHashCode(ClassWithDataSourceProperties obj)
return SymbolEqualityComparer.Default.GetHashCode(obj.ClassSymbol);
}
}
+
+///
+/// Model for types that implement IAsyncInitializer and have properties returning IAsyncInitializer.
+/// Used for generating AOT-compatible nested initializer discovery metadata.
+///
+internal sealed class AsyncInitializerTypeInfo
+{
+ public required INamedTypeSymbol TypeSymbol { get; init; }
+ public required ImmutableArray Properties { get; init; }
+}
+
+///
+/// Metadata about a property that returns an IAsyncInitializer type.
+///
+internal sealed class InitializerPropertyMetadata
+{
+ public required IPropertySymbol Property { get; init; }
+}
diff --git a/TUnit.Core/Discovery/InitializerPropertyRegistry.cs b/TUnit.Core/Discovery/InitializerPropertyRegistry.cs
new file mode 100644
index 0000000000..fe7269a3db
--- /dev/null
+++ b/TUnit.Core/Discovery/InitializerPropertyRegistry.cs
@@ -0,0 +1,60 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using TUnit.Core.Interfaces;
+
+namespace TUnit.Core.Discovery;
+
+///
+/// Registry for IAsyncInitializer property metadata generated at compile time.
+/// Used for AOT-compatible nested initializer discovery.
+///
+public static class InitializerPropertyRegistry
+{
+ private static readonly ConcurrentDictionary Registry = new();
+
+ ///
+ /// Registers property metadata for a type. Called by generated code.
+ ///
+ public static void Register(Type type, InitializerPropertyInfo[] properties)
+ {
+ Registry[type] = properties;
+ }
+
+ ///
+ /// Gets property metadata for a type, or null if not registered.
+ ///
+ public static InitializerPropertyInfo[]? GetProperties(Type type)
+ {
+ return Registry.TryGetValue(type, out var properties) ? properties : null;
+ }
+
+ ///
+ /// Checks if a type has registered property metadata.
+ ///
+ public static bool HasRegistration(Type type)
+ {
+ return Registry.ContainsKey(type);
+ }
+}
+
+///
+/// Metadata about a property that returns an IAsyncInitializer.
+///
+public sealed class InitializerPropertyInfo
+{
+ ///
+ /// The name of the property.
+ ///
+ public required string PropertyName { get; init; }
+
+ ///
+ /// The property type.
+ ///
+ public required Type PropertyType { get; init; }
+
+ ///
+ /// Delegate to get the property value from an instance.
+ ///
+ public required Func