From 45a5477a4b6bbcf58e0b6804e9183290af827d34 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Sun, 6 Oct 2024 13:49:01 +0200 Subject: [PATCH 1/5] :recycle: refactored to be consistent with .net9 release (e.g., enable ValidateOnBuild and ValidateScopes) and have a running host --- .../AspNetCoreHostFixture.cs | 2 ++ .../HostFixture.cs | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/AspNetCoreHostFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/AspNetCoreHostFixture.cs index 6bae8cb..3e7c912 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/AspNetCoreHostFixture.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/AspNetCoreHostFixture.cs @@ -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); diff --git a/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs index 7b6ac24..2919227 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs @@ -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; @@ -54,13 +55,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; } /// From aa292403a0e57b479aea5a6eee8628ce68705545 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Sun, 6 Oct 2024 13:50:54 +0200 Subject: [PATCH 2/5] :recycle: improved to support RequestServicesFeature --- .../Http/FakeHttpContextAccessor.cs | 10 +++++++--- .../ServiceCollectionExtensions.cs | 7 +++---- .../WebHostTest.cs | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs index 2304910..054618f 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs @@ -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 { @@ -13,17 +14,20 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Http /// public class FakeHttpContextAccessor : IHttpContextAccessor { - /// /// Initializes a new instance of the class. /// - public FakeHttpContextAccessor() + public FakeHttpContextAccessor(IServiceScopeFactory factory = null) { + var context = new DefaultHttpContext(); var fc = new FeatureCollection(); + fc.Set(new RequestServicesFeature(context, factory)); fc.Set(new FakeHttpResponseFeature()); fc.Set(new FakeHttpRequestFeature()); fc.Set(new StreamResponseBodyFeature(MakeGreeting("Hello awesome developers!"))); - HttpContext = new DefaultHttpContext(fc); + context.Uninitialize(); + context.Initialize(fc); + HttpContext = context; } private Stream MakeGreeting(string greeting) diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ServiceCollectionExtensions.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ServiceCollectionExtensions.cs index 859d0d3..c8b6978 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ServiceCollectionExtensions.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/ServiceCollectionExtensions.cs @@ -16,9 +16,9 @@ public static class ServiceCollectionExtensions /// Adds a unit test optimized implementation for the service. /// /// The to extend. - /// The lifetime of the service. + /// The lifetime of the service. Default is . /// A reference to after the operation has completed. - public static IServiceCollection AddFakeHttpContextAccessor(this IServiceCollection services, ServiceLifetime lifetime) + public static IServiceCollection AddFakeHttpContextAccessor(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Singleton) { switch (lifetime) { @@ -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()); } } } diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebHostTest.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebHostTest.cs index 0d722bf..b31c050 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebHostTest.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/WebHostTest.cs @@ -83,7 +83,7 @@ public override void ConfigureServices(IServiceCollection services) hbc.HostingEnvironment = HostingEnvironment; return hbc; }), services); - services.AddFakeHttpContextAccessor(ServiceLifetime.Singleton); + services.AddFakeHttpContextAccessor(); } } } From 533a21abbc36461ca3e677cf74ba6a187fd8a91e Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Sun, 6 Oct 2024 13:51:32 +0200 Subject: [PATCH 3/5] :white_check_mark: fixed and improved unittesting for Transient, Scoped and Singleton for both HostTest and AspNetCoreHostTest --- .../AspNetCoreHostTestTest.cs | 77 +++++++++++++++++-- .../Assets/ScopedCorrelation.cs | 8 ++ .../Assets/SingletonCorrelation.cs | 8 ++ .../Assets/TransientCorrelation.cs | 8 ++ ...ions.Xunit.Hosting.AspNetCore.Tests.csproj | 4 +- ...belt.Extensions.Xunit.Hosting.Tests.csproj | 2 +- .../HostTestTest.cs | 28 ++++--- 7 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/ScopedCorrelation.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/SingletonCorrelation.cs create mode 100644 test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/TransientCorrelation.cs diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/AspNetCoreHostTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/AspNetCoreHostTestTest.cs index 559e90a..07fb254 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/AspNetCoreHostTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/AspNetCoreHostTestTest.cs @@ -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; @@ -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>(); - logger.LogInformation("Hello from {0}", nameof(ShouldHaveResultOfBoolMiddlewareInBody)); + var logger = _pipeline.ApplicationServices.GetRequiredService>(); + logger.LogInformation("Hello from {0}", nameof(ShouldHaveResultOfBoolMiddlewareInBody)); await pipeline(context); @@ -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(() => _provider.GetServices()); + + TestOutput.WriteLine(ex.Message); + + Assert.Contains("from root provider because it requires scoped service", ex.Message); + } +#endif + + [Fact] + public void ShouldHaveAccessToCorrelationTokens_UsingScopedProvider() + { + using var scope = _provider.CreateScope(); + + var firstRequest = scope.ServiceProvider.GetServices().ToList(); + var secondRequest = scope.ServiceProvider.GetServices().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().HttpContext!; + + var firstRequest = context.RequestServices.GetServices().ToList(); + var secondRequest = context.RequestServices.GetServices().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().HttpContext; var logger = _pipeline.ApplicationServices.GetRequiredService>(); logger.LogInformation("Hello from {0}", nameof(ShouldLogToXunitTestLogging)); var store = _pipeline.ApplicationServices.GetRequiredService>().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); } @@ -70,7 +133,7 @@ public override void ConfigureApplication(IApplicationBuilder app) public override void ConfigureServices(IServiceCollection services) { - services.AddTransient(); + services.AddFakeHttpContextAccessor(); services.Configure(o => { o.A = true; @@ -79,6 +142,10 @@ public override void ConfigureServices(IServiceCollection services) }); services.AddXunitTestLoggingOutputHelperAccessor(); services.AddXunitTestLogging(TestOutput); + + services.AddSingleton(); + services.AddTransient(); + services.AddScoped(); } } } diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/ScopedCorrelation.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/ScopedCorrelation.cs new file mode 100644 index 0000000..fca7bea --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/ScopedCorrelation.cs @@ -0,0 +1,8 @@ +using Cuemon.Messaging; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Assets +{ + public sealed record ScopedCorrelation : CorrelationToken + { + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/SingletonCorrelation.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/SingletonCorrelation.cs new file mode 100644 index 0000000..91c264f --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/SingletonCorrelation.cs @@ -0,0 +1,8 @@ +using Cuemon.Messaging; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Assets +{ + public sealed record SingletonCorrelation : CorrelationToken + { + } +} diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/TransientCorrelation.cs b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/TransientCorrelation.cs new file mode 100644 index 0000000..1656baa --- /dev/null +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Assets/TransientCorrelation.cs @@ -0,0 +1,8 @@ +using Cuemon.Messaging; + +namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Assets +{ + public sealed record TransientCorrelation : CorrelationToken + { + } +} \ No newline at end of file diff --git a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj index c67c1ea..7f9809d 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj +++ b/test/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests/Codebelt.Extensions.Xunit.Hosting.AspNetCore.Tests.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/test/Codebelt.Extensions.Xunit.Hosting.Tests/Codebelt.Extensions.Xunit.Hosting.Tests.csproj b/test/Codebelt.Extensions.Xunit.Hosting.Tests/Codebelt.Extensions.Xunit.Hosting.Tests.csproj index cae2ea1..6c49c08 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.Tests/Codebelt.Extensions.Xunit.Hosting.Tests.csproj +++ b/test/Codebelt.Extensions.Xunit.Hosting.Tests/Codebelt.Extensions.Xunit.Hosting.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/Codebelt.Extensions.Xunit.Hosting.Tests/HostTestTest.cs b/test/Codebelt.Extensions.Xunit.Hosting.Tests/HostTestTest.cs index 04b8533..5f89a32 100644 --- a/test/Codebelt.Extensions.Xunit.Hosting.Tests/HostTestTest.cs +++ b/test/Codebelt.Extensions.Xunit.Hosting.Tests/HostTestTest.cs @@ -14,16 +14,18 @@ namespace Codebelt.Extensions.Xunit.Hosting [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] public class HostTestTest : HostTest { + private readonly IServiceScope _scope; private readonly Func> _correlationsFactory; - private static readonly ConcurrentBag ScopedCorrelations = new ConcurrentBag(); + private static readonly ConcurrentBag ScopedCorrelations = new(); public HostTestTest(HostFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { - _correlationsFactory = () => hostFixture.ServiceProvider.GetServices().ToList(); + _scope = hostFixture.ServiceProvider.CreateScope(); + _correlationsFactory = () => _scope.ServiceProvider.GetServices().ToList(); } [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); @@ -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); @@ -41,7 +43,7 @@ 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); @@ -49,15 +51,6 @@ public void Test_ScopedShouldBeSame() 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() { @@ -70,6 +63,11 @@ public void Test_ShouldHaveEnvironmentOfDevelopment() Assert.Equal("Development", HostingEnvironment.EnvironmentName); } + protected override void OnDisposeManagedResources() + { + _scope?.Dispose(); + } + public override void ConfigureServices(IServiceCollection services) { services.AddSingleton(); @@ -77,4 +75,4 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); } } -} \ No newline at end of file +} From 69a0a0e326d6790bada36aefc930037db9db1f7a Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Sun, 6 Oct 2024 13:58:52 +0200 Subject: [PATCH 4/5] :thread: added threadsafety to Dispose --- .../HostFixture.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs b/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs index 2919227..af3485a 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting/HostFixture.cs @@ -13,6 +13,8 @@ namespace Codebelt.Extensions.Xunit.Hosting /// public class HostFixture : IDisposable, IHostFixture { + private readonly object _lock = new(); + /// /// Initializes a new instance of the class. /// @@ -195,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; } } } From f12084241c2fa2847e6b2a243863f050559aaa3d Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Sun, 6 Oct 2024 14:19:19 +0200 Subject: [PATCH 5/5] :memo: updated xml doc --- .../Http/FakeHttpContextAccessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs index 054618f..9ca0c80 100644 --- a/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs +++ b/src/Codebelt.Extensions.Xunit.Hosting.AspNetCore/Http/FakeHttpContextAccessor.cs @@ -15,8 +15,9 @@ namespace Codebelt.Extensions.Xunit.Hosting.AspNetCore.Http public class FakeHttpContextAccessor : IHttpContextAccessor { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// An optional for resolving services. public FakeHttpContextAccessor(IServiceScopeFactory factory = null) { var context = new DefaultHttpContext();