From b82f9ad9f782b8e469ececfb4336edaf7a0721f4 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Sat, 14 Feb 2026 20:21:09 -0600 Subject: [PATCH] Add session configuration methods to ancillary stores (GH-4131) AddMartenStore() now supports UseLightweightSessions(), UseIdentitySessions(), UseDirtyTrackedSessions(), and BuildSessionsWith() for configuring the ISessionFactory via keyed DI services, matching the existing AddMarten() API. Co-Authored-By: Claude Opus 4.6 --- docs/configuration/hostbuilder.md | 42 ++++++++ .../Examples/MultipleDocumentStores.cs | 29 ++++++ ...ping_with_service_collection_extensions.cs | 99 +++++++++++++++++++ .../MartenServiceCollectionExtensions.cs | 63 ++++++++++++ 4 files changed, 233 insertions(+) diff --git a/docs/configuration/hostbuilder.md b/docs/configuration/hostbuilder.md index a728d224de..5b0fd80e8c 100644 --- a/docs/configuration/hostbuilder.md +++ b/docs/configuration/hostbuilder.md @@ -711,3 +711,45 @@ public class InvoicingService ``` snippet source | anchor + +### Session Configuration for Ancillary Stores + +Just like the main store configured with `AddMarten()`, ancillary stores support fluent session configuration +to control the default `ISessionFactory` used for dependency-injected sessions. The session factory is registered +as a [keyed service](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#keyed-services) +keyed by the store marker interface type (e.g., `typeof(IInvoicingStore)`). + + + +```cs +using var host = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddMarten("some connection string"); + + services.AddMartenStore(opts => + { + opts.Connection("different connection string"); + }) + // Use lightweight sessions for this ancillary store + .UseLightweightSessions(); + + // Or use identity map sessions + // .UseIdentitySessions(); + + // Or use dirty-tracked sessions + // .UseDirtyTrackedSessions(); + + // Or use a custom session factory + // .BuildSessionsWith(); + }).StartAsync(); +``` +snippet source | anchor + + +You can resolve the keyed `ISessionFactory` for an ancillary store directly from the DI container if needed: + +```csharp +var factory = serviceProvider.GetRequiredKeyedService(typeof(IInvoicingStore)); +using var session = factory.OpenSession(); +``` diff --git a/src/CoreTests/Examples/MultipleDocumentStores.cs b/src/CoreTests/Examples/MultipleDocumentStores.cs index e250bb33c3..018650095b 100644 --- a/src/CoreTests/Examples/MultipleDocumentStores.cs +++ b/src/CoreTests/Examples/MultipleDocumentStores.cs @@ -50,6 +50,35 @@ public static async Task bootstrap() #endregion } + + public static async Task bootstrap_with_session_config() + { + #region sample_bootstrapping_separate_store_with_session_config + + using var host = Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddMarten("some connection string"); + + services.AddMartenStore(opts => + { + opts.Connection("different connection string"); + }) + // Use lightweight sessions for this ancillary store + .UseLightweightSessions(); + + // Or use identity map sessions + // .UseIdentitySessions(); + + // Or use dirty-tracked sessions + // .UseDirtyTrackedSessions(); + + // Or use a custom session factory + // .BuildSessionsWith(); + }).StartAsync(); + + #endregion + } } public class DefaultDataSet: IInitialData diff --git a/src/CoreTests/bootstrapping_with_service_collection_extensions.cs b/src/CoreTests/bootstrapping_with_service_collection_extensions.cs index c2a2c070fa..c73e16d866 100644 --- a/src/CoreTests/bootstrapping_with_service_collection_extensions.cs +++ b/src/CoreTests/bootstrapping_with_service_collection_extensions.cs @@ -8,6 +8,7 @@ using JasperFx.Core; using JasperFx.Core.Reflection; using Lamar; +using CoreTests.Examples; using Marten; using Marten.Events; using Marten.Internal.Sessions; @@ -482,6 +483,104 @@ Action GetStore(IServiceProvider c) => () => exc.Message.Contains("UseNpgsqlDataSource").ShouldBeTrue(); } + [Fact] + public void ancillary_store_use_lightweight_sessions() + { + var services = new ServiceCollection(); + services.AddMarten(ConnectionSource.ConnectionString); + services.AddMartenStore(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "invoicing"; + }).UseLightweightSessions(); + + var sp = services.BuildServiceProvider(); + var factory = sp.GetRequiredKeyedService(typeof(IInvoicingStore)); + factory.ShouldBeOfType(); + + using var session = factory.OpenSession(); + session.ShouldBeOfType(); + } + + [Fact] + public void ancillary_store_use_identity_sessions() + { + var services = new ServiceCollection(); + services.AddMarten(ConnectionSource.ConnectionString); + services.AddMartenStore(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "invoicing"; + }).UseIdentitySessions(); + + var sp = services.BuildServiceProvider(); + var factory = sp.GetRequiredKeyedService(typeof(IInvoicingStore)); + factory.ShouldBeOfType(); + + using var session = factory.OpenSession(); + session.ShouldBeOfType(); + } + + [Fact] + public void ancillary_store_use_dirty_tracked_sessions() + { + var services = new ServiceCollection(); + services.AddMarten(ConnectionSource.ConnectionString); + services.AddMartenStore(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "invoicing"; + }).UseDirtyTrackedSessions(); + + var sp = services.BuildServiceProvider(); + var factory = sp.GetRequiredKeyedService(typeof(IInvoicingStore)); + factory.ShouldBeOfType(); + + using var session = factory.OpenSession(); + session.ShouldBeOfType(); + } + + [Fact] + public void ancillary_store_build_sessions_with_custom_factory() + { + var services = new ServiceCollection(); + services.AddMarten(ConnectionSource.ConnectionString); + services.AddMartenStore(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "invoicing"; + }).BuildSessionsWith(); + + var sp = services.BuildServiceProvider(); + var factory = sp.GetRequiredKeyedService(typeof(IInvoicingStore)); + var builder = factory.ShouldBeOfType(); + + builder.BuiltQuery.ShouldBeFalse(); + builder.BuiltSession.ShouldBeFalse(); + + using var session = factory.OpenSession(); + builder.BuiltSession.ShouldBeTrue(); + + using var query = factory.QuerySession(); + builder.BuiltQuery.ShouldBeTrue(); + } + + [Fact] + public void ancillary_store_default_session_factory() + { + var services = new ServiceCollection(); + services.AddMarten(ConnectionSource.ConnectionString); + services.AddMartenStore(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = "invoicing"; + }); + + var sp = services.BuildServiceProvider(); + var factory = sp.GetRequiredKeyedService(typeof(IInvoicingStore)); + factory.ShouldBeOfType(); + } + public class SpecialBuilder: ISessionFactory { private readonly IDocumentStore _store; diff --git a/src/Marten/MartenServiceCollectionExtensions.cs b/src/Marten/MartenServiceCollectionExtensions.cs index 957dd37c5d..c71e3185a3 100644 --- a/src/Marten/MartenServiceCollectionExtensions.cs +++ b/src/Marten/MartenServiceCollectionExtensions.cs @@ -314,6 +314,13 @@ public static MartenStoreExpression AddMartenStore(this IServiceCollection services.AddSingleton(s => config.Build(s)); + // Default keyed session factory for the ancillary store + services.AddKeyedSingleton(typeof(T), (sp, _) => + { + var store = (IDocumentStore)sp.GetRequiredService(); + var logger = sp.GetService>() ?? new NullLogger(); + return new DefaultSessionFactory(store, logger); + }); services.AddSingleton(s => (IMasterTableMultiTenancy)s.GetRequiredService()); @@ -602,6 +609,62 @@ public MartenStoreExpression AddSubscriptionWithServices(Servi return this; } + + /// + /// Use an alternative strategy / configuration for opening IDocumentSession or IQuerySession + /// objects for this ancillary store with a custom ISessionFactory type registered as a keyed singleton + /// + /// + /// IoC service lifetime for the session factory. Default is Singleton, but use Scoped if you need + /// to reference per-scope services + /// + /// The custom session factory type + /// + public MartenStoreExpression BuildSessionsWith(ServiceLifetime lifetime = ServiceLifetime.Singleton) + where TFactory : class, ISessionFactory + { + var descriptor = new ServiceDescriptor( + typeof(ISessionFactory), + typeof(T), + (sp, _) => + { + var store = sp.GetRequiredService(); + return ActivatorUtilities.CreateInstance(sp, (IDocumentStore)store); + }, + lifetime); + Services.Add(descriptor); + return this; + } + + /// + /// Use lightweight sessions by default for this ancillary store. Equivalent to + /// IDocumentStore.LightweightSession(); + /// + /// + public MartenStoreExpression UseLightweightSessions() + { + return BuildSessionsWith(); + } + + /// + /// Use identity sessions by default for this ancillary store. Equivalent to + /// IDocumentStore.IdentitySession(); + /// + /// + public MartenStoreExpression UseIdentitySessions() + { + return BuildSessionsWith(); + } + + /// + /// Use dirty-tracked sessions by default for this ancillary store. Equivalent to + /// IDocumentStore.DirtyTrackedSession(); + /// + /// + public MartenStoreExpression UseDirtyTrackedSessions() + { + return BuildSessionsWith(); + } } public class MartenConfigurationExpression