Skip to content

Commit

Permalink
Asynchronous version of configure marten to enable FeatureManagement …
Browse files Browse the repository at this point in the history
…usage. Closes GH-3133
  • Loading branch information
jeremydmiller committed Apr 10, 2024
1 parent 4e1f28c commit 389765c
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 7 deletions.
71 changes: 66 additions & 5 deletions docs/configuration/hostbuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -211,7 +212,7 @@ public interface IConfigureMarten
void Configure(IServiceProvider services, StoreOptions options);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/MartenServiceCollectionExtensions.cs#L855-L866' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_iconfiguremarten' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/MartenServiceCollectionExtensions.cs#L894-L905' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_iconfiguremarten' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

You could alternatively implement a custom `IConfigureMarten` (or `IConfigureMarten<T> where T : IDocumentStore` if you're [working with multiple databases](#working-with-multiple-marten-databases)) class like so:
Expand Down Expand Up @@ -254,7 +255,67 @@ public static IServiceCollection AddUserModule2(this IServiceCollection services
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CoreTests/BootstrappingExamples.cs#L36-L53' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_addusermodule2' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Using Lightweight Sessions
### Using IoC Services for Configuring Marten <Badge type="tip" text="7.7" />

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:

<!-- snippet: sample_IAsyncConfigureMarten -->
<a id='snippet-sample_iasyncconfiguremarten'></a>
```cs
/// <summary>
/// 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
/// </summary>
public interface IAsyncConfigureMarten
{
ValueTask Configure(StoreOptions options, CancellationToken cancellationToken);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/Marten/MartenServiceCollectionExtensions.cs#L907-L919' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_iasyncconfiguremarten' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

As an example from the tests, here's a custom version that uses the Feature Management service:

<!-- snippet: sample_FeatureManagementUsingExtension -->
<a id='snippet-sample_featuremanagementusingextension'></a>
```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<Module1Event>("module1:event");
}
}
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CoreTests/configuring_marten_with_async_extensions.cs#L77-L97' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_featuremanagementusingextension' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And lastly, these extensions can be registered directly against `IServiceCollection` like so:

<!-- snippet: sample_registering_async_config_marten -->
<a id='snippet-sample_registering_async_config_marten'></a>
```cs
services.ConfigureMartenWithServices<FeatureManagementUsingExtension>();
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/CoreTests/configuring_marten_with_async_extensions.cs#L34-L38' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_registering_async_config_marten' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Using Lightweight Sessions

::: tip
Most usages of Marten should default to the lightweight sessions for better performance
Expand Down
1 change: 1 addition & 0 deletions src/CoreTests/CoreTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Jil" Version="3.0.0-alpha2" />
<PackageReference Include="Microsoft.FeatureManagement" Version="3.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Npgsql.DependencyInjection" Version="8.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
Expand Down
103 changes: 103 additions & 0 deletions src/CoreTests/configuring_marten_with_async_extensions.cs
Original file line number Diff line number Diff line change
@@ -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<IFeatureManager>();
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<FeatureManagementUsingExtension>();

#endregion
services.AddSingleton(featureManager);
}).StartAsync();

var store = (DocumentStore)host.Services.GetRequiredService<IDocumentStore>();

store.Events.EventMappingFor<Module1Event>()
.Alias.ShouldBe("module1:event");

}

[Fact]
public async Task feature_flag_negative()
{
var featureManager = Substitute.For<IFeatureManager>();

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<FeatureManagementUsingExtension>();
services.AddSingleton(featureManager);
}).StartAsync();

var store = (DocumentStore)host.Services.GetRequiredService<IDocumentStore>();

store.Events.EventMappingFor<Module1Event>()
.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<Module1Event>("module1:event");
}
}
}

#endregion

public class Module1Event
{

}

82 changes: 80 additions & 2 deletions src/Marten/MartenServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +30,20 @@ namespace Marten;

public static class MartenServiceCollectionExtensions
{
/// <summary>
/// Apply additional configuration to a Marten DocumentStore. This is applied *after*
/// AddMarten(), but before the DocumentStore is initialized
/// </summary>
/// <param name="services"></param>
/// <param name="configure"></param>
/// <returns></returns>
public static IServiceCollection ConfigureMartenWithServices<T>(this IServiceCollection services) where T : class, IAsyncConfigureMarten
{
services.EnsureAsyncConfigureMartenApplicationIsRegistered();
services.AddSingleton<IAsyncConfigureMarten, T>();
return services;
}

/// <summary>
/// Apply additional configuration to a Marten DocumentStore. This is applied *after*
/// AddMarten(), but before the DocumentStore is initialized
Expand Down Expand Up @@ -301,13 +317,36 @@ public static IReadOnlyList<IDocumentStore> 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));
}
}
}

Expand Down Expand Up @@ -865,6 +904,45 @@ public interface IConfigureMarten

#endregion

#region sample_IAsyncConfigureMarten

/// <summary>
/// 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
/// </summary>
public interface IAsyncConfigureMarten
{
ValueTask Configure(StoreOptions options, CancellationToken cancellationToken);
}

#endregion

internal class AsyncConfigureMartenApplication: IHostedService
{
private readonly IList<IAsyncConfigureMarten> _configures;
private readonly StoreOptions _options;

public AsyncConfigureMartenApplication(IEnumerable<IAsyncConfigureMarten> 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<IServiceProvider, StoreOptions> _configure;
Expand Down

0 comments on commit 389765c

Please sign in to comment.