diff --git a/docs/guide/handlers/multi-tenancy.md b/docs/guide/handlers/multi-tenancy.md index 43bb00c2..717f8f76 100644 --- a/docs/guide/handlers/multi-tenancy.md +++ b/docs/guide/handlers/multi-tenancy.md @@ -64,3 +64,61 @@ public static IEnumerable Handle(IncomingMessage message) ``` snippet source | anchor + +## Referencing the TenantId + +Let's say that you want to reference the current tenant id in your Wolverine message handler or Wolverine HTTP endpoint, +but you don't want to inject the Wolverine `IMessageContext` or `Envelope` into your methods, but instead would like +an easy way to just "push" the current tenant id into your handler methods. Maybe this is for ease of writing unit tests, +or conditional logic, or some other reason. + +To that end, you can inject the `Wolverine.Persistence.TenantId` into any Wolverine message handler or HTTP endpoint method +to get easy access to the tenant id: + + + +```cs +/// +/// Strong typed identifier for the tenant id within a Wolverine message handler +/// or HTTP endpoint that is using multi-tenancy +/// +/// The active tenant id. Note that this can be null +public record TenantId(string Value) +{ + public const string DefaultTenantId = "*DEFAULT*"; + + /// + /// Is there a non-default tenant id? + /// + /// + public bool IsEmpty() => Value.IsEmpty() || Value == DefaultTenantId; +} +``` +snippet source | anchor + + +There's really nothing to it other than just pulling that type in as a parameter argument to a message handler: + + + +```cs +public static class SomeCommandHandler +{ + // Wolverine is keying off the type, the parameter name + // doesn't really matter + public static void Handle(SomeCommand command, TenantId tenantId) + { + Debug.WriteLine($"I got a command {command} for tenant {tenantId.Value}"); + } +} +``` +snippet source | anchor + + +In tests, you can create that `TenantId` value just by: + +```csharp +var tenantId = new TenantId("tenant1"); +``` + +and then just pass the value into the method under test. diff --git a/docs/guide/http/multi-tenancy.md b/docs/guide/http/multi-tenancy.md index 222f40c9..00a89ad8 100644 --- a/docs/guide/http/multi-tenancy.md +++ b/docs/guide/http/multi-tenancy.md @@ -230,6 +230,10 @@ public class GET_todoitems_tenant : Wolverine.Http.HttpHandler } ``` +## Referencing the Tenant Id in Endpoint Methods + +See [Referencing the TenantId](/guide/handlers/multi-tenancy.html#referencing-the-tenantid) on using Wolverine's `TenantId` type. + ## Requiring Tenant Id -- or Not! You can direct Wolverine.HTTP to verify that there is a non-null, non-empty tenant id on all requests with this syntax: @@ -265,7 +269,7 @@ public static string NoTenantNoProblem() return "hey"; } ``` -snippet source | anchor +snippet source | anchor If the above usage completely disabled all tenant id detection or validation, in the case of an endpoint that *might* be @@ -283,7 +287,7 @@ public static string MaybeTenanted(IMessageBus bus) return bus.TenantId ?? "none"; } ``` -snippet source | anchor +snippet source | anchor diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc.cs deleted file mode 100644 index a6a929db..00000000 --- a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc.cs +++ /dev/null @@ -1,77 +0,0 @@ -// -#pragma warning disable -using Microsoft.AspNetCore.Routing; -using System; -using System.Linq; -using Wolverine.Http; -using Wolverine.Marten.Publishing; -using Wolverine.Runtime; - -namespace Internal.Generated.WolverineHandlers -{ - // START: POST_api_tenants_tenant_counters_id_inc - public class POST_api_tenants_tenant_counters_id_inc : Wolverine.Http.HttpHandler - { - private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; - private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; - private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory; - - public POST_api_tenants_tenant_counters_id_inc(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory) : base(wolverineHttpOptions) - { - _wolverineHttpOptions = wolverineHttpOptions; - _wolverineRuntime = wolverineRuntime; - _outboxedSessionFactory = outboxedSessionFactory; - } - - - - public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) - { - if (!System.Guid.TryParse((string)httpContext.GetRouteValue("id"), out var id)) - { - httpContext.Response.StatusCode = 404; - return; - } - - - var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); - // Building the Marten session - await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext); - var counter = await documentSession.LoadAsync(id, httpContext.RequestAborted).ConfigureAwait(false); - // 404 if this required object is null - if (counter == null) - { - httpContext.Response.StatusCode = 404; - return; - } - - - // The actual HTTP request handler execution - (var result, var martenOp) = Wolverine.Http.Tests.Bugs.CounterEndpoint.Increment(counter); - - if (martenOp != null) - { - - // Placed by Wolverine's ISideEffect policy - martenOp.Execute(documentSession); - - } - - - // Save all pending changes to this Marten session - await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false); - - - // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 - await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); - - await result.ExecuteAsync(httpContext).ConfigureAwait(false); - } - - } - - // END: POST_api_tenants_tenant_counters_id_inc - - -} - diff --git a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc2.cs b/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc2.cs deleted file mode 100644 index 196a83f2..00000000 --- a/src/Http/Wolverine.Http.Tests/Internal/Generated/WolverineHandlers/POST_api_tenants_tenant_counters_id_inc2.cs +++ /dev/null @@ -1,78 +0,0 @@ -// -#pragma warning disable -using Microsoft.AspNetCore.Routing; -using System; -using System.Linq; -using Wolverine.Http; -using Wolverine.Marten.Publishing; -using Wolverine.Runtime; - -namespace Internal.Generated.WolverineHandlers -{ - // START: POST_api_tenants_tenant_counters_id_inc2 - public class POST_api_tenants_tenant_counters_id_inc2 : Wolverine.Http.HttpHandler - { - private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions; - private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime; - private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory; - - public POST_api_tenants_tenant_counters_id_inc2(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory) : base(wolverineHttpOptions) - { - _wolverineHttpOptions = wolverineHttpOptions; - _wolverineRuntime = wolverineRuntime; - _outboxedSessionFactory = outboxedSessionFactory; - } - - - - public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext) - { - var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime); - // Building the Marten session - await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext); - if (!System.Guid.TryParse((string)httpContext.GetRouteValue("id"), out var id)) - { - httpContext.Response.StatusCode = 404; - return; - } - - - var counter = await documentSession.LoadAsync(id, httpContext.RequestAborted).ConfigureAwait(false); - // 404 if this required object is null - if (counter == null) - { - httpContext.Response.StatusCode = 404; - return; - } - - - // The actual HTTP request handler execution - var martenOp = Wolverine.Http.Tests.Bugs.CounterEndpoint.Increment2(counter); - - if (martenOp != null) - { - - // Placed by Wolverine's ISideEffect policy - martenOp.Execute(documentSession); - - } - - - // Save all pending changes to this Marten session - await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false); - - - // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536 - await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false); - - // Wolverine automatically sets the status code to 204 for empty responses - if (!httpContext.Response.HasStarted) httpContext.Response.StatusCode = 204; - } - - } - - // END: POST_api_tenants_tenant_counters_id_inc2 - - -} - diff --git a/src/Http/Wolverine.Http.Tests/multi_tenancy_detection_and_integration.cs b/src/Http/Wolverine.Http.Tests/multi_tenancy_detection_and_integration.cs index cfbd16c2..e73e7b52 100644 --- a/src/Http/Wolverine.Http.Tests/multi_tenancy_detection_and_integration.cs +++ b/src/Http/Wolverine.Http.Tests/multi_tenancy_detection_and_integration.cs @@ -17,6 +17,7 @@ using Wolverine.Http.Runtime.MultiTenancy; using Wolverine.Http.Tests.Bugs; using Wolverine.Marten; +using Wolverine.Persistence; using Xunit.Abstractions; namespace Wolverine.Http.Tests; @@ -49,7 +50,11 @@ protected async Task configure(Action configure) // Haven't gotten around to it yet, but there'll be some end to // end tests in a bit from the ASP.Net request all the way down // to the underlying tenant databases - builder.Services.AddMarten(Servers.PostgresConnectionString) + builder.Services.AddMarten(m => + { + m.Connection(Servers.PostgresConnectionString); + m.DisableNpgsqlLogging = true; + }) .IntegrateWithWolverine(); // Defaults are good enough here @@ -376,31 +381,34 @@ public static class TenantedEndpoints { [Authorize] [WolverineGet("/tenant/route/{tenant}")] - public static string GetTenantIdFromRoute(IMessageBus bus) + public static string GetTenantIdFromRoute(IMessageBus bus, TenantId tenantId) { + tenantId.Value.ShouldBe(bus.TenantId); return bus.TenantId; } [Authorize] [WolverineGet("/tenant")] - public static string GetTenantIdFromWhatever(IMessageBus bus, HttpContext httpContext) + public static string GetTenantIdFromWhatever(IMessageBus bus, HttpContext httpContext, TenantId tenantId) { // IHttpActivityFeature.Activity is set to null after the request, so to access the // Activity in the test we capture the Activity into a custom Feature httpContext.Features.Set(CustomActivityFeature.FromHttpContext(httpContext)); - + tenantId.Value.ShouldBe(bus.TenantId); return bus.TenantId; } [WolverineGet("/todo/{id}")] - public static Task Get(string id, IQuerySession session) + public static Task Get(string id, IQuerySession session, TenantId tenantId) { + tenantId.Value.ShouldBe(session.TenantId); return session.LoadAsync(id); } [WolverinePost("/todo/create")] - public static IMartenOp Create(CreateTodo command) + public static IMartenOp Create(CreateTodo command, TenantId tenantId) { + tenantId.IsEmpty().ShouldBeFalse(); return MartenOps.Insert(new TenantTodo { Id = command.Id, @@ -409,19 +417,21 @@ public static IMartenOp Create(CreateTodo command) } [WolverineGet("/tenant/bus/{tenant}")] - public static string GetTenantWithArgs1(IMessageBus bus) + public static string GetTenantWithArgs1(IMessageBus bus, TenantId tenantId) { + tenantId.Value.ShouldBe(bus.TenantId); return bus.TenantId; } [WolverineGet("/tenant/context/{tenant}")] - public static string GetTenantWithArgs1(IMessageContext context) + public static string GetTenantWithArgs1(IMessageContext context, TenantId tenantId) { + tenantId.Value.ShouldBe(context.TenantId); return context.TenantId; } [WolverineGet("/tenant/both/{tenant}")] - public static string GetTenantWithArgs1(IMessageContext context, IMessageBus bus) + public static string GetTenantWithArgs1(IMessageContext context, IMessageBus bus, TenantId tenantId) { bus.TenantId.ShouldBe(context.TenantId); return context.TenantId; diff --git a/src/Testing/CoreTests/Acceptance/multi_tenancy.cs b/src/Testing/CoreTests/Acceptance/multi_tenancy.cs index a17d011d..d2f03dd6 100644 --- a/src/Testing/CoreTests/Acceptance/multi_tenancy.cs +++ b/src/Testing/CoreTests/Acceptance/multi_tenancy.cs @@ -1,6 +1,8 @@ using System.Diagnostics; +using CoreTests.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Wolverine.Persistence; using Wolverine.Tracking; using Xunit; @@ -75,22 +77,44 @@ public record TenantedResult(string TenantId); public static class TenantedHandler { - public static (TenantedResult, TenantedMessage2) Handle(TenantedMessage1 message, Envelope envelope, TenantedMessageTracker tracker) + public static (TenantedResult, TenantedMessage2) Handle(TenantedMessage1 message, Envelope envelope, TenantedMessageTracker tracker, TenantId tenantId) { + tenantId.Value.ShouldBe(envelope.TenantId); + tracker.TrackedOne[message.Id] = envelope.TenantId; return (new TenantedResult(envelope.TenantId), new TenantedMessage2(message.Id)); } - public static TenantedMessage3 Handle(TenantedMessage2 message, Envelope envelope, TenantedMessageTracker tracker) + public static TenantedMessage3 Handle(TenantedMessage2 message, Envelope envelope, TenantedMessageTracker tracker, TenantId tenantId) { + tenantId.Value.ShouldBe(envelope.TenantId); + tracker.TrackedTwo[message.Id] = envelope.TenantId; return new TenantedMessage3(message.Id); } - public static void Handle(TenantedMessage3 message, Envelope envelope, TenantedMessageTracker tracker) + public static void Handle(TenantedMessage3 message, Envelope envelope, TenantedMessageTracker tracker, TenantId tenantId) { + tenantId.Value.ShouldBe(envelope.TenantId); + tracker.TrackedThree[message.Id] = envelope.TenantId; } public static void Handle(TenantedResult result) => Debug.WriteLine("Got a tracked result"); } + +public record SomeCommand; + +#region sample_injecting_tenant_id + +public static class SomeCommandHandler +{ + // Wolverine is keying off the type, the parameter name + // doesn't really matter + public static void Handle(SomeCommand command, TenantId tenantId) + { + Debug.WriteLine($"I got a command {command} for tenant {tenantId.Value}"); + } +} + +#endregion diff --git a/src/Wolverine/Persistence/TenantId.cs b/src/Wolverine/Persistence/TenantId.cs new file mode 100644 index 00000000..069b170a --- /dev/null +++ b/src/Wolverine/Persistence/TenantId.cs @@ -0,0 +1,82 @@ +using JasperFx.CodeGeneration; +using JasperFx.CodeGeneration.Frames; +using JasperFx.CodeGeneration.Model; +using JasperFx.Core; +using JasperFx.Core.Reflection; + +namespace Wolverine.Persistence; + +#region sample_TenantId + +/// +/// Strong typed identifier for the tenant id within a Wolverine message handler +/// or HTTP endpoint that is using multi-tenancy +/// +/// The active tenant id. Note that this can be null +public record TenantId(string Value) +{ + public const string DefaultTenantId = "*DEFAULT*"; + + /// + /// Is there a non-default tenant id? + /// + /// + public bool IsEmpty() => Value.IsEmpty() || Value == DefaultTenantId; +} + +#endregion + +internal class TenantIdSource : IVariableSource +{ + public bool Matches(Type type) + { + return type == typeof(TenantId); + } + + public Variable Create(Type type) + { + return new TenantIdResolutionFrame().TenantId; + } +} + +internal class TenantIdResolutionFrame : SyncFrame +{ + private bool _useRawTenantId = false; + private Variable _context; + + public TenantIdResolutionFrame() + { + TenantId = new Variable(typeof(TenantId), "tenantIdentifier", this); + } + + public Variable TenantId { get; } + + public override IEnumerable FindVariables(IMethodVariables chain) + { + if (chain.TryFindVariableByName(typeof(string), PersistenceConstants.TenantIdVariableName, + out var rawId)) + { + yield return rawId; + _useRawTenantId = true; + } + else + { + _context = chain.FindVariable(typeof(IMessageContext)); + yield return _context; + } + } + + public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) + { + if (_useRawTenantId) + { + writer.WriteLine($"var {TenantId.Usage} = new {typeof(TenantId).FullNameInCode()}({PersistenceConstants.TenantIdVariableName});"); + } + else + { + writer.WriteLine($"var {TenantId.Usage} = new {typeof(TenantId).FullNameInCode()}({_context.Usage}.{nameof(IMessageContext.TenantId)});"); + } + + Next?.GenerateCode(method, writer); + } +} \ No newline at end of file diff --git a/src/Wolverine/WolverineOptions.cs b/src/Wolverine/WolverineOptions.cs index 1af9ff1e..0be85db2 100644 --- a/src/Wolverine/WolverineOptions.cs +++ b/src/Wolverine/WolverineOptions.cs @@ -5,6 +5,7 @@ using JasperFx.Core; using Microsoft.Extensions.DependencyInjection; using Wolverine.Configuration; +using Wolverine.Persistence; using Wolverine.Runtime.Handlers; using Wolverine.Runtime.Scheduled; using Wolverine.Runtime.Serialization; @@ -51,6 +52,7 @@ public WolverineOptions(string? assemblyName) CodeGeneration = new GenerationRules("Internal.Generated"); CodeGeneration.Sources.Add(new NowTimeVariableSource()); + CodeGeneration.Sources.Add(new TenantIdSource()); CodeGeneration.Assemblies.Add(GetType().Assembly); establishApplicationAssembly(assemblyName);