diff --git a/TUnit.Core/TestBuilderContext.cs b/TUnit.Core/TestBuilderContext.cs index 712f7303f4..fb802b2bcb 100644 --- a/TUnit.Core/TestBuilderContext.cs +++ b/TUnit.Core/TestBuilderContext.cs @@ -9,6 +9,7 @@ namespace TUnit.Core; public record TestBuilderContext { private static readonly AsyncLocal BuilderContexts = new(); + private string? _definitionId; /// /// Gets the current test builder context. @@ -19,17 +20,31 @@ public static TestBuilderContext? Current internal set => BuilderContexts.Value = value; } - public string DefinitionId { get; } = Guid.NewGuid().ToString(); + /// + /// Gets the unique definition ID for this context. Generated lazily on first access. + /// + public string DefinitionId => _definitionId ??= Guid.NewGuid().ToString(); [Obsolete("Use StateBag property instead.")] public ConcurrentDictionary ObjectBag => StateBag; + private ConcurrentDictionary? _stateBag; + private TestContextEvents? _events; + /// /// Gets the state bag for storing arbitrary data during test building. /// - public ConcurrentDictionary StateBag { get; set; } = new(); + public ConcurrentDictionary StateBag + { + get => _stateBag ??= new ConcurrentDictionary(); + set => _stateBag = value; + } - public TestContextEvents Events { get; set; } = new(); + public TestContextEvents Events + { + get => _events ??= new TestContextEvents(); + set => _events = value; + } public IDataSourceAttribute? DataSourceAttribute { get; set; } diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index 5ebc233899..8921b35590 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -162,11 +162,10 @@ public async Task> BuildTestsFromMetadataAsy } // Create a single context accessor that we'll reuse, updating its Current property for each test + // StateBag and Events are lazy-initialized, so we don't need to pre-allocate them var testBuilderContext = new TestBuilderContext { TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), - StateBag = new ConcurrentDictionary(), InitializedAttributes = attributes // Store the initialized attributes }; @@ -309,11 +308,10 @@ await _objectLifecycleService.RegisterObjectAsync( for (var i = 0; i < repeatCount + 1; i++) { // Update context BEFORE calling data factories so they track objects in the right context + // StateBag and Events are lazy-initialized for performance contextAccessor.Current = new TestBuilderContext { TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), - StateBag = new ConcurrentDictionary(), DataSourceAttribute = methodDataSource, InitializedAttributes = testBuilderContext.InitializedAttributes, // Preserve attributes from parent context ClassConstructor = testBuilderContext.ClassConstructor // Preserve ClassConstructor for instance creation @@ -443,10 +441,10 @@ await _objectLifecycleService.RegisterObjectAsync( Metadata = finalMetadata }; + // Events is lazy-initialized; explicitly share StateBag from per-iteration context var testSpecificContext = new TestBuilderContext { TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), StateBag = contextAccessor.Current.StateBag, ClassConstructor = testBuilderContext.ClassConstructor, DataSourceAttribute = contextAccessor.Current.DataSourceAttribute, @@ -508,11 +506,10 @@ await _objectLifecycleService.RegisterObjectAsync( ResolvedMethodGenericArguments = Type.EmptyTypes }; + // StateBag and Events are lazy-initialized var testSpecificContext = new TestBuilderContext { TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), - StateBag = new ConcurrentDictionary(), ClassConstructor = testBuilderContext.ClassConstructor, DataSourceAttribute = methodDataSource, InitializedAttributes = attributes @@ -568,11 +565,10 @@ await _objectLifecycleService.RegisterObjectAsync( ResolvedMethodGenericArguments = Type.EmptyTypes }; + // StateBag and Events are lazy-initialized var testSpecificContext = new TestBuilderContext { TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), - StateBag = new ConcurrentDictionary(), ClassConstructor = testBuilderContext.ClassConstructor, DataSourceAttribute = classDataSource, InitializedAttributes = attributes @@ -1428,11 +1424,10 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( var attributes = await InitializeAttributesAsync(metadata.AttributeFactory.Invoke()); // Create base context with ClassConstructor if present + // StateBag and Events are lazy-initialized for performance var baseContext = new TestBuilderContext { TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), - StateBag = new ConcurrentDictionary(), InitializedAttributes = attributes // Store the initialized attributes }; @@ -1732,10 +1727,10 @@ private Task CreateInstanceForMethodDataSources( Metadata = finalMetadata }; + // Events is lazy-initialized; explicitly share StateBag from per-iteration context var testSpecificContext = new TestBuilderContext { TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), StateBag = contextAccessor.Current.StateBag, ClassConstructor = contextAccessor.Current.ClassConstructor, DataSourceAttribute = contextAccessor.Current.DataSourceAttribute,