diff --git a/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs b/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs index d6862331ea..94b13875a5 100644 --- a/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs +++ b/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs @@ -171,7 +171,9 @@ public IScenarioWithEndpointBehavior Done(Func behaviors = []; int componentCount = 0; - readonly IServiceCollection services = new ServiceCollection(); + // The default service collection is not thread safe but the acceptance testing framework does concurrent starting of endpoints and service registration, so we need to use a thread safe collection here. + // In the future we probably want to change the framework to not allow concurrent modifications to the service collection, but for now this is a simpler change. + readonly IServiceCollection services = new ThreadSafeServiceCollection(); Task? doneTask; readonly TaskCompletionSource<(TContext scenarioContext, CancellationToken cancellationToken)> kickOffTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); Func? doneFunc; diff --git a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs index ed7a15de83..474a64a293 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs @@ -49,7 +49,8 @@ async Task PerformTestRun(CancellationToken cancellationToken) runResult.ActiveEndpoints = [.. endpoints.Select(r => r.Name)]; - runDescriptor.ServiceProvider = runDescriptor.Services.BuildServiceProvider(runDescriptor.Settings.Get()); + var services = runDescriptor.Services is ThreadSafeServiceCollection safe ? safe.Unwrap() : runDescriptor.Services; + runDescriptor.ServiceProvider = services.BuildServiceProvider(runDescriptor.Settings.Get()); await PerformScenarios(endpoints, cancellationToken).ConfigureAwait(false); diff --git a/src/NServiceBus.AcceptanceTesting/Support/ThreadSafeServiceCollection.cs b/src/NServiceBus.AcceptanceTesting/Support/ThreadSafeServiceCollection.cs new file mode 100644 index 0000000000..79a1ca6772 --- /dev/null +++ b/src/NServiceBus.AcceptanceTesting/Support/ThreadSafeServiceCollection.cs @@ -0,0 +1,97 @@ +namespace NServiceBus.AcceptanceTesting.Support; + +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +sealed class ThreadSafeServiceCollection : IServiceCollection +{ + public int Count + { + get + { + using var _ = gate.EnterScope(); + return inner.Count; + } + } + + public bool IsReadOnly => inner.IsReadOnly; + + public ServiceDescriptor this[int index] + { + get + { + using var _ = gate.EnterScope(); + return inner[index]; + } + set + { + using var _ = gate.EnterScope(); + inner[index] = value; + } + } + + public void Add(ServiceDescriptor item) + { + using var _ = gate.EnterScope(); + inner.Add(item); + } + + public void Clear() + { + using var _ = gate.EnterScope(); + inner.Clear(); + } + + public bool Contains(ServiceDescriptor item) + { + using var _ = gate.EnterScope(); + return inner.Contains(item); + } + + public void CopyTo(ServiceDescriptor[] array, int arrayIndex) + { + using var _ = gate.EnterScope(); + inner.CopyTo(array, arrayIndex); + } + + public bool Remove(ServiceDescriptor item) + { + using var _ = gate.EnterScope(); + return inner.Remove(item); + } + + public IEnumerator GetEnumerator() + { + using var _ = gate.EnterScope(); + IEnumerable snapshot = [.. inner]; + return snapshot.GetEnumerator(); + } + + public int IndexOf(ServiceDescriptor item) + { + using var _ = gate.EnterScope(); + return inner.IndexOf(item); + } + + public void Insert(int index, ServiceDescriptor item) + { + using var _ = gate.EnterScope(); + inner.Insert(index, item); + } + + public void RemoveAt(int index) + { + using var _ = gate.EnterScope(); + inner.RemoveAt(index); + } + + internal IServiceCollection Unwrap() => inner; + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + readonly ServiceCollection inner = []; + readonly Lock gate = new(); +} \ No newline at end of file