diff --git a/src/BuildingBlocks/Eventing.Abstractions/IEventTenantScope.cs b/src/BuildingBlocks/Eventing.Abstractions/IEventTenantScope.cs
index acfe796a1e..484ee6e66e 100644
--- a/src/BuildingBlocks/Eventing.Abstractions/IEventTenantScope.cs
+++ b/src/BuildingBlocks/Eventing.Abstractions/IEventTenantScope.cs
@@ -24,4 +24,18 @@ public interface IEventTenantScope
/// ambient context unchanged (global, non-tenant-scoped events).
///
IDisposable Begin(string? tenantId);
+
+ ///
+ /// Runs inside the tenant scope for . Implementations
+ /// that resolve tenant metadata asynchronously override this so resolution and the action share one
+ /// async flow. The default bridges to .
+ ///
+ async Task ExecuteAsync(string? tenantId, Func action, CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+ using (Begin(tenantId))
+ {
+ await action(ct).ConfigureAwait(false);
+ }
+ }
}
diff --git a/src/BuildingBlocks/Eventing/InMemory/InMemoryEventBus.cs b/src/BuildingBlocks/Eventing/InMemory/InMemoryEventBus.cs
index 24afdae0c6..201a46f0ec 100644
--- a/src/BuildingBlocks/Eventing/InMemory/InMemoryEventBus.cs
+++ b/src/BuildingBlocks/Eventing/InMemory/InMemoryEventBus.cs
@@ -62,7 +62,7 @@ private async Task PublishSingleAsync(IIntegrationEvent @event, CancellationToke
// Set tenant context BEFORE resolving handlers — MultiTenantDbContext captures TenantInfo at
// construction, so a late tenant NREs the query filter. This is what makes background publishers work.
- using (_tenantScope.Begin(@event.TenantId))
+ await _tenantScope.ExecuteAsync(@event.TenantId, async _ =>
{
// One failing handler must not starve the others: every handler gets its turn, and the failure
// surfaces afterwards so the outbox retries the event. The inbox records each handler that
@@ -105,7 +105,7 @@ private async Task PublishSingleAsync(IIntegrationEvent @event, CancellationToke
}
ThrowIfAnyFailed(failures, @event.Id);
- }
+ }, ct).ConfigureAwait(false);
}
private static void ThrowIfAnyFailed(List? failures, Guid eventId)
diff --git a/src/Modules/Multitenancy/Modules.Multitenancy/Services/FinbuckleEventTenantScope.cs b/src/Modules/Multitenancy/Modules.Multitenancy/Services/FinbuckleEventTenantScope.cs
index 5fb7646395..f5372c2839 100644
--- a/src/Modules/Multitenancy/Modules.Multitenancy/Services/FinbuckleEventTenantScope.cs
+++ b/src/Modules/Multitenancy/Modules.Multitenancy/Services/FinbuckleEventTenantScope.cs
@@ -1,7 +1,12 @@
using Finbuckle.MultiTenant;
using Finbuckle.MultiTenant.Abstractions;
+using Finbuckle.MultiTenant.EntityFrameworkCore.Stores;
+using Finbuckle.MultiTenant.Stores;
using FSH.Framework.Eventing.Abstractions;
using FSH.Framework.Shared.Multitenancy;
+using FSH.Modules.Multitenancy.Data;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace FSH.Modules.Multitenancy.Services;
@@ -11,22 +16,26 @@ namespace FSH.Modules.Multitenancy.Services;
/// handler DbContexts resolved afterwards capture a real TenantInfo instead of the
/// null one a background scope would otherwise carry.
///
-/// Mirrors the create-scope-then-set-tenant pattern already used by
-/// WebhookDispatchJob / SqlAuditSink, generalized to the event pipeline.
-/// Only tenant identity is set (sufficient for the row-level tenant query filter in the
-/// shared-database model); per-tenant connection strings are not resolved here.
+/// resolves the full tenant (including a dedicated connection string)
+/// before the action constructs consumers, in the same async flow.
///
public sealed class FinbuckleEventTenantScope : IEventTenantScope
{
private readonly IMultiTenantContextAccessor _accessor;
private readonly IMultiTenantContextSetter _setter;
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly ILogger _logger;
public FinbuckleEventTenantScope(
IMultiTenantContextAccessor accessor,
- IMultiTenantContextSetter setter)
+ IMultiTenantContextSetter setter,
+ IServiceScopeFactory scopeFactory,
+ ILogger logger)
{
_accessor = accessor;
_setter = setter;
+ _scopeFactory = scopeFactory;
+ _logger = logger;
}
public IDisposable Begin(string? tenantId)
@@ -44,6 +53,63 @@ public IDisposable Begin(string? tenantId)
return new RestoreScope(_setter, previous);
}
+ public async Task ExecuteAsync(
+ string? tenantId,
+ Func action,
+ CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ var previous = _accessor.MultiTenantContext;
+ if (string.IsNullOrWhiteSpace(tenantId) ||
+ (string.Equals(previous.TenantInfo?.Id, tenantId, StringComparison.Ordinal) &&
+ !string.IsNullOrWhiteSpace(previous.TenantInfo?.ConnectionString)))
+ {
+ await action(ct).ConfigureAwait(false);
+ return;
+ }
+
+ try
+ {
+ var tenant = await FindTenantAsync(tenantId, ct).ConfigureAwait(false);
+ if (tenant is null || !tenant.IsActive)
+ {
+ _logger.LogWarning("Event tenant {TenantId} is missing or inactive; dispatching with tenant identity only", tenantId);
+ tenant = new AppTenantInfo(tenantId, tenantId);
+ }
+
+ _setter.MultiTenantContext = new MultiTenantContext(tenant);
+ await action(ct).ConfigureAwait(false);
+ }
+ finally
+ {
+ _setter.MultiTenantContext = previous;
+ }
+ }
+
+ private async Task FindTenantAsync(string tenantId, CancellationToken ct)
+ {
+ // A single IMultiTenantStore resolve returns only the last registration (the EF store), so enumerate all of them.
+ await using var scope = _scopeFactory.CreateAsyncScope();
+ var stores = scope.ServiceProvider.GetServices>().ToList();
+ var cache = stores.OfType>().Single();
+
+ var tenant = await cache.GetAsync(tenantId).WaitAsync(ct).ConfigureAwait(false);
+ if (tenant is not null)
+ {
+ return tenant;
+ }
+
+ var efStore = stores.OfType>().Single();
+ tenant = await efStore.GetAsync(tenantId).WaitAsync(ct).ConfigureAwait(false);
+ if (tenant is not null)
+ {
+ await cache.AddAsync(tenant).ConfigureAwait(false);
+ }
+
+ return tenant;
+ }
+
private sealed class RestoreScope : IDisposable
{
private readonly IMultiTenantContextSetter _setter;
diff --git a/src/Modules/Webhooks/Modules.Webhooks/Services/WebhookFanoutHandler.cs b/src/Modules/Webhooks/Modules.Webhooks/Services/WebhookFanoutHandler.cs
index 46806263cf..62fdc2104e 100644
--- a/src/Modules/Webhooks/Modules.Webhooks/Services/WebhookFanoutHandler.cs
+++ b/src/Modules/Webhooks/Modules.Webhooks/Services/WebhookFanoutHandler.cs
@@ -59,9 +59,13 @@ public async Task HandleAsync(TEvent @event, CancellationToken ct = default)
var prev = _tenantContextAccessor.MultiTenantContext;
try
{
- var info = new AppTenantInfo(@event.TenantId, @event.TenantId);
- ((IMultiTenantContextSetter)_tenantContextAccessor).MultiTenantContext =
- new MultiTenantContext(info);
+ // Keep a complete context (e.g. a dedicated-database connection) the bus already resolved.
+ if (prev.TenantInfo?.Id != @event.TenantId || string.IsNullOrWhiteSpace(prev.TenantInfo.ConnectionString))
+ {
+ var info = new AppTenantInfo(@event.TenantId, @event.TenantId);
+ ((IMultiTenantContextSetter)_tenantContextAccessor).MultiTenantContext =
+ new MultiTenantContext(info);
+ }
var eventType = typeof(TEvent).Name;
diff --git a/src/Tests/Integration.Tests/Tests/Eventing/DedicatedTenantRoutingTests.cs b/src/Tests/Integration.Tests/Tests/Eventing/DedicatedTenantRoutingTests.cs
new file mode 100644
index 0000000000..de0e572493
--- /dev/null
+++ b/src/Tests/Integration.Tests/Tests/Eventing/DedicatedTenantRoutingTests.cs
@@ -0,0 +1,308 @@
+using Finbuckle.MultiTenant;
+using Finbuckle.MultiTenant.Abstractions;
+using Finbuckle.MultiTenant.EntityFrameworkCore.Stores;
+using Finbuckle.MultiTenant.Extensions;
+using Finbuckle.MultiTenant.Stores;
+using FSH.Framework.Eventing;
+using FSH.Framework.Eventing.Abstractions;
+using FSH.Framework.Eventing.InMemory;
+using FSH.Framework.Eventing.Outbox;
+using FSH.Framework.Eventing.Persistence;
+using FSH.Framework.Persistence;
+using FSH.Framework.Shared.Multitenancy;
+using FSH.Modules.Catalog.Data;
+using FSH.Modules.Catalog.Domain;
+using FSH.Modules.Files.Contracts.Events;
+using FSH.Modules.Multitenancy.Data;
+using FSH.Modules.Multitenancy.Services;
+using FSH.Modules.Webhooks.Data;
+using FSH.Modules.Webhooks.Domain;
+using FSH.Modules.Webhooks.Services;
+using Integration.Tests.Infrastructure;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Npgsql;
+using System.Collections.Concurrent;
+
+namespace Integration.Tests.Tests.Eventing;
+
+[Collection(FshCollectionDefinition.Name)]
+public sealed class DedicatedTenantRoutingTests(FshWebApplicationFactory factory)
+{
+ [Fact]
+ public async Task DispatchAsync_Should_WriteConsumerAndInboxOnlyToDedicatedDatabase()
+ {
+ await using var environment = await RoutingEnvironment.CreateAsync(factory.Services, false);
+ var integrationEvent = NewEvent(environment.Tenant.Id);
+ environment.EventIds.Add(integrationEvent.Id);
+
+ await environment.DispatchAsync(integrationEvent);
+
+ DatabaseSnapshot dedicated = await ReadSnapshotAsync(environment.DedicatedConnection, integrationEvent);
+ DatabaseSnapshot shared = await ReadSnapshotAsync(environment.DefaultConnection, integrationEvent);
+ dedicated.ConsumerRows.ShouldBe(1);
+ dedicated.InboxRows.ShouldBe(1);
+ shared.ConsumerRows.ShouldBe(0);
+ shared.InboxRows.ShouldBe(0);
+ }
+
+ [Fact]
+ public async Task DispatchAsync_Should_LookUpWebhookSubscriptionsInDedicatedDatabase()
+ {
+ await using var environment = await RoutingEnvironment.CreateAsync(factory.Services, true);
+ var integrationEvent = NewEvent(environment.Tenant.Id);
+ environment.EventIds.Add(integrationEvent.Id);
+ Guid expected = await environment.SeedSubscriptionAsync();
+
+ await environment.DispatchAsync(integrationEvent);
+
+ environment.Recorder.Records.ShouldHaveSingleItem().ShouldBe((environment.Tenant.Id, expected));
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ExecuteAsync_Should_RestorePreviousContext_AfterSuccessOrFailure(bool fail)
+ {
+ var services = new ServiceCollection();
+ services.AddMultiTenant().WithDistributedCacheStore(TimeSpan.FromMinutes(60));
+ services.AddDistributedMemoryCache();
+ using ServiceProvider provider = services.BuildServiceProvider();
+ var accessor = provider.GetRequiredService>();
+ var setter = provider.GetRequiredService();
+ var previous = new MultiTenantContext(new AppTenantInfo("source", "source", "source-db", "test@example.test"));
+ setter.MultiTenantContext = previous;
+ var tenant = new AppTenantInfo("target", "target", "target-db", "test@example.test");
+ var store = provider.GetServices>().OfType>().Single();
+ (await store.AddAsync(tenant)).ShouldBeTrue();
+ var scope = new FinbuckleEventTenantScope(accessor, setter,
+ provider.GetRequiredService(), NullLogger.Instance);
+
+ async Task DispatchAsync() => await scope.ExecuteAsync("target", async _ =>
+ {
+ await Task.Yield();
+ accessor.MultiTenantContext.TenantInfo.ShouldNotBeNull().Id.ShouldBe(tenant.Id);
+ accessor.MultiTenantContext.TenantInfo.ConnectionString.ShouldBe(tenant.ConnectionString);
+ if (fail) throw new InvalidOperationException("handler failed");
+ });
+
+ if (fail) await Should.ThrowAsync(DispatchAsync);
+ else await DispatchAsync();
+ accessor.MultiTenantContext.ShouldBeSameAs(previous);
+ }
+
+ private static FileFinalizedIntegrationEvent NewEvent(string tenantId)
+ {
+ Guid id = Guid.CreateVersion7();
+ return new(id, DateTime.UtcNow, tenantId, $"corr-{id:N}", "Files", Guid.CreateVersion7(), "Test", null,
+ "application/octet-stream", 1, 1);
+ }
+
+ private static async Task ReadSnapshotAsync(string connectionString, FileFinalizedIntegrationEvent integrationEvent)
+ {
+ await using var connection = new NpgsqlConnection(connectionString);
+ await connection.OpenAsync();
+ await using var command = new NpgsqlCommand("""
+ SELECT
+ (SELECT COUNT(*) FROM catalog."Brands" WHERE "Name" = @marker),
+ (SELECT COUNT(*) FROM framework."InboxMessages" WHERE "Id" = @event AND "HandlerName" = @handler)
+ """, connection);
+ command.Parameters.AddWithValue("marker", $"dispatch-{integrationEvent.Id:N}");
+ command.Parameters.AddWithValue("event", integrationEvent.Id);
+ command.Parameters.AddWithValue("handler", typeof(FileEventMarkerHandler).FullName!);
+ await using var reader = await command.ExecuteReaderAsync();
+ (await reader.ReadAsync()).ShouldBeTrue();
+ return new DatabaseSnapshot(reader.GetInt64(0), reader.GetInt64(1));
+ }
+
+ private sealed record DatabaseSnapshot(long ConsumerRows, long InboxRows);
+
+ private sealed class RoutingEnvironment : IAsyncDisposable
+ {
+ private readonly string _databaseName;
+ private bool _databaseCreated;
+
+ private RoutingEnvironment(ServiceProvider provider, string defaultConnection, string dedicatedConnection,
+ string databaseName, AppTenantInfo tenant, WebhookRecorder recorder)
+ {
+ Provider = provider;
+ DefaultConnection = defaultConnection;
+ DedicatedConnection = dedicatedConnection;
+ _databaseName = databaseName;
+ Tenant = tenant;
+ Recorder = recorder;
+ }
+
+ public ServiceProvider Provider { get; }
+ public string DefaultConnection { get; }
+ public string DedicatedConnection { get; }
+ public AppTenantInfo Tenant { get; }
+ public WebhookRecorder Recorder { get; }
+ public List EventIds { get; } = [];
+ public EventingDrainTarget DedicatedTarget => new(Tenant.Id, DedicatedConnection);
+
+ public static async Task CreateAsync(IServiceProvider host, bool webhooks)
+ {
+ string defaultConnection = host.GetRequiredService>().Value.ConnectionString;
+ string suffix = Guid.CreateVersion7().ToString("N");
+ string databaseName = $"routing_{suffix}";
+ string dedicatedConnection = new NpgsqlConnectionStringBuilder(defaultConnection) { Database = databaseName }.ConnectionString;
+ var tenant = new AppTenantInfo($"tenant-{suffix}", $"tenant-{suffix}", dedicatedConnection, "routing@example.test");
+ var recorder = new WebhookRecorder();
+ ServiceProvider provider = CreateProvider(host, defaultConnection, recorder, webhooks);
+ var environment = new RoutingEnvironment(provider, defaultConnection, dedicatedConnection, databaseName, tenant, recorder);
+ try
+ {
+ await ExecuteDatabaseCommandAsync(defaultConnection, $"CREATE DATABASE {QuoteIdentifier(databaseName)}");
+ environment._databaseCreated = true;
+ using (provider.GetRequiredService().Begin(environment.DedicatedTarget))
+ using (var scope = provider.CreateScope())
+ {
+ await scope.ServiceProvider.GetRequiredService().Database.MigrateAsync();
+ await scope.ServiceProvider.GetRequiredService().Database.MigrateAsync();
+ if (webhooks) await scope.ServiceProvider.GetRequiredService().Database.MigrateAsync();
+ }
+ using (var scope = provider.CreateScope())
+ {
+ (await scope.ServiceProvider.GetRequiredService>().AddAsync(tenant)).ShouldBeTrue();
+ }
+ return environment;
+ }
+ catch
+ {
+ await environment.DisposeAsync();
+ throw;
+ }
+ }
+
+ public async Task DispatchAsync(IIntegrationEvent integrationEvent)
+ {
+ using (Provider.GetRequiredService().Begin(new EventingDrainTarget(MultitenancyConstants.Root.Id, null)))
+ using (var scope = Provider.CreateScope())
+ {
+ await scope.ServiceProvider.GetRequiredService().AddAsync(integrationEvent);
+ await scope.ServiceProvider.GetRequiredService().DispatchAsync();
+ }
+ }
+
+ public async Task SeedSubscriptionAsync()
+ {
+ using (Provider.GetRequiredService().Begin(DedicatedTarget))
+ using (var scope = Provider.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService();
+ var subscription = WebhookSubscription.Create("https://routing.example.test/hook", [nameof(FileFinalizedIntegrationEvent)], "test-hash");
+ context.Subscriptions.Add(subscription);
+ await context.SaveChangesAsync();
+ return subscription.Id;
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ using (var scope = Provider.CreateScope())
+ {
+ await scope.ServiceProvider.GetRequiredService>().RemoveAsync(Tenant.Id);
+ }
+ await using var connection = new NpgsqlConnection(DefaultConnection);
+ await connection.OpenAsync();
+ await using var command = new NpgsqlCommand("""
+ DELETE FROM catalog."Brands" WHERE "Name" = ANY(@markers);
+ DELETE FROM framework."InboxMessages" WHERE "Id" = ANY(@ids);
+ DELETE FROM framework."OutboxMessages" WHERE "Id" = ANY(@ids);
+ """, connection);
+ command.Parameters.AddWithValue("markers", EventIds.Select(id => $"dispatch-{id:N}").ToArray());
+ command.Parameters.AddWithValue("ids", EventIds.ToArray());
+ await command.ExecuteNonQueryAsync();
+ }
+ finally
+ {
+ await Provider.DisposeAsync();
+ if (_databaseCreated)
+ await ExecuteDatabaseCommandAsync(DefaultConnection, $"DROP DATABASE {QuoteIdentifier(_databaseName)} WITH (FORCE)");
+ }
+ }
+
+ private static ServiceProvider CreateProvider(IServiceProvider host, string connection, WebhookRecorder recorder, bool webhooks)
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary
+ {
+ ["EventingOptions:Provider"] = "InMemory",
+ ["EventingOptions:UseHostedServiceDispatcher"] = "false"
+ }).Build();
+ var services = new ServiceCollection();
+ services.AddSingleton(configuration);
+ services.AddSingleton(host.GetRequiredService());
+ services.AddSingleton(TimeProvider.System);
+ services.AddLogging();
+ services.Configure(options =>
+ {
+ options.Provider = "POSTGRESQL";
+ options.ConnectionString = connection;
+ options.MigrationsAssembly = "FSH.Starter.Migrations.PostgreSQL";
+ });
+ services.AddScoped();
+ services.AddScoped();
+ services.AddDbContext(options => options.UseNpgsql(connection));
+ services.AddDistributedMemoryCache();
+ services.AddMultiTenant()
+ .WithDistributedCacheStore(TimeSpan.FromMinutes(60))
+ .WithStore>(ServiceLifetime.Scoped);
+ services.AddEventingCore(configuration);
+ services.AddHeroDbContext();
+ services.Replace(ServiceDescriptor.Singleton());
+ services.Replace(ServiceDescriptor.Singleton());
+ services.AddScoped, FileEventMarkerHandler>();
+ if (webhooks)
+ {
+ services.AddHeroDbContext();
+ services.AddSingleton(recorder);
+ services.AddScoped(typeof(IIntegrationEventHandler<>), typeof(WebhookFanoutHandler<>));
+ }
+ return services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true });
+ }
+
+ private static string QuoteIdentifier(string identifier)
+ {
+ using var builder = new NpgsqlCommandBuilder();
+ return builder.QuoteIdentifier(identifier);
+ }
+
+ private static async Task ExecuteDatabaseCommandAsync(string connectionString, string sql)
+ {
+ await using var connection = new NpgsqlConnection(connectionString);
+ await connection.OpenAsync();
+#pragma warning disable CA2100
+ await using var command = new NpgsqlCommand(sql, connection);
+#pragma warning restore CA2100
+ await command.ExecuteNonQueryAsync();
+ }
+ }
+
+ public sealed class FileEventMarkerHandler(CatalogDbContext context) : IIntegrationEventHandler
+ {
+ public async Task HandleAsync(FileFinalizedIntegrationEvent integrationEvent, CancellationToken ct = default)
+ {
+ context.Brands.Add(Brand.Create($"dispatch-{integrationEvent.Id:N}", null, null));
+ await context.SaveChangesAsync(ct);
+ }
+ }
+
+ private sealed class WebhookRecorder : IWebhookDispatcher
+ {
+ public ConcurrentQueue<(string Tenant, Guid Subscription)> Records { get; } = new();
+
+ public Task EnqueueAsync(string tenantId, Guid subscriptionId, string eventType, string payloadJson, CancellationToken cancellationToken = default)
+ {
+ Records.Enqueue((tenantId, subscriptionId));
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Tests/Multitenancy.Tests/Services/FinbuckleEventTenantScopeTests.cs b/src/Tests/Multitenancy.Tests/Services/FinbuckleEventTenantScopeTests.cs
index a01c9aeca1..545b077d03 100644
--- a/src/Tests/Multitenancy.Tests/Services/FinbuckleEventTenantScopeTests.cs
+++ b/src/Tests/Multitenancy.Tests/Services/FinbuckleEventTenantScopeTests.cs
@@ -2,6 +2,9 @@
using Finbuckle.MultiTenant.Abstractions;
using FSH.Framework.Shared.Multitenancy;
using FSH.Modules.Multitenancy.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using NSubstitute;
using Shouldly;
using Xunit;
@@ -14,7 +17,7 @@ public sealed class FinbuckleEventTenantScopeTests
[Fact]
public void Begin_Should_SetTenantContext_When_TenantIdProvided()
{
- var sut = new FinbuckleEventTenantScope(_accessor, _accessor);
+ var sut = CreateScope();
using (sut.Begin("acme"))
{
@@ -30,7 +33,7 @@ public void Begin_Should_RestorePreviousContext_When_Disposed()
// Seed a pre-existing ambient tenant.
((IMultiTenantContextSetter)_accessor).MultiTenantContext =
new MultiTenantContext(new AppTenantInfo("root", "root"));
- var sut = new FinbuckleEventTenantScope(_accessor, _accessor);
+ var sut = CreateScope();
using (sut.Begin("acme"))
{
@@ -46,7 +49,7 @@ public void Begin_Should_LeaveContextUntouched_When_TenantIdNullOrWhitespace()
// Seed a known tenant so we can prove Begin(null/"") does not replace it.
((IMultiTenantContextSetter)_accessor).MultiTenantContext =
new MultiTenantContext(new AppTenantInfo("root", "root"));
- var sut = new FinbuckleEventTenantScope(_accessor, _accessor);
+ var sut = CreateScope();
using (sut.Begin(null))
{
@@ -59,6 +62,9 @@ public void Begin_Should_LeaveContextUntouched_When_TenantIdNullOrWhitespace()
}
}
+ private FinbuckleEventTenantScope CreateScope()
+ => new(_accessor, _accessor, Substitute.For(), NullLogger.Instance);
+
#pragma warning disable S2376 // Finbuckle's IMultiTenantContextSetter is a set-only contract.
private sealed class StubAccessor : IMultiTenantContextAccessor, IMultiTenantContextSetter
{
@@ -75,4 +81,4 @@ IMultiTenantContext IMultiTenantContextSetter.MultiTenantContext
}
}
#pragma warning restore S2376
-}
+}
\ No newline at end of file