Skip to content

Commit 07c3c1e

Browse files
authored
feat: enhance data source initialization and property injection handling (#3959)
* feat: enhance data source initialization and property injection handling * feat: implement initialization order tracking for async components
1 parent 9a86180 commit 07c3c1e

7 files changed

Lines changed: 365 additions & 251 deletions

File tree

Lines changed: 16 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
using System.Collections.Concurrent;
2-
using System.Diagnostics.CodeAnalysis;
1+
using System.Diagnostics.CodeAnalysis;
32
using System.Reflection;
43
using System.Runtime.ExceptionServices;
54
using TUnit.Core.Data;
6-
using TUnit.Core.PropertyInjection;
75

86
namespace TUnit.Core;
97

@@ -45,11 +43,11 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys)
4543
{
4644
return sharedType switch
4745
{
48-
SharedType.None => Create<T>(dataGeneratorMetadata),
49-
SharedType.PerTestSession => (T) TestDataContainer.GetGlobalInstance(typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
50-
SharedType.PerClass => (T) TestDataContainer.GetInstanceForClass(testClassType, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
51-
SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
52-
SharedType.PerAssembly => (T) TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!,
46+
SharedType.None => Create<T>(),
47+
SharedType.PerTestSession => (T) TestDataContainer.GetGlobalInstance(typeof(T), _ => Create(typeof(T)))!,
48+
SharedType.PerClass => (T) TestDataContainer.GetInstanceForClass(testClassType, typeof(T), _ => Create(typeof(T)))!,
49+
SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => Create(typeof(T)))!,
50+
SharedType.PerAssembly => (T) TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, typeof(T), _ => Create(typeof(T)))!,
5351
_ => throw new ArgumentOutOfRangeException()
5452
};
5553
}
@@ -58,43 +56,27 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys)
5856
{
5957
return sharedType switch
6058
{
61-
SharedType.None => Create(type, dataGeneratorMetadata),
62-
SharedType.PerTestSession => TestDataContainer.GetGlobalInstance(type, _ => Create(type, dataGeneratorMetadata)),
63-
SharedType.PerClass => TestDataContainer.GetInstanceForClass(testClassType, type, _ => Create(type, dataGeneratorMetadata)),
64-
SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => Create(type, dataGeneratorMetadata)),
65-
SharedType.PerAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, type, _ => Create(type, dataGeneratorMetadata)),
59+
SharedType.None => Create(type),
60+
SharedType.PerTestSession => TestDataContainer.GetGlobalInstance(type, _ => Create(type)),
61+
SharedType.PerClass => TestDataContainer.GetInstanceForClass(testClassType, type, _ => Create(type)),
62+
SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => Create(type)),
63+
SharedType.PerAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, type, _ => Create(type)),
6664
_ => throw new ArgumentOutOfRangeException()
6765
};
6866
}
6967

7068
[return: NotNull]
71-
private static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>(DataGeneratorMetadata dataGeneratorMetadata)
69+
private static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>()
7270
{
73-
return ((T) Create(typeof(T), dataGeneratorMetadata))!;
71+
return ((T) Create(typeof(T)))!;
7472
}
7573

76-
private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata)
74+
private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type)
7775
{
78-
return Create(type, dataGeneratorMetadata, recursionDepth: 0);
79-
}
80-
81-
private const int MaxRecursionDepth = 10;
82-
83-
private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth)
84-
{
85-
if (recursionDepth >= MaxRecursionDepth)
86-
{
87-
throw new InvalidOperationException($"Maximum recursion depth ({MaxRecursionDepth}) exceeded when creating nested ClassDataSource dependencies. This may indicate a circular dependency.");
88-
}
89-
9076
try
9177
{
92-
var instance = Activator.CreateInstance(type)!;
93-
94-
// Inject properties into the created instance
95-
InjectPropertiesSync(instance, type, dataGeneratorMetadata, recursionDepth);
96-
97-
return instance;
78+
// Just create the instance - initialization happens in the Engine
79+
return Activator.CreateInstance(type)!;
9880
}
9981
catch (TargetInvocationException targetInvocationException)
10082
{
@@ -106,197 +88,4 @@ private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemb
10688
throw;
10789
}
10890
}
109-
110-
/// <summary>
111-
/// Injects properties into an instance synchronously.
112-
/// Used when creating instances via ClassDataSource for nested data source dependencies.
113-
/// </summary>
114-
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Type is already annotated with DynamicallyAccessedMembers")]
115-
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling", Justification = "Fallback to reflection mode when source-gen not available")]
116-
private static void InjectPropertiesSync(
117-
object instance,
118-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type,
119-
DataGeneratorMetadata dataGeneratorMetadata,
120-
int recursionDepth)
121-
{
122-
// Get the injection plan for this type
123-
var plan = PropertyInjectionPlanBuilder.Build(type);
124-
if (!plan.HasProperties)
125-
{
126-
return;
127-
}
128-
129-
// Handle source-generated properties
130-
foreach (var metadata in plan.SourceGeneratedProperties)
131-
{
132-
var dataSource = metadata.CreateDataSource();
133-
var propertyMetadata = CreatePropertyMetadata(type, metadata.PropertyName, metadata.PropertyType);
134-
135-
var propertyDataGeneratorMetadata = DataGeneratorMetadataCreator.CreateForPropertyInjection(
136-
propertyMetadata,
137-
dataGeneratorMetadata.TestInformation,
138-
dataSource,
139-
testContext: null,
140-
testClassInstance: instance,
141-
events: new TestContextEvents(),
142-
objectBag: new ConcurrentDictionary<string, object?>());
143-
144-
var value = ResolveDataSourceValueSync(dataSource, propertyDataGeneratorMetadata, recursionDepth + 1);
145-
if (value != null)
146-
{
147-
metadata.SetProperty(instance, value);
148-
}
149-
}
150-
151-
// Handle reflection-mode properties
152-
foreach (var (property, dataSource) in plan.ReflectionProperties)
153-
{
154-
var propertyMetadata = CreatePropertyMetadataFromPropertyInfo(property);
155-
156-
var propertyDataGeneratorMetadata = DataGeneratorMetadataCreator.CreateForPropertyInjection(
157-
propertyMetadata,
158-
dataGeneratorMetadata.TestInformation,
159-
dataSource,
160-
testContext: null,
161-
testClassInstance: instance,
162-
events: new TestContextEvents(),
163-
objectBag: new ConcurrentDictionary<string, object?>());
164-
165-
var value = ResolveDataSourceValueSync(dataSource, propertyDataGeneratorMetadata, recursionDepth + 1);
166-
if (value != null)
167-
{
168-
SetPropertyValue(property, instance, value);
169-
}
170-
}
171-
}
172-
173-
[UnconditionalSuppressMessage("Trimming", "IL2067:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "Type is already annotated in caller")]
174-
[UnconditionalSuppressMessage("Trimming", "IL2070:Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute'", Justification = "Type is already annotated in caller")]
175-
private static PropertyMetadata CreatePropertyMetadata(
176-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type containingType,
177-
string propertyName,
178-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] Type propertyType)
179-
{
180-
return new PropertyMetadata
181-
{
182-
Name = propertyName,
183-
Type = propertyType,
184-
IsStatic = false,
185-
ClassMetadata = GetClassMetadataForType(containingType),
186-
ContainingTypeMetadata = GetClassMetadataForType(containingType),
187-
ReflectionInfo = containingType.GetProperty(propertyName)!,
188-
Getter = parent => containingType.GetProperty(propertyName)?.GetValue(parent)
189-
};
190-
}
191-
192-
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "PropertyInfo already obtained")]
193-
[UnconditionalSuppressMessage("Trimming", "IL2072:'value' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "PropertyInfo already obtained with type annotations")]
194-
private static PropertyMetadata CreatePropertyMetadataFromPropertyInfo(PropertyInfo property)
195-
{
196-
var containingType = property.DeclaringType!;
197-
return new PropertyMetadata
198-
{
199-
Name = property.Name,
200-
Type = property.PropertyType,
201-
IsStatic = property.GetMethod?.IsStatic ?? false,
202-
ClassMetadata = GetClassMetadataForType(containingType),
203-
ContainingTypeMetadata = GetClassMetadataForType(containingType),
204-
ReflectionInfo = property,
205-
Getter = parent => property.GetValue(parent)
206-
};
207-
}
208-
209-
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Type is already annotated")]
210-
[UnconditionalSuppressMessage("Trimming", "IL2070:Target method return value does not satisfy 'DynamicallyAccessedMembersAttribute'", Justification = "Type is already annotated")]
211-
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "Type is already annotated")]
212-
[UnconditionalSuppressMessage("Trimming", "IL2067:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "Type is already annotated")]
213-
private static ClassMetadata GetClassMetadataForType(Type type)
214-
{
215-
return ClassMetadata.GetOrAdd(type.FullName ?? type.Name, () =>
216-
{
217-
var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
218-
var constructor = constructors.FirstOrDefault();
219-
220-
var constructorParameters = constructor?.GetParameters().Select((p, i) => new ParameterMetadata(p.ParameterType)
221-
{
222-
Name = p.Name ?? $"param{i}",
223-
TypeInfo = new ConcreteType(p.ParameterType),
224-
ReflectionInfo = p
225-
}).ToArray() ?? [];
226-
227-
return new ClassMetadata
228-
{
229-
Type = type,
230-
TypeInfo = new ConcreteType(type),
231-
Name = type.Name,
232-
Namespace = type.Namespace ?? string.Empty,
233-
Assembly = AssemblyMetadata.GetOrAdd(type.Assembly.GetName().Name ?? type.Assembly.GetName().FullName ?? "Unknown", () => new AssemblyMetadata
234-
{
235-
Name = type.Assembly.GetName().Name ?? type.Assembly.GetName().FullName ?? "Unknown"
236-
}),
237-
Properties = [],
238-
Parameters = constructorParameters,
239-
Parent = type.DeclaringType != null ? GetClassMetadataForType(type.DeclaringType) : null
240-
};
241-
});
242-
}
243-
244-
/// <summary>
245-
/// Resolves a data source value synchronously by running the async enumerable.
246-
/// </summary>
247-
private static object? ResolveDataSourceValueSync(IDataSourceAttribute dataSource, DataGeneratorMetadata metadata, int recursionDepth)
248-
{
249-
var dataRows = dataSource.GetDataRowsAsync(metadata);
250-
251-
// Get the first value from the async enumerable synchronously
252-
var enumerator = dataRows.GetAsyncEnumerator();
253-
try
254-
{
255-
if (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult())
256-
{
257-
var factory = enumerator.Current;
258-
var args = factory().GetAwaiter().GetResult();
259-
if (args is { Length: > 0 })
260-
{
261-
var value = args[0];
262-
263-
// Initialize the value if it implements IAsyncInitializer
264-
ObjectInitializer.InitializeAsync(value).AsTask().GetAwaiter().GetResult();
265-
266-
return value;
267-
}
268-
}
269-
}
270-
finally
271-
{
272-
enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult();
273-
}
274-
275-
return null;
276-
}
277-
278-
/// <summary>
279-
/// Sets a property value, handling init-only properties via backing field if necessary.
280-
/// </summary>
281-
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "PropertyInfo already obtained")]
282-
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method", Justification = "PropertyInfo already obtained with type annotations")]
283-
private static void SetPropertyValue(PropertyInfo property, object instance, object? value)
284-
{
285-
if (property.CanWrite && property.SetMethod != null)
286-
{
287-
property.SetValue(instance, value);
288-
return;
289-
}
290-
291-
// Try to set via backing field for init-only properties
292-
var backingFieldName = $"<{property.Name}>k__BackingField";
293-
var backingField = property.DeclaringType?.GetField(
294-
backingFieldName,
295-
BindingFlags.Instance | BindingFlags.NonPublic);
296-
297-
if (backingField != null)
298-
{
299-
backingField.SetValue(instance, value);
300-
}
301-
}
30291
}

TUnit.Engine/Framework/TUnitServiceProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public TUnitServiceProvider(IExtension extension,
188188
// Create test finder service after discovery service so it can use its cache
189189
TestFinder = Register<ITestFinder>(new TestFinder(DiscoveryService));
190190

191-
var testInitializer = new TestInitializer(EventReceiverOrchestrator, PropertyInjectionService, objectTracker);
191+
var testInitializer = new TestInitializer(EventReceiverOrchestrator, PropertyInjectionService, DataSourceInitializer, objectTracker);
192192

193193
// Create the new TestCoordinator that orchestrates the granular services
194194
var testCoordinator = Register<ITestCoordinator>(

TUnit.Engine/Services/DataSourceInitializer.cs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,21 @@ private void CollectNestedObjects(
162162
{
163163
var plan = PropertyInjectionCache.GetOrCreatePlan(obj.GetType());
164164

165-
if (!SourceRegistrar.IsEnabled)
165+
// Use whichever properties are available in the plan
166+
// For closed generic types, source-gen may not have registered them, so use reflection fallback
167+
if (plan.SourceGeneratedProperties.Length > 0)
166168
{
167-
// Reflection mode
168-
foreach (var prop in plan.ReflectionProperties)
169+
// Source-generated mode
170+
foreach (var metadata in plan.SourceGeneratedProperties)
169171
{
170-
var value = prop.Property.GetValue(obj);
172+
var property = metadata.ContainingType.GetProperty(metadata.PropertyName);
173+
174+
if (property == null || !property.CanRead)
175+
{
176+
continue;
177+
}
178+
179+
var value = property.GetValue(obj);
171180

172181
if (value == null || !visitedObjects.Add(value))
173182
{
@@ -192,19 +201,12 @@ private void CollectNestedObjects(
192201
}
193202
}
194203
}
195-
else
204+
else if (plan.ReflectionProperties.Length > 0)
196205
{
197-
// Source-generated mode
198-
foreach (var metadata in plan.SourceGeneratedProperties)
206+
// Reflection mode fallback
207+
foreach (var prop in plan.ReflectionProperties)
199208
{
200-
var property = metadata.ContainingType.GetProperty(metadata.PropertyName);
201-
202-
if (property == null || !property.CanRead)
203-
{
204-
continue;
205-
}
206-
207-
var value = property.GetValue(obj);
209+
var value = prop.Property.GetValue(obj);
208210

209211
if (value == null || !visitedObjects.Add(value))
210212
{

TUnit.Engine/Services/PropertyInitializationOrchestrator.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,14 @@ public async Task InitializeObjectWithPropertiesAsync(
196196
return;
197197
}
198198

199-
// Initialize properties based on the mode (source-generated or reflection)
200-
if (SourceRegistrar.IsEnabled)
199+
// Initialize properties based on what's available in the plan
200+
// For closed generic types, source-gen may not have registered them, so use reflection fallback
201+
if (plan.SourceGeneratedProperties.Length > 0)
201202
{
202203
await InitializeSourceGeneratedPropertiesAsync(
203204
instance, plan.SourceGeneratedProperties, objectBag, methodMetadata, events, visitedObjects);
204205
}
205-
else
206+
else if (plan.ReflectionProperties.Length > 0)
206207
{
207208
await InitializeReflectionPropertiesAsync(
208209
instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects);

TUnit.Engine/Services/PropertyInjectionService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ private async Task RecurseIntoNestedPropertiesAsync(
162162
return;
163163
}
164164

165-
if (SourceRegistrar.IsEnabled)
165+
// Use whichever properties are available in the plan
166+
// For closed generic types, source-gen may not have registered them, so use reflection fallback
167+
if (plan.SourceGeneratedProperties.Length > 0)
166168
{
167169
foreach (var metadata in plan.SourceGeneratedProperties)
168170
{
@@ -184,7 +186,7 @@ private async Task RecurseIntoNestedPropertiesAsync(
184186
}
185187
}
186188
}
187-
else
189+
else if (plan.ReflectionProperties.Length > 0)
188190
{
189191
foreach (var (property, _) in plan.ReflectionProperties)
190192
{

0 commit comments

Comments
 (0)