diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 0c468f1b1..05fe57231 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -31,7 +31,7 @@ "commands": ["docfx"] }, "strawberryshake.tools": { - "version": "14.0.0-p.90", + "version": "14.0.0-p.93", "commands": ["dotnet-graphql"] }, "dotnet-outdated-tool": { diff --git a/Directory.Packages.props b/Directory.Packages.props index d541dd160..58e030a53 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,8 @@  + + @@ -32,27 +34,27 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -72,6 +74,7 @@ + @@ -91,15 +94,15 @@ - - - - - + + + + + - - + + @@ -133,6 +136,7 @@ + diff --git a/sample/Sample.BlazorServer/Program.cs b/sample/Sample.BlazorServer/Program.cs index 16071cc9c..9355fa055 100644 --- a/sample/Sample.BlazorServer/Program.cs +++ b/sample/Sample.BlazorServer/Program.cs @@ -1,5 +1,3 @@ -using System.Runtime.Loader; -using Rocket.Surgery.Conventions; using Rocket.Surgery.Hosting; using Rocket.Surgery.LaunchPad.AspNetCore; using Sample.BlazorServer; @@ -11,7 +9,7 @@ builder.Services.AddServerSideBlazor(); builder.Services.AddSingleton(); -var app = await builder.LaunchWith(RocketBooster.For(Imports.Instance), b => b.Set(AssemblyLoadContext.Default)); +var app = await builder.LaunchWith(RocketBooster.For(Imports.Instance)); if (builder.Environment.IsDevelopment()) { diff --git a/sample/Sample.Classic.Restful/Program.cs b/sample/Sample.Classic.Restful/Program.cs index cc6378eef..06e7104d1 100644 --- a/sample/Sample.Classic.Restful/Program.cs +++ b/sample/Sample.Classic.Restful/Program.cs @@ -1,7 +1,5 @@ using System.Reflection; -using System.Runtime.Loader; using Hellang.Middleware.ProblemDetails; -using Rocket.Surgery.Conventions; using Rocket.Surgery.Hosting; using Rocket.Surgery.LaunchPad.AspNetCore; using Sample.Classic.Restful; @@ -22,7 +20,7 @@ } ) ); -var app = await builder.LaunchWith(RocketBooster.For(Imports.Instance), b => b.Set(AssemblyLoadContext.Default)); +var app = await builder.LaunchWith(RocketBooster.For(Imports.Instance)); app.UseProblemDetails(); app.UseHttpsRedirection(); diff --git a/sample/Sample.Graphql/Program.cs b/sample/Sample.Graphql/Program.cs index 05a39b914..df1a93b41 100644 --- a/sample/Sample.Graphql/Program.cs +++ b/sample/Sample.Graphql/Program.cs @@ -1,6 +1,4 @@ -using System.Runtime.Loader; using HotChocolate.Types.Spatial; -using Rocket.Surgery.Conventions; using Rocket.Surgery.Hosting; using Rocket.Surgery.LaunchPad.AspNetCore; using Rocket.Surgery.LaunchPad.HotChocolate; @@ -30,7 +28,7 @@ .ModifyRequestOptions(options => options.IncludeExceptionDetails = true); var app = await builder - .LaunchWith(RocketBooster.For(Imports.Instance), b => b.Set(AssemblyLoadContext.Default)); + .LaunchWith(RocketBooster.For(Imports.Instance)); app.UseHttpLogging(); app.UseLaunchPadRequestLogging(); diff --git a/sample/Sample.Pages/Program.cs b/sample/Sample.Pages/Program.cs index 688d52c7a..078dbfab6 100644 --- a/sample/Sample.Pages/Program.cs +++ b/sample/Sample.Pages/Program.cs @@ -1,9 +1,7 @@ -using System.Runtime.Loader; using System.Text; using System.Text.Json; using Humanizer; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Rocket.Surgery.Conventions; using Rocket.Surgery.Hosting; using Rocket.Surgery.LaunchPad.AspNetCore; using Sample.Pages; @@ -12,7 +10,7 @@ builder.Services.AddRazorPages(); builder.Services.AddControllersWithViews(); -var app = await builder.LaunchWith(RocketBooster.For(Imports.Instance), b => b.Set(AssemblyLoadContext.Default)); +var app = await builder.LaunchWith(RocketBooster.For(Imports.Instance)); if (builder.Environment.IsDevelopment()) { diff --git a/src/AspNetCore/Conventions/AspNetCoreConventionInstrumentationConvention.cs b/src/AspNetCore/Conventions/AspNetCoreConventionInstrumentationConvention.cs index 1f9e17776..a15a0e056 100644 --- a/src/AspNetCore/Conventions/AspNetCoreConventionInstrumentationConvention.cs +++ b/src/AspNetCore/Conventions/AspNetCoreConventionInstrumentationConvention.cs @@ -19,7 +19,7 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions; public class AspNetCoreConventionInstrumentationConvention : IOpenTelemetryConvention { /// - public void Register(IConventionContext conventionContext, IConfiguration configuration, IOpenTelemetryBuilder builder) + public void Register(IConventionContext context, IConfiguration configuration, IOpenTelemetryBuilder builder) { builder.WithTracing(b => b.AddAspNetCoreInstrumentation(options => options.RecordException = true)); builder.WithMetrics(b => b.AddAspNetCoreInstrumentation()); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e8adea179..81b1e87aa 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,5 +15,6 @@ + diff --git a/src/Foundation/Conventions/InstrumentationConvention.cs b/src/Foundation/Conventions/InstrumentationConvention.cs index c020f0c46..5ef29f59e 100644 --- a/src/Foundation/Conventions/InstrumentationConvention.cs +++ b/src/Foundation/Conventions/InstrumentationConvention.cs @@ -17,9 +17,9 @@ namespace Rocket.Surgery.LaunchPad.Foundation.Conventions; public class InstrumentationConvention : IOpenTelemetryConvention { /// - public void Register(IConventionContext conventionContext, IConfiguration configuration, IOpenTelemetryBuilder builder) + public void Register(IConventionContext context, IConfiguration configuration, IOpenTelemetryBuilder builder) { builder.WithTracing(b => b.AddHttpClientInstrumentation(x => x.RecordException = true)); - builder.WithMetrics(b => b.AddHttpClientInstrumentation()); + builder.WithMetrics(b => b.AddRuntimeInstrumentation().AddHttpClientInstrumentation()); } } \ No newline at end of file diff --git a/src/Foundation/Conventions/OptionsConvention.cs b/src/Foundation/Conventions/OptionsConvention.cs new file mode 100644 index 000000000..40d0326bc --- /dev/null +++ b/src/Foundation/Conventions/OptionsConvention.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.Conventions; +using Rocket.Surgery.Conventions.DependencyInjection; + +namespace Rocket.Surgery.LaunchPad.Foundation.Conventions; + +/// +/// A convention that registers any options POCOs that are found with the +/// +[ExportConvention] +public class OptionsConvention : IServiceConvention +{ + private readonly MethodInfo _configureMethod; + + /// + /// A convention that registers any options POCOs that are found with the + /// + public OptionsConvention() + { + _configureMethod = typeof(OptionsConfigurationServiceCollectionExtensions).GetMethod( + nameof(OptionsConfigurationServiceCollectionExtensions.Configure), + [typeof(IServiceCollection), typeof(string), typeof(IConfiguration),] + )!; + } + + /// + public void Register(IConventionContext context, IConfiguration configuration, IServiceCollection services) + { + var classes = context.AssemblyProvider.GetTypes( + s => s.FromAssemblyDependenciesOf().GetTypes(f => f.WithAttribute()) + ); + foreach (var options in classes) + { + var attribute = options.GetCustomAttribute()!; + _configureMethod.MakeGenericMethod(options).Invoke(null, [services, attribute.OptionsName, configuration.GetSection(attribute.ConfigurationKey),]); + } + } +} \ No newline at end of file diff --git a/src/Foundation/Conventions/ServiceDiscoveryConvention.cs b/src/Foundation/Conventions/ServiceDiscoveryConvention.cs new file mode 100644 index 000000000..13866f843 --- /dev/null +++ b/src/Foundation/Conventions/ServiceDiscoveryConvention.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.Conventions; +using Rocket.Surgery.Conventions.DependencyInjection; + +namespace Rocket.Surgery.LaunchPad.Foundation.Conventions; + +/// +/// Service conventions using service discovery +/// +[PublicAPI] +[ExportConvention] +public class ServiceDiscoveryConvention : IServiceConvention +{ + /// + public void Register(IConventionContext context, IConfiguration configuration, IServiceCollection services) + { + services.AddServiceDiscovery(); + + services.ConfigureHttpClientDefaults( + http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + } + ); + } +} \ No newline at end of file diff --git a/src/Foundation/GenerationIgnoreAttribute.cs b/src/Foundation/GenerationIgnoreAttribute.cs index 8fcbdcb60..65c7a825e 100644 --- a/src/Foundation/GenerationIgnoreAttribute.cs +++ b/src/Foundation/GenerationIgnoreAttribute.cs @@ -3,5 +3,6 @@ namespace Rocket.Surgery.LaunchPad.Foundation; /// /// Exclude the given property from the generation via launch pad source generators /// +[PublicAPI] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Constructor)] public sealed class GenerationIgnoreAttribute : Attribute; \ No newline at end of file diff --git a/src/Foundation/RegisterOptionsConfigurationAttribute.cs b/src/Foundation/RegisterOptionsConfigurationAttribute.cs new file mode 100644 index 000000000..918063ff3 --- /dev/null +++ b/src/Foundation/RegisterOptionsConfigurationAttribute.cs @@ -0,0 +1,19 @@ +namespace Rocket.Surgery.LaunchPad.Foundation; + +/// +/// Register the options using the configuration key as the configuration root +/// +[PublicAPI] +[AttributeUsage(AttributeTargets.Class)] +public sealed class RegisterOptionsConfigurationAttribute(string configurationKey) : Attribute +{ + /// + /// The configuration key to use + /// + public string ConfigurationKey { get; } = configurationKey; + + /// + /// The optional options name + /// + public string? OptionsName { get; set; } +} \ No newline at end of file diff --git a/src/Foundation/Rocket.Surgery.LaunchPad.Foundation.csproj b/src/Foundation/Rocket.Surgery.LaunchPad.Foundation.csproj index c9ba1712f..3a25e4f96 100644 --- a/src/Foundation/Rocket.Surgery.LaunchPad.Foundation.csproj +++ b/src/Foundation/Rocket.Surgery.LaunchPad.Foundation.csproj @@ -16,7 +16,9 @@ - + + + @@ -28,6 +30,7 @@ + diff --git a/src/Hosting/ApplicationLifecycleExtensions.cs b/src/Hosting/ApplicationLifecycleExtensions.cs new file mode 100644 index 000000000..f9bc30e16 --- /dev/null +++ b/src/Hosting/ApplicationLifecycleExtensions.cs @@ -0,0 +1,203 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Rocket.Surgery.LaunchPad.Hosting; + +/// +/// Extensions for configuring the application lifecycle +/// +[PublicAPI] +public static class ApplicationLifecycleExtensions +{ + /// + /// Run a simple action when the host has started + /// + /// + /// + /// + /// + public static T OnHostStarted(this T builder, Action action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton( + new ApplicationLifecycleRegistration( + nameof(IHostedLifecycleService.StartedAsync), + (provider, _) => + { + action(provider); + return Task.CompletedTask; + } + ) + ); + return builder; + } + + /// + /// Run a simple action when the host has started + /// + /// + /// + /// + /// + public static T OnHostStarted(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StartedAsync), (provider, _) => action(provider))); + return builder; + } + + /// + /// Run a simple action when the host has started + /// + /// + /// + /// + /// + public static T OnHostStarted(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StartedAsync), action)); + return builder; + } + + /// + /// Run a simple action when the host has starting + /// + /// + /// + /// + /// + public static T OnHostStarting(this T builder, Action action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton( + new ApplicationLifecycleRegistration( + nameof(IHostedLifecycleService.StartingAsync), + (provider, _) => + { + action(provider); + return Task.CompletedTask; + } + ) + ); + return builder; + } + + /// + /// Run a simple action when the host has starting + /// + /// + /// + /// + /// + public static T OnHostStarting(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StartingAsync), (provider, _) => action(provider))); + return builder; + } + + /// + /// Run a simple action when the host has starting + /// + /// + /// + /// + /// + public static T OnHostStarting(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StartingAsync), action)); + return builder; + } + + /// + /// Run a simple action when the host has stopping + /// + /// + /// + /// + /// + public static T OnHostStopping(this T builder, Action action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton( + new ApplicationLifecycleRegistration( + nameof(IHostedLifecycleService.StoppingAsync), + (provider, _) => + { + action(provider); + return Task.CompletedTask; + } + ) + ); + return builder; + } + + /// + /// Run a simple action when the host has stopping + /// + /// + /// + /// + /// + public static T OnHostStopping(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StoppingAsync), (provider, _) => action(provider))); + return builder; + } + + /// + /// Run a simple action when the host has stopping + /// + /// + /// + /// + /// + public static T OnHostStopping(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StoppingAsync), action)); + return builder; + } + + /// + /// Run a simple action when the host has stopped + /// + /// + /// + /// + /// + public static T OnHostStopped(this T builder, Action action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton( + new ApplicationLifecycleRegistration( + nameof(IHostedLifecycleService.StoppedAsync), + (provider, _) => + { + action(provider); + return Task.CompletedTask; + } + ) + ); + return builder; + } + + /// + /// Run a simple action when the host has stopped + /// + /// + /// + /// + /// + public static T OnHostStopped(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StoppedAsync), (provider, _) => action(provider))); + return builder; + } + + /// + /// Run a simple action when the host has stopped + /// + /// + /// + /// + /// + public static T OnHostStopped(this T builder, Func action) where T : IHostApplicationBuilder + { + builder.Services.AddSingleton(new ApplicationLifecycleRegistration(nameof(IHostedLifecycleService.StoppedAsync), action)); + return builder; + } +} \ No newline at end of file diff --git a/src/Hosting/ApplicationLifecycleRegistration.cs b/src/Hosting/ApplicationLifecycleRegistration.cs new file mode 100644 index 000000000..e19859bf5 --- /dev/null +++ b/src/Hosting/ApplicationLifecycleRegistration.cs @@ -0,0 +1,3 @@ +namespace Rocket.Surgery.LaunchPad.Hosting; + +internal record ApplicationLifecycleRegistration(string Method, Func Action); \ No newline at end of file diff --git a/src/Hosting/ApplicationLifecycleService.cs b/src/Hosting/ApplicationLifecycleService.cs new file mode 100644 index 000000000..2010002ce --- /dev/null +++ b/src/Hosting/ApplicationLifecycleService.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Hosting; + +namespace Rocket.Surgery.LaunchPad.Hosting; + +internal class ApplicationLifecycleService + (IServiceProvider serviceProvider, IEnumerable registrations) : IHostedLifecycleService +{ + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StartingAsync(CancellationToken cancellationToken) + { + return Task.WhenAll(registrations.Where(z => z.Method == nameof(StartingAsync)).Select(z => z.Action(serviceProvider, cancellationToken))); + } + + public Task StartedAsync(CancellationToken cancellationToken) + { + return Task.WhenAll(registrations.Where(z => z.Method == nameof(StartedAsync)).Select(z => z.Action(serviceProvider, cancellationToken))); + } + + public Task StoppingAsync(CancellationToken cancellationToken) + { + return Task.WhenAll(registrations.Where(z => z.Method == nameof(StoppingAsync)).Select(z => z.Action(serviceProvider, cancellationToken))); + } + + public Task StoppedAsync(CancellationToken cancellationToken) + { + return Task.WhenAll(registrations.Where(z => z.Method == nameof(StoppedAsync)).Select(z => z.Action(serviceProvider, cancellationToken))); + } +} \ No newline at end of file diff --git a/src/Hosting/Conventions/HostingConvention.cs b/src/Hosting/Conventions/HostingConvention.cs new file mode 100644 index 000000000..cdfe897a5 --- /dev/null +++ b/src/Hosting/Conventions/HostingConvention.cs @@ -0,0 +1,68 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Resources; +using Rocket.Surgery.Conventions; +using Rocket.Surgery.Conventions.DependencyInjection; +using Rocket.Surgery.Hosting; +using Rocket.Surgery.LaunchPad.Serilog; +using Rocket.Surgery.LaunchPad.Telemetry; + +namespace Rocket.Surgery.LaunchPad.Hosting.Conventions; + +[ExportConvention] +internal class HostingConvention : IServiceConvention, IHostApplicationConvention, IOpenTelemetryConvention +{ + public void Register(IConventionContext context, IHostApplicationBuilder builder) + { + if (context.GetOrAdd(() => new LaunchPadLoggingOptions()).WriteToProviders != true) + { + var providers = builder.Services.Where(z => z.ServiceType == typeof(ILoggerProvider)).ToArray(); + builder.Logging.ClearProviders(); + builder.OnHostStarting( + provider => providers.Aggregate( + provider.GetRequiredService(), + (factory, descriptor) => + { + switch (descriptor) + { + case { ImplementationFactory: { } method, } when method(provider) is ILoggerProvider p: + factory.AddProvider(p); + break; + case { ImplementationInstance: ILoggerProvider instance, }: + factory.AddProvider(instance); + break; + case { ImplementationType: { } type, } when ActivatorUtilities.CreateInstance(provider, type) is ILoggerProvider instance: + factory.AddProvider(instance); + break; + } + + return factory; + } + ) + ); + } + } + + public void Register(IConventionContext context, IConfiguration configuration, IOpenTelemetryBuilder builder) + { + builder.ConfigureResource( + z => z + .AddTelemetrySdk() + .AddService( + configuration["OTEL_SERVICE_NAME"] ?? context.Get()?.ApplicationName ?? "unknown", + "Syndicates", + Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion, + serviceInstanceId: configuration["WEBSITE_SITE_NAME"] + ) + ); + } + + public void Register(IConventionContext context, IConfiguration configuration, IServiceCollection services) + { + services.AddHostedService(); + } +} \ No newline at end of file diff --git a/src/HotChocolate/Conventions/InstrumentationConvention.cs b/src/HotChocolate/Conventions/InstrumentationConvention.cs index ece094307..a6d01de0c 100644 --- a/src/HotChocolate/Conventions/InstrumentationConvention.cs +++ b/src/HotChocolate/Conventions/InstrumentationConvention.cs @@ -16,7 +16,7 @@ namespace Rocket.Surgery.LaunchPad.HotChocolate.Conventions; public class InstrumentationConvention : IOpenTelemetryConvention { /// - public void Register(IConventionContext conventionContext, IConfiguration configuration, IOpenTelemetryBuilder builder) + public void Register(IConventionContext context, IConfiguration configuration, IOpenTelemetryBuilder builder) { builder.WithTracing(b => b.AddHotChocolateInstrumentation()); } diff --git a/src/Serilog/LaunchPadLoggingOptions.cs b/src/Serilog/LaunchPadLoggingOptions.cs index 8b61c7a8f..eebe219d7 100644 --- a/src/Serilog/LaunchPadLoggingOptions.cs +++ b/src/Serilog/LaunchPadLoggingOptions.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Logging; - namespace Rocket.Surgery.LaunchPad.Serilog; /// @@ -32,15 +30,10 @@ public class LaunchPadLoggingOptions /// /// Base option from the serilog package /// - public bool WriteToProviders { get; set; } = true; + public bool WriteToProviders { get; set; } = false; /// /// Base option from the serilog package /// public bool PreserveStaticLogger { get; set; } - - /// - /// Set a custom logger factory - /// - public ILoggerFactory? LoggerFactory { get; set; } -} +} \ No newline at end of file diff --git a/src/Telemetry/IOpenTelemetryConvention.cs b/src/Telemetry/IOpenTelemetryConvention.cs index 22a63e84a..0a950b1af 100644 --- a/src/Telemetry/IOpenTelemetryConvention.cs +++ b/src/Telemetry/IOpenTelemetryConvention.cs @@ -14,8 +14,8 @@ public interface IOpenTelemetryConvention : IConvention /// /// Register metrics /// - /// + /// /// /// - void Register(IConventionContext conventionContext, IConfiguration configuration, IOpenTelemetryBuilder builder); + void Register(IConventionContext context, IConfiguration configuration, IOpenTelemetryBuilder builder); } \ No newline at end of file diff --git a/test/Extensions.Tests/ConventionFakeTest.cs b/test/Extensions.Tests/ConventionFakeTest.cs index 319309b24..773999ba5 100644 --- a/test/Extensions.Tests/ConventionFakeTest.cs +++ b/test/Extensions.Tests/ConventionFakeTest.cs @@ -1,4 +1,4 @@ -using System.Runtime.Loader; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Rocket.Surgery.Conventions; using Rocket.Surgery.Conventions.Testing; @@ -13,10 +13,12 @@ protected async Task Init(Action? action = null) var conventionContextBuilder = ConventionContextBuilder .Create() .ForTesting(Imports.Instance, LoggerFactory) - .Set(AssemblyLoadContext.Default) .WithLogger(Logger); action?.Invoke(conventionContextBuilder); + var context = await ConventionContext.FromAsync(conventionContextBuilder); + var configuration = await new ConfigurationBuilder().ApplyConventionsAsync(context); + context.Set(configuration.Build()); Populate(await new ServiceCollection().ApplyConventionsAsync(context)); } diff --git a/test/Extensions.Tests/Extensions.Tests.csproj b/test/Extensions.Tests/Extensions.Tests.csproj index 9158f4084..58a56169b 100644 --- a/test/Extensions.Tests/Extensions.Tests.csproj +++ b/test/Extensions.Tests/Extensions.Tests.csproj @@ -9,12 +9,6 @@ - - - - - - diff --git a/test/Extensions.Tests/FakeClockConventionTests.cs b/test/Extensions.Tests/FakeClockConventionTests.cs index f7b50c735..4cf63ebf6 100644 --- a/test/Extensions.Tests/FakeClockConventionTests.cs +++ b/test/Extensions.Tests/FakeClockConventionTests.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NodaTime; using NodaTime.Testing; using Rocket.Surgery.Conventions; +using Rocket.Surgery.LaunchPad.Foundation; using Rocket.Surgery.LaunchPad.Testing; namespace Extensions.Tests; @@ -28,4 +31,29 @@ public async Task Clock_Convention_Override() clock.GetCurrentInstant().Should().Be(Instant.FromUnixTimeSeconds(0)); clock.GetCurrentInstant().Should().Be(Instant.FromUnixTimeSeconds(0) + Duration.FromMinutes(1)); } +} + +public class AutoRegisterOptions(ITestOutputHelper testOutputHelper) : ConventionFakeTest(testOutputHelper) +{ + [Fact] + public async Task Should_Register_Options() + { + await Init(x => x.ConfigureConfiguration(builder => builder.AddInMemoryCollection([new("OptionsA:A", "B"), new("OptionsB:B", "A"),]))); + ServiceProvider.GetRequiredService>().Value.A.Should().Be("B"); + ServiceProvider.GetRequiredService>().Value.B.Should().Be("A"); + } + + [RegisterOptionsConfiguration("OptionsA")] + [PublicAPI] + private class OptionsA + { + public string A { get; set; } + } + + [RegisterOptionsConfiguration("OptionsB")] + [PublicAPI] + private class OptionsB + { + public string B { get; set; } + } } \ No newline at end of file diff --git a/test/Extensions.Tests/Mapping/AutoMapperProfile.cs b/test/Extensions.Tests/Mapping/AutoMapperProfile.cs index 4ee1d8634..7c75c8eb4 100644 --- a/test/Extensions.Tests/Mapping/AutoMapperProfile.cs +++ b/test/Extensions.Tests/Mapping/AutoMapperProfile.cs @@ -1,4 +1,3 @@ -using System.Runtime.Loader; using AutoMapper; using Rocket.Surgery.Conventions; using Rocket.Surgery.Extensions.Testing; @@ -252,8 +251,7 @@ public class AutoMapperConventionTests public async Task ShouldRegisterAutoMapperTypes() { var conventionBuilder = new ConventionContextBuilder(new Dictionary()) - .UseConventionFactory(Imports.Instance) - .Set(AssemblyLoadContext.Default); + .UseConventionFactory(Imports.Instance); var context = await ConventionContext.FromAsync(conventionBuilder); var types = context .AssemblyProvider.GetTypes( diff --git a/test/Extensions.Tests/Validation/OptionsValidationTests.cs b/test/Extensions.Tests/Validation/OptionsValidationTests.cs index 1fa4eb037..09d91d0a1 100644 --- a/test/Extensions.Tests/Validation/OptionsValidationTests.cs +++ b/test/Extensions.Tests/Validation/OptionsValidationTests.cs @@ -1,5 +1,4 @@ #if NET6_0_OR_GREATER -using System.Runtime.Loader; using DryIoc; using FluentValidation; using Microsoft.Extensions.DependencyInjection; @@ -93,7 +92,6 @@ public async Task InitializeAsync() var conventionContextBuilder = ConventionContextBuilder .Create() .ForTesting(Imports.Instance, LoggerFactory) - .Set(AssemblyLoadContext.Default) .Set( new FoundationOptions { diff --git a/test/Sample.Core.Tests/HandleTestHostBase.cs b/test/Sample.Core.Tests/HandleTestHostBase.cs index cf681380b..bfd398abe 100644 --- a/test/Sample.Core.Tests/HandleTestHostBase.cs +++ b/test/Sample.Core.Tests/HandleTestHostBase.cs @@ -1,5 +1,4 @@ -using System.Runtime.Loader; -using Microsoft.Data.Sqlite; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -26,7 +25,6 @@ protected HandleTestHostBase(ITestOutputHelper outputHelper, LogLevel logLevel = ConventionContextBuilder .Create() .ForTesting(Imports.Instance, LoggerFactory) - .Set(AssemblyLoadContext.Default) .WithLogger(LoggerFactory.CreateLogger(nameof(AutoFakeTest))); ExcludeSourceContext(nameof(AutoFakeTest)); } diff --git a/test/Sample.Pages.Tests/FoundationTests.cs b/test/Sample.Pages.Tests/FoundationTests.cs index e7f03fff5..a4601d7c2 100644 --- a/test/Sample.Pages.Tests/FoundationTests.cs +++ b/test/Sample.Pages.Tests/FoundationTests.cs @@ -1,6 +1,9 @@ using System.Net; using AutoMapper; +using FakeItEasy; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Rocket.Surgery.LaunchPad.Hosting; using Sample.Pages.Tests.Helpers; namespace Sample.Pages.Tests; @@ -10,8 +13,9 @@ public class FoundationTests(ITestOutputHelper testOutputHelper, TestWebAppFixtu [Fact] public void AutoMapper() { - AlbaHost.Services.GetRequiredService() - .ConfigurationProvider.AssertConfigurationIsValid(); + AlbaHost + .Services.GetRequiredService() + .ConfigurationProvider.AssertConfigurationIsValid(); } [Fact] @@ -20,4 +24,28 @@ public async Task Starts() var response = await AlbaHost.Server.CreateClient().GetAsync("/"); response.StatusCode.Should().Be(HttpStatusCode.OK); } -} + + [Fact] + public async Task StartingEvents() + { + var builder = Host.CreateApplicationBuilder(); + var onStarted = A.Fake>(); + var onStarting = A.Fake>(); + var onStopping = A.Fake>(); + var onStopped = A.Fake>(); + builder.OnHostStarted(onStarted); + builder.OnHostStarting(onStarting); + builder.OnHostStopped(onStopped); + builder.OnHostStopping(onStopping); + builder.Services.AddHostedService(); + + var app = builder.Build(); + await app.StartAsync(); + await app.StopAsync(); + + A.CallTo(onStarted).MustHaveHappenedOnceExactly(); + A.CallTo(onStarting).MustHaveHappenedOnceExactly(); + A.CallTo(onStopped).MustHaveHappenedOnceExactly(); + A.CallTo(onStopping).MustHaveHappenedOnceExactly(); + } +} \ No newline at end of file