From 389765c49929ac0fddf733a7f4ba9cf89deea2da Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Wed, 10 Apr 2024 15:06:02 -0500 Subject: [PATCH] Asynchronous version of configure marten to enable FeatureManagement usage. Closes GH-3133 --- docs/configuration/hostbuilder.md | 71 +++++++++++- src/CoreTests/CoreTests.csproj | 1 + ...onfiguring_marten_with_async_extensions.cs | 103 ++++++++++++++++++ .../MartenServiceCollectionExtensions.cs | 82 +++++++++++++- 4 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 src/CoreTests/configuring_marten_with_async_extensions.cs diff --git a/docs/configuration/hostbuilder.md b/docs/configuration/hostbuilder.md index 2e212e8cca..6518159bdd 100644 --- a/docs/configuration/hostbuilder.md +++ b/docs/configuration/hostbuilder.md @@ -42,9 +42,10 @@ At runtime, when your application needs to resolve `IDocumentStore` for the firs 1. Resolve a `StoreOptions` object from the initial `AddMarten()` configuration 2. Apply all registered `IConfigureMarten` services to alter that `StoreOptions` object -3. Reads the `IHostEnvironment` for the application if it exists to try to determine the main application assembly and paths for generated code output -4. Attaches any `IInitialData` services that were registered in the IoC container to the `StoreOptions` object -5. *Finally*, Marten builds a new `DocumentStore` object using the now configured `StoreOptions` object +3. Apply all registered `IAsyncConfigureMarten` services to alter that `StoreOptions` object +4. Reads the `IHostEnvironment` for the application if it exists to try to determine the main application assembly and paths for generated code output +5. Attaches any `IInitialData` services that were registered in the IoC container to the `StoreOptions` object +6. *Finally*, Marten builds a new `DocumentStore` object using the now configured `StoreOptions` object This model is comparable to the .Net `IOptions` model. @@ -211,7 +212,7 @@ public interface IConfigureMarten void Configure(IServiceProvider services, StoreOptions options); } ``` -snippet source | anchor +snippet source | anchor You could alternatively implement a custom `IConfigureMarten` (or `IConfigureMarten where T : IDocumentStore` if you're [working with multiple databases](#working-with-multiple-marten-databases)) class like so: @@ -254,7 +255,67 @@ public static IServiceCollection AddUserModule2(this IServiceCollection services snippet source | anchor -## Using Lightweight Sessions +### Using IoC Services for Configuring Marten + +There is also a newer mechanism called `IAsyncConfigureMarten` that was originally built to enable services +like the [Feature Management library from Microsoft](https://learn.microsoft.com/en-us/azure/azure-app-configuration/use-feature-flags-dotnet-core) to +be used to selectively configure Marten using potentially asynchronous methods and IoC resolved services. + +That interface signature is: + + + +```cs +/// +/// Mechanism to register additional Marten configuration that is applied after AddMarten() +/// configuration, but before DocumentStore is initialized when you need to utilize some +/// kind of asynchronous services like Microsoft's FeatureManagement feature to configure Marten +/// +public interface IAsyncConfigureMarten +{ + ValueTask Configure(StoreOptions options, CancellationToken cancellationToken); +} +``` +snippet source | anchor + + +As an example from the tests, here's a custom version that uses the Feature Management service: + + + +```cs +public class FeatureManagementUsingExtension: IAsyncConfigureMarten +{ + private readonly IFeatureManager _manager; + + public FeatureManagementUsingExtension(IFeatureManager manager) + { + _manager = manager; + } + + public async ValueTask Configure(StoreOptions options, CancellationToken cancellationToken) + { + if (await _manager.IsEnabledAsync("Module1")) + { + options.Events.MapEventType("module1:event"); + } + } +} +``` +snippet source | anchor + + +And lastly, these extensions can be registered directly against `IServiceCollection` like so: + + + +```cs +services.ConfigureMartenWithServices(); +``` +snippet source | anchor + + +## Using Lightweight Sessions ::: tip Most usages of Marten should default to the lightweight sessions for better performance diff --git a/src/CoreTests/CoreTests.csproj b/src/CoreTests/CoreTests.csproj index 6df66a292e..d744366ffb 100644 --- a/src/CoreTests/CoreTests.csproj +++ b/src/CoreTests/CoreTests.csproj @@ -20,6 +20,7 @@ + diff --git a/src/CoreTests/configuring_marten_with_async_extensions.cs b/src/CoreTests/configuring_marten_with_async_extensions.cs new file mode 100644 index 0000000000..1fd469c5bd --- /dev/null +++ b/src/CoreTests/configuring_marten_with_async_extensions.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JasperFx.Core; +using Marten; +using Marten.Testing.Harness; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.FeatureManagement; +using NSubstitute; +using Shouldly; +using Xunit; + +namespace CoreTests; + +public class configuring_marten_with_async_extensions +{ + [Fact] + public async Task feature_flag_positive() + { + var featureManager = Substitute.For(); + featureManager.IsEnabledAsync("Module1").Returns(true); + + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddMarten(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "async_config"; + }).ApplyAllDatabaseChangesOnStartup(); + + #region sample_registering_async_config_marten + + services.ConfigureMartenWithServices(); + + #endregion + services.AddSingleton(featureManager); + }).StartAsync(); + + var store = (DocumentStore)host.Services.GetRequiredService(); + + store.Events.EventMappingFor() + .Alias.ShouldBe("module1:event"); + + } + + [Fact] + public async Task feature_flag_negative() + { + var featureManager = Substitute.For(); + + featureManager.IsEnabledAsync("Module1").Returns(false); + + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddMarten(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "async_config"; + }); + + services.ConfigureMartenWithServices(); + services.AddSingleton(featureManager); + }).StartAsync(); + + var store = (DocumentStore)host.Services.GetRequiredService(); + + store.Events.EventMappingFor() + .Alias.ShouldBe("module_1_event"); + + } +} + +#region sample_FeatureManagementUsingExtension + +public class FeatureManagementUsingExtension: IAsyncConfigureMarten +{ + private readonly IFeatureManager _manager; + + public FeatureManagementUsingExtension(IFeatureManager manager) + { + _manager = manager; + } + + public async ValueTask Configure(StoreOptions options, CancellationToken cancellationToken) + { + if (await _manager.IsEnabledAsync("Module1")) + { + options.Events.MapEventType("module1:event"); + } + } +} + +#endregion + +public class Module1Event +{ + +} + diff --git a/src/Marten/MartenServiceCollectionExtensions.cs b/src/Marten/MartenServiceCollectionExtensions.cs index 8216f8c766..666a8a32c2 100644 --- a/src/Marten/MartenServiceCollectionExtensions.cs +++ b/src/Marten/MartenServiceCollectionExtensions.cs @@ -5,6 +5,8 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using JasperFx.CodeGeneration; using JasperFx.Core; using JasperFx.Core.Reflection; @@ -28,6 +30,20 @@ namespace Marten; public static class MartenServiceCollectionExtensions { + /// + /// Apply additional configuration to a Marten DocumentStore. This is applied *after* + /// AddMarten(), but before the DocumentStore is initialized + /// + /// + /// + /// + public static IServiceCollection ConfigureMartenWithServices(this IServiceCollection services) where T : class, IAsyncConfigureMarten + { + services.EnsureAsyncConfigureMartenApplicationIsRegistered(); + services.AddSingleton(); + return services; + } + /// /// Apply additional configuration to a Marten DocumentStore. This is applied *after* /// AddMarten(), but before the DocumentStore is initialized @@ -301,13 +317,36 @@ public static IReadOnlyList AllDocumentStores(this IServiceProvi return list; } + internal static void EnsureAsyncConfigureMartenApplicationIsRegistered(this IServiceCollection services) + { + if (!services.Any( + x => x.ServiceType == typeof(IHostedService) && x.ImplementationType == typeof(AsyncConfigureMartenApplication))) + { + services.Insert(0, + new ServiceDescriptor(typeof(IHostedService), typeof(AsyncConfigureMartenApplication), ServiceLifetime.Singleton)); + } + } + internal static void EnsureMartenActivatorIsRegistered(this IServiceCollection services) { if (!services.Any( x => x.ServiceType == typeof(IHostedService) && x.ImplementationType == typeof(MartenActivator))) { - services.Insert(0, - new ServiceDescriptor(typeof(IHostedService), typeof(MartenActivator), ServiceLifetime.Singleton)); + var descriptor = services.FirstOrDefault(x => + x.ServiceType == typeof(IHostedService) && + x.ImplementationType == typeof(AsyncConfigureMartenApplication)); + + if (descriptor != null) + { + var index = services.IndexOf(descriptor); + services.Insert(index + 1, + new ServiceDescriptor(typeof(IHostedService), typeof(MartenActivator), ServiceLifetime.Singleton)); + } + else + { + services.Insert(0, + new ServiceDescriptor(typeof(IHostedService), typeof(MartenActivator), ServiceLifetime.Singleton)); + } } } @@ -865,6 +904,45 @@ public interface IConfigureMarten #endregion +#region sample_IAsyncConfigureMarten + +/// +/// Mechanism to register additional Marten configuration that is applied after AddMarten() +/// configuration, but before DocumentStore is initialized when you need to utilize some +/// kind of asynchronous services like Microsoft's FeatureManagement feature to configure Marten +/// +public interface IAsyncConfigureMarten +{ + ValueTask Configure(StoreOptions options, CancellationToken cancellationToken); +} + +#endregion + +internal class AsyncConfigureMartenApplication: IHostedService +{ + private readonly IList _configures; + private readonly StoreOptions _options; + + public AsyncConfigureMartenApplication(IEnumerable configures, StoreOptions options) + { + _configures = configures.ToList(); + _options = options; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + foreach (var configure in _configures) + { + await configure.Configure(_options, cancellationToken).ConfigureAwait(false); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} + internal class LambdaConfigureMarten: IConfigureMarten { private readonly Action _configure;