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
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ public override void ConfigureHost(Test hostTest)
.UseSetting(HostDefaults.ApplicationKey, hostTest.CallerType.Assembly.GetName().Name);
});

#if NET9_0_OR_GREATER
hb.UseDefaultServiceProvider(o =>
{
o.ValidateOnBuild = true;
o.ValidateScopes = true;
});
#endif

ConfigureHostCallback(hb);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Codebelt.Extensions.Xunit.Hosting.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;

namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Http
{
Expand All @@ -13,17 +14,21 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Http
/// <seealso cref="IHttpContextAccessor" />
public class FakeHttpContextAccessor : IHttpContextAccessor
{

/// <summary>
/// Initializes a new instance of the <see cref="FakeHttpContextAccessor"/> class.
/// Initializes a new instance of the <see cref="FakeHttpContextAccessor" /> class.
/// </summary>
public FakeHttpContextAccessor()
/// <param name="factory">An optional <see cref="IServiceScopeFactory"/> for resolving services.</param>
public FakeHttpContextAccessor(IServiceScopeFactory factory = null)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update XML documentation to reflect constructor changes.

The constructor now accepts an optional IServiceScopeFactory, but the XML documentation has not been updated to describe this parameter. Please update the comments to maintain accurate documentation.

Apply this diff to update the XML documentation:

 /// <summary>
-/// Initializes a new instance of the <see cref="FakeHttpContextAccessor"/> class.
+/// Initializes a new instance of the <see cref="FakeHttpContextAccessor"/> class with an optional <see cref="IServiceScopeFactory"/>.
 /// </summary>
+/// <param name="factory">An optional service scope factory for resolving services.</param>
 public FakeHttpContextAccessor(IServiceScopeFactory factory = null)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public FakeHttpContextAccessor(IServiceScopeFactory factory = null)
/// <summary>
/// Initializes a new instance of the <see cref="FakeHttpContextAccessor"/> class with an optional <see cref="IServiceScopeFactory"/>.
/// </summary>
/// <param name="factory">An optional service scope factory for resolving services.</param>
public FakeHttpContextAccessor(IServiceScopeFactory factory = null)

{
var context = new DefaultHttpContext();
var fc = new FeatureCollection();
fc.Set<IServiceProvidersFeature>(new RequestServicesFeature(context, factory));
fc.Set<IHttpResponseFeature>(new FakeHttpResponseFeature());
fc.Set<IHttpRequestFeature>(new FakeHttpRequestFeature());
fc.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(MakeGreeting("Hello awesome developers!")));
HttpContext = new DefaultHttpContext(fc);
context.Uninitialize();
context.Initialize(fc);
HttpContext = context;
}

private Stream MakeGreeting(string greeting)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ public static class ServiceCollectionExtensions
/// Adds a unit test optimized implementation for the <see cref="IHttpContextAccessor"/> service.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to extend.</param>
/// <param name="lifetime">The lifetime of the service.</param>
/// <param name="lifetime">The lifetime of the service. Default is <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>A reference to <paramref name="services"/> after the operation has completed.</returns>
public static IServiceCollection AddFakeHttpContextAccessor(this IServiceCollection services, ServiceLifetime lifetime)
public static IServiceCollection AddFakeHttpContextAccessor(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
switch (lifetime)
{
Expand All @@ -39,8 +39,7 @@ public static IServiceCollection AddFakeHttpContextAccessor(this IServiceCollect

private static IHttpContextAccessor FakeHttpContextAccessorFactory(IServiceProvider provider)
{
var contextAccessor = new FakeHttpContextAccessor { HttpContext = { RequestServices = provider } };
return contextAccessor;
return new FakeHttpContextAccessor(provider.GetRequiredService<IServiceScopeFactory>());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public override void ConfigureServices(IServiceCollection services)
hbc.HostingEnvironment = HostingEnvironment;
return hbc;
}), services);
services.AddFakeHttpContextAccessor(ServiceLifetime.Singleton);
services.AddFakeHttpContextAccessor();
}
}
}
32 changes: 24 additions & 8 deletions src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand All @@ -12,6 +13,8 @@ namespace Codebelt.Extensions.Xunit.Hosting
/// <seealso cref="IHostFixture" />
public class HostFixture : IDisposable, IHostFixture
{
private readonly object _lock = new();

/// <summary>
/// Initializes a new instance of the <see cref="HostFixture"/> class.
/// </summary>
Expand Down Expand Up @@ -54,13 +57,22 @@ public virtual void ConfigureHost(Test hostTest)
ConfigureServicesCallback(services);
});

ConfigureHostCallback(hb);

#if NET9_0_OR_GREATER
hb.UseDefaultServiceProvider(o => o.ValidateScopes = false); // this is by intent
hb.UseDefaultServiceProvider(o =>
{
o.ValidateOnBuild = true;
o.ValidateScopes = true;
});
#endif

Host = hb.Build();
ConfigureHostCallback(hb);

var host = hb.Build();
Task.Run(() => host.StartAsync().ConfigureAwait(false))
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
Host = host;
Comment on lines +70 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Simplify host startup to avoid unnecessary Task.Run and potential deadlocks

The current implementation wraps host.StartAsync() inside a Task.Run and then synchronously waits for it using .GetAwaiter().GetResult(). This pattern can introduce unnecessary complexity and potential deadlocks. Since StartAsync() returns a Task, you can synchronously wait on it directly.

Apply this diff to simplify the host startup:

 var host = hb.Build();
-Task.Run(() => host.StartAsync().ConfigureAwait(false))
-    .ConfigureAwait(false)
-    .GetAwaiter()
-    .GetResult();
+host.StartAsync().GetAwaiter().GetResult();
 Host = host;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var host = hb.Build();
Task.Run(() => host.StartAsync().ConfigureAwait(false))
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
Host = host;
var host = hb.Build();
host.StartAsync().GetAwaiter().GetResult();
Host = host;

}

/// <summary>
Expand Down Expand Up @@ -185,12 +197,16 @@ public void Dispose()
protected void Dispose(bool disposing)
{
if (Disposed) { return; }
if (disposing)
lock (_lock)
{
OnDisposeManagedResources();
if (Disposed) { return; }
if (disposing)
{
OnDisposeManagedResources();
}
OnDisposeUnmanagedResources();
Disposed = true;
}
OnDisposeUnmanagedResources();
Disposed = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
using System.Linq;
using System.Threading.Tasks;
using Codebelt.Extensions.Xunit.Hosting.AspNetCore.Assets;
using Codebelt.Extensions.Xunit.Hosting.AspNetCore.Http;
using Cuemon.Extensions.IO;
using Cuemon.Messaging;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -35,8 +35,8 @@ public async Task ShouldHaveResultOfBoolMiddlewareInBody()

Assert.Equal("Hello awesome developers!", context!.Response.Body.ToEncodedString(o => o.LeaveOpen = true));

var logger = _pipeline.ApplicationServices.GetRequiredService<ILogger<AspNetCoreHostTestTest>>();
logger.LogInformation("Hello from {0}", nameof(ShouldHaveResultOfBoolMiddlewareInBody));
var logger = _pipeline.ApplicationServices.GetRequiredService<ILogger<AspNetCoreHostTestTest>>();
logger.LogInformation("Hello from {0}", nameof(ShouldHaveResultOfBoolMiddlewareInBody));

await pipeline(context);

Expand All @@ -50,14 +50,77 @@ public async Task ShouldHaveResultOfBoolMiddlewareInBody()
Assert.False(options.Value.F);
}

#if NET9_0_OR_GREATER
[Fact]
public void ShouldThrowInvalidOperationException_BecauseOneOfTheServicesIsScoped()
{
var ex = Assert.Throws<InvalidOperationException>(() => _provider.GetServices<ICorrelationToken>());

TestOutput.WriteLine(ex.Message);

Assert.Contains("from root provider because it requires scoped service", ex.Message);
}
#endif
Comment on lines +53 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve Exception Message Assertion for Robustness

When asserting on exception messages, consider that they may vary between .NET versions or due to localization. To make the test more robust, use culture-invariant checks or assert on exception properties if available.

Consider modifying the assertion as follows:

-Assert.Contains("from root provider because it requires scoped service", ex.Message);
+Assert.Contains("requires scoped service", ex.Message, StringComparison.OrdinalIgnoreCase);

Alternatively, if the exception provides specific properties or error codes, assert on those to avoid dependency on message text.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#if NET9_0_OR_GREATER
[Fact]
public void ShouldThrowInvalidOperationException_BecauseOneOfTheServicesIsScoped()
{
var ex = Assert.Throws<InvalidOperationException>(() => _provider.GetServices<ICorrelationToken>());
TestOutput.WriteLine(ex.Message);
Assert.Contains("from root provider because it requires scoped service", ex.Message);
}
#endif
#if NET9_0_OR_GREATER
[Fact]
public void ShouldThrowInvalidOperationException_BecauseOneOfTheServicesIsScoped()
{
var ex = Assert.Throws<InvalidOperationException>(() => _provider.GetServices<ICorrelationToken>());
TestOutput.WriteLine(ex.Message);
Assert.Contains("requires scoped service", ex.Message, StringComparison.OrdinalIgnoreCase);
}
#endif


[Fact]
public void ShouldHaveAccessToCorrelationTokens_UsingScopedProvider()
{
using var scope = _provider.CreateScope();

var firstRequest = scope.ServiceProvider.GetServices<ICorrelationToken>().ToList();
var secondRequest = scope.ServiceProvider.GetServices<ICorrelationToken>().ToList();

TestOutput.WriteLine("----");
TestOutput.WriteLines(firstRequest);
TestOutput.WriteLine("----");
TestOutput.WriteLines(secondRequest);

Assert.Equal(3, firstRequest.Count);
Assert.Equal(3, secondRequest.Count);

Assert.Same(firstRequest[0], secondRequest[0]);
Assert.NotSame(firstRequest[1], secondRequest[1]);
Assert.Same(firstRequest[2], secondRequest[2]);

Assert.Equal(firstRequest[0].CorrelationId, secondRequest[0].CorrelationId);
Assert.NotEqual(firstRequest[1].CorrelationId, secondRequest[1].CorrelationId);
Assert.Equal(firstRequest[2].CorrelationId, secondRequest[2].CorrelationId);
}

[Fact]
public void ShouldHaveAccessToCorrelationTokens_UsingRequestServices() // reference: https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/Features/RequestServicesFeature.cs
{
var context = _provider.GetRequiredService<IHttpContextAccessor>().HttpContext!;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid Using the Null-Forgiving Operator

Using the null-forgiving operator ! can mask potential NullReferenceException issues. Instead, explicitly check for null to ensure the test fails with a meaningful message if HttpContext is not available.

Consider modifying the code:

-var context = _provider.GetRequiredService<IHttpContextAccessor>().HttpContext!;
+var httpContextAccessor = _provider.GetRequiredService<IHttpContextAccessor>();
+Assert.NotNull(httpContextAccessor.HttpContext);
+var context = httpContextAccessor.HttpContext!;

This way, the test will explicitly assert that HttpContext is not null before proceeding.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var context = _provider.GetRequiredService<IHttpContextAccessor>().HttpContext!;
var httpContextAccessor = _provider.GetRequiredService<IHttpContextAccessor>();
Assert.NotNull(httpContextAccessor.HttpContext);
var context = httpContextAccessor.HttpContext!;


var firstRequest = context.RequestServices.GetServices<ICorrelationToken>().ToList();
var secondRequest = context.RequestServices.GetServices<ICorrelationToken>().ToList();

TestOutput.WriteLine("----");
TestOutput.WriteLines(firstRequest);
TestOutput.WriteLine("----");
TestOutput.WriteLines(secondRequest);

Assert.Equal(3, firstRequest.Count);
Assert.Equal(3, secondRequest.Count);

Assert.Same(firstRequest[0], secondRequest[0]);
Assert.NotSame(firstRequest[1], secondRequest[1]);
Assert.Same(firstRequest[2], secondRequest[2]);

Assert.Equal(firstRequest[0].CorrelationId, secondRequest[0].CorrelationId);
Assert.NotEqual(firstRequest[1].CorrelationId, secondRequest[1].CorrelationId);
Assert.Equal(firstRequest[2].CorrelationId, secondRequest[2].CorrelationId);
}

[Fact]
public void ShouldLogToXunitTestLogging()
{
var context = _provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
var logger = _pipeline.ApplicationServices.GetRequiredService<ILogger<AspNetCoreHostTestTest>>();
logger.LogInformation("Hello from {0}", nameof(ShouldLogToXunitTestLogging));
var store = _pipeline.ApplicationServices.GetRequiredService<ILogger<AspNetCoreHostTestTest>>().GetTestStore();
var entry = store.Query(entry => entry.Message.Contains("Hello from", StringComparison.OrdinalIgnoreCase)).SingleOrDefault();

Assert.NotNull(entry);
Assert.Equal("Information: Hello from ShouldLogToXunitTestLogging", entry.Message);
}
Expand All @@ -70,7 +133,7 @@ public override void ConfigureApplication(IApplicationBuilder app)

public override void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IHttpContextAccessor, FakeHttpContextAccessor>();
services.AddFakeHttpContextAccessor();
services.Configure<BoolOptions>(o =>
{
o.A = true;
Expand All @@ -79,6 +142,10 @@ public override void ConfigureServices(IServiceCollection services)
});
services.AddXunitTestLoggingOutputHelperAccessor();
services.AddXunitTestLogging(TestOutput);

services.AddSingleton<ICorrelationToken, SingletonCorrelation>();
services.AddTransient<ICorrelationToken, TransientCorrelation>();
services.AddScoped<ICorrelationToken, ScopedCorrelation>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Cuemon.Messaging;

namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Assets
{
public sealed record ScopedCorrelation : CorrelationToken
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Cuemon.Messaging;

namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Assets
{
public sealed record SingletonCorrelation : CorrelationToken
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Cuemon.Messaging;

namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Assets
{
public sealed record TransientCorrelation : CorrelationToken
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Cuemon.Extensions.AspNetCore" Version="9.0.0-preview.9" />
<PackageReference Include="Cuemon.Extensions.IO" Version="9.0.0-preview.9" />
<PackageReference Include="Cuemon.Extensions.AspNetCore" Version="9.0.0-preview.10" />
<PackageReference Include="Cuemon.Extensions.IO" Version="9.0.0-preview.10" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Cuemon.Core" Version="9.0.0-preview.9" />
<PackageReference Include="Cuemon.Core" Version="9.0.0-preview.10" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
</ItemGroup>

Expand Down
28 changes: 13 additions & 15 deletions test/Codebelt.Extensions.Xunit.Hosting.Tests/HostTestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ namespace Codebelt.Extensions.Xunit.Hosting
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public class HostTestTest : HostTest<HostFixture>
{
private readonly IServiceScope _scope;
private readonly Func<IList<ICorrelationToken>> _correlationsFactory;
private static readonly ConcurrentBag<ICorrelationToken> ScopedCorrelations = new ConcurrentBag<ICorrelationToken>();
private static readonly ConcurrentBag<ICorrelationToken> ScopedCorrelations = new();
Comment on lines +17 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid using static fields in test classes to prevent shared state across tests

The field ScopedCorrelations is declared as static, which can lead to shared state between tests and potentially cause flaky tests due to unintended interactions.

Consider making ScopedCorrelations an instance field:

-private static readonly ConcurrentBag<ICorrelationToken> ScopedCorrelations = new();
+private readonly ConcurrentBag<ICorrelationToken> ScopedCorrelations = new();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private readonly IServiceScope _scope;
private readonly Func<IList<ICorrelationToken>> _correlationsFactory;
private static readonly ConcurrentBag<ICorrelationToken> ScopedCorrelations = new ConcurrentBag<ICorrelationToken>();
private static readonly ConcurrentBag<ICorrelationToken> ScopedCorrelations = new();
private readonly IServiceScope _scope;
private readonly Func<IList<ICorrelationToken>> _correlationsFactory;
private readonly ConcurrentBag<ICorrelationToken> ScopedCorrelations = new();


public HostTestTest(HostFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output)
{
_correlationsFactory = () => hostFixture.ServiceProvider.GetServices<ICorrelationToken>().ToList();
_scope = hostFixture.ServiceProvider.CreateScope();
_correlationsFactory = () => _scope.ServiceProvider.GetServices<ICorrelationToken>().ToList();
Comment on lines +23 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Reusing the same IServiceScope across tests may cause unintended shared state

Creating a single IServiceScope in the constructor and reusing it across multiple tests can lead to shared instances of scoped services, which might result in tests affecting each other.

Consider creating a new IServiceScope within each test method to ensure proper isolation. This can be achieved by initializing the scope at the beginning of each test and disposing of it at the end.

}

[Fact, Priority(1)]
public void Test_SingletonShouldBeSame()
public void Test_SingletonShouldBeSame() // simulate a request
{
ScopedCorrelations.Add(_correlationsFactory().Single(c => c is ScopedCorrelation));
var c1 = _correlationsFactory().Single(c => c is SingletonCorrelation);
Expand All @@ -32,7 +34,7 @@ public void Test_SingletonShouldBeSame()
}

[Fact, Priority(2)]
public void Test_TransientShouldBeDifferent()
public void Test_TransientShouldBeDifferent() // simulate a request
{
ScopedCorrelations.Add(_correlationsFactory().Single(c => c is ScopedCorrelation));
var c1 = _correlationsFactory().Single(c => c is TransientCorrelation);
Expand All @@ -41,23 +43,14 @@ public void Test_TransientShouldBeDifferent()
}

[Fact, Priority(3)]
public void Test_ScopedShouldBeSame()
public void Test_ScopedShouldBeSame() // simulate a request
{
ScopedCorrelations.Add(_correlationsFactory().Single(c => c is ScopedCorrelation));
var c1 = _correlationsFactory().Single(c => c is ScopedCorrelation);
var c2 = _correlationsFactory().Single(c => c is ScopedCorrelation);
Assert.Equal(c1.CorrelationId, c2.CorrelationId);
}

[Fact]
public void Test_ScopedShouldBeSameInLastTestRun()
{
var c1 = _correlationsFactory().Single(c => c is ScopedCorrelation);
if (ScopedCorrelations.IsEmpty) { return; }
Assert.Equal(3, ScopedCorrelations.Count);
Assert.All(ScopedCorrelations, c => Assert.Equal(c1.CorrelationId, c.CorrelationId));
}

[Fact]
public void Test_ShouldHaveConfigurationEntry()
{
Expand All @@ -70,11 +63,16 @@ public void Test_ShouldHaveEnvironmentOfDevelopment()
Assert.Equal("Development", HostingEnvironment.EnvironmentName);
}

protected override void OnDisposeManagedResources()
{
_scope?.Dispose();
}
Comment on lines +66 to +69
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Override OnDisposeManagedResources should call base class method

When overriding OnDisposeManagedResources, it's important to call the base class implementation to ensure that any disposal logic in the base class is executed.

Apply this diff to call the base class method:

protected override void OnDisposeManagedResources()
{
+    base.OnDisposeManagedResources();
    _scope?.Dispose();
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected override void OnDisposeManagedResources()
{
_scope?.Dispose();
}
protected override void OnDisposeManagedResources()
{
base.OnDisposeManagedResources();
_scope?.Dispose();
}


public override void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ICorrelationToken, SingletonCorrelation>();
services.AddTransient<ICorrelationToken, TransientCorrelation>();
services.AddScoped<ICorrelationToken, ScopedCorrelation>();
}
}
}
}