From bdaae64e72c30642f7ae8dc541bdcff4deff2c04 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 17 Oct 2024 19:47:55 +0200 Subject: [PATCH] Add base path providers and standardize tenant accessor property Introduce DefaultHttpEndpointBasePathProvider and TenantPrefixHttpEndpointBasePathProvider to manage HTTP endpoint base paths. Update ITenantAccessor property from CurrentTenant to Tenant for consistency across the codebase. --- ...nnectionStringServiceProviderExtensions.cs | 2 +- .../Multitenancy/Contracts/ITenantAccessor.cs | 4 +-- .../Implementations/DefaultTenantAccessor.cs | 2 +- .../Multitenancy/Models/TenantScope.cs | 6 ++-- src/modules/Elsa.Dapper/Services/Store.cs | 4 +-- .../ElsaDbContextBase.cs | 2 +- .../IHttpEndpointBasePathProvider.cs | 6 ++++ src/modules/Elsa.Http/Features/HttpFeature.cs | 15 +++++++++ .../CachingHttpWorkflowLookupService.cs | 2 +- .../DefaultHttpEndpointBasePathProvider.cs | 18 ++++++++++ .../UIHints/HttpEndpointPathUIHandler.cs | 11 +++---- .../Elsa.MongoDb/Common/MongoDbStore.cs | 6 ++-- .../Features/MultitenantHttpRoutingFeature.cs | 1 + ...enantPrefixHttpEndpointBasePathProvider.cs | 33 +++++++++++++++++++ .../Stores/CachingWorkflowDefinitionStore.cs | 2 +- .../Stores/CachingTriggerStore.cs | 2 +- 16 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 src/modules/Elsa.Http/Contracts/IHttpEndpointBasePathProvider.cs create mode 100644 src/modules/Elsa.Http/Services/DefaultHttpEndpointBasePathProvider.cs create mode 100644 src/modules/Elsa.Tenants.AspNetCore/Services/TenantPrefixHttpEndpointBasePathProvider.cs diff --git a/src/apps/Elsa.Server.Web/Extensions/MultitenantConnectionStringServiceProviderExtensions.cs b/src/apps/Elsa.Server.Web/Extensions/MultitenantConnectionStringServiceProviderExtensions.cs index cb6eae817..0cc7a9c41 100644 --- a/src/apps/Elsa.Server.Web/Extensions/MultitenantConnectionStringServiceProviderExtensions.cs +++ b/src/apps/Elsa.Server.Web/Extensions/MultitenantConnectionStringServiceProviderExtensions.cs @@ -14,7 +14,7 @@ public static class MultitenantConnectionStringServiceProviderExtensions { var tenantAccessor = serviceProvider.GetRequiredService(); var configuration = serviceProvider.GetRequiredService(); - var tenant = tenantAccessor.CurrentTenant; + var tenant = tenantAccessor.Tenant; var defaultConnectionString = configuration.GetConnectionString(name)!; var connectionString = tenant?.GetConnectionString(name) ?? defaultConnectionString; return connectionString; diff --git a/src/modules/Elsa.Common/Multitenancy/Contracts/ITenantAccessor.cs b/src/modules/Elsa.Common/Multitenancy/Contracts/ITenantAccessor.cs index 5b029994d..09355ef5c 100644 --- a/src/modules/Elsa.Common/Multitenancy/Contracts/ITenantAccessor.cs +++ b/src/modules/Elsa.Common/Multitenancy/Contracts/ITenantAccessor.cs @@ -3,8 +3,8 @@ namespace Elsa.Common.Multitenancy; public interface ITenantAccessor { /// - /// Get the current . + /// Get the current . /// /// Current tenant or null. - Tenant? CurrentTenant { get; set; } + Tenant? Tenant { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs index 5b18788de..7325c664c 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs @@ -8,7 +8,7 @@ public class DefaultTenantAccessor : ITenantAccessor private static readonly AsyncLocal CurrentTenantField = new(); /// - public Tenant? CurrentTenant + public Tenant? Tenant { get => CurrentTenantField.Value; set => CurrentTenantField.Value = value; diff --git a/src/modules/Elsa.Common/Multitenancy/Models/TenantScope.cs b/src/modules/Elsa.Common/Multitenancy/Models/TenantScope.cs index 2a6f7c099..4c86328c9 100644 --- a/src/modules/Elsa.Common/Multitenancy/Models/TenantScope.cs +++ b/src/modules/Elsa.Common/Multitenancy/Models/TenantScope.cs @@ -16,15 +16,15 @@ public class TenantScope : IDisposable { _serviceScope = serviceScope; _tenantAccessor = tenantAccessor; - _originalTenant = tenantAccessor.CurrentTenant; - _tenantAccessor.CurrentTenant = tenant; + _originalTenant = tenantAccessor.Tenant; + _tenantAccessor.Tenant = tenant; } public IServiceProvider ServiceProvider => _serviceScope.ServiceProvider; public void Dispose() { - _tenantAccessor.CurrentTenant = _originalTenant; + _tenantAccessor.Tenant = _originalTenant; _serviceScope.Dispose(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Dapper/Services/Store.cs b/src/modules/Elsa.Dapper/Services/Store.cs index 33a5b90e2..6381d3d84 100644 --- a/src/modules/Elsa.Dapper/Services/Store.cs +++ b/src/modules/Elsa.Dapper/Services/Store.cs @@ -512,7 +512,7 @@ public class Store(IDbConnectionProvider dbConnectionProvider, ITenantAccesso if (tenantAgnostic) return; - var tenant = tenantAccessor.CurrentTenant; + var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; query.Is(nameof(Record.TenantId), (object?)tenantId ?? DBNull.Value); } @@ -522,7 +522,7 @@ public class Store(IDbConnectionProvider dbConnectionProvider, ITenantAccesso if (record is not Record recordWithTenant) return; - var tenant = tenantAccessor.CurrentTenant; + var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; recordWithTenant.TenantId = tenantId; } diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs index d526b230f..4fc1538d6 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs @@ -46,7 +46,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema Schema = !string.IsNullOrWhiteSpace(elsaDbContextOptions?.SchemaName) ? elsaDbContextOptions.SchemaName : ElsaSchema; var tenantAccessor = serviceProvider.GetService(); - TenantId = tenantAccessor?.CurrentTenant?.Id; + TenantId = tenantAccessor?.Tenant?.Id; } /// diff --git a/src/modules/Elsa.Http/Contracts/IHttpEndpointBasePathProvider.cs b/src/modules/Elsa.Http/Contracts/IHttpEndpointBasePathProvider.cs new file mode 100644 index 000000000..229e1fa99 --- /dev/null +++ b/src/modules/Elsa.Http/Contracts/IHttpEndpointBasePathProvider.cs @@ -0,0 +1,6 @@ +namespace Elsa.Http; + +public interface IHttpEndpointBasePathProvider +{ + string GetBasePath(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 416ed7d84..acce72a5a 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -21,6 +21,7 @@ using FluentStorage; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Elsa.Http.Features; @@ -32,6 +33,7 @@ namespace Elsa.Http.Features; public class HttpFeature(IModule module) : FeatureBase(module) { private Func _httpEndpointRouteProvider = sp => sp.GetRequiredService(); + private Func _httpEndpointBasePathProvider = sp => sp.GetRequiredService(); /// /// A delegate to configure . @@ -106,6 +108,18 @@ public class HttpFeature(IModule module) : FeatureBase(module) _httpEndpointRouteProvider = httpEndpointRouteProvider; return this; } + + public HttpFeature WithHttpEndpointBasePathProvider() where T : class, IHttpEndpointBasePathProvider + { + Services.TryAddScoped(); + return WithHttpEndpointBasePathProvider(sp => sp.GetRequiredService()); + } + + public HttpFeature WithHttpEndpointBasePathProvider(Func httpEndpointBasePathProvider) + { + _httpEndpointBasePathProvider = httpEndpointBasePathProvider; + return this; + } /// public override void Configure() @@ -178,6 +192,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) // Activity property options providers. .AddScoped() + .AddScoped(_httpEndpointBasePathProvider) // Port resolvers. .AddScoped() diff --git a/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs b/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs index dc0052fac..d67b87ced 100644 --- a/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs +++ b/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs @@ -16,7 +16,7 @@ public class CachingHttpWorkflowLookupService( /// public async Task FindWorkflowAsync(string bookmarkHash, CancellationToken cancellationToken = default) { - var tenant = tenantAccessor.CurrentTenant; + var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; var tenantIdPrefix = !string.IsNullOrEmpty(tenantId) ? $"{tenantId}:" : string.Empty; var key = $"{tenantIdPrefix}http-workflow:{bookmarkHash}"; diff --git a/src/modules/Elsa.Http/Services/DefaultHttpEndpointBasePathProvider.cs b/src/modules/Elsa.Http/Services/DefaultHttpEndpointBasePathProvider.cs new file mode 100644 index 000000000..89d97ce89 --- /dev/null +++ b/src/modules/Elsa.Http/Services/DefaultHttpEndpointBasePathProvider.cs @@ -0,0 +1,18 @@ +using Elsa.Http.Options; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; + +namespace Elsa.Http.Services; + +[UsedImplicitly] +public class DefaultHttpEndpointBasePathProvider(IOptions options) : IHttpEndpointBasePathProvider +{ + public string GetBasePath() + { + var baseUrl = options.Value.BaseUrl; + var basePath = options.Value.BasePath; + var completeBaseUrl = new Uri(baseUrl, basePath); + + return completeBaseUrl.ToString().TrimEnd('/') + '/'; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/UIHints/HttpEndpointPathUIHandler.cs b/src/modules/Elsa.Http/UIHints/HttpEndpointPathUIHandler.cs index 273a435ba..487a8b21b 100644 --- a/src/modules/Elsa.Http/UIHints/HttpEndpointPathUIHandler.cs +++ b/src/modules/Elsa.Http/UIHints/HttpEndpointPathUIHandler.cs @@ -1,30 +1,27 @@ using System.Reflection; -using Elsa.Http.Options; using Elsa.Workflows; using Elsa.Workflows.UIHints; using Elsa.Workflows.UIHints.SingleLine; -using Microsoft.Extensions.Options; namespace Elsa.Http.UIHints; /// /// Provides additional options for the Path input field. /// -public class HttpEndpointPathUIHandler(IOptions options) : IPropertyUIHandler +public class HttpEndpointPathUIHandler(IHttpEndpointBasePathProvider httpEndpointBasePathProvider) : IPropertyUIHandler { /// public ValueTask> GetUIPropertiesAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken = default) { - var baseUrl = options.Value.BaseUrl; - var basePath = options.Value.BasePath; - var completeBaseUrl = new Uri(baseUrl, basePath); + var completeBaseUrl = httpEndpointBasePathProvider.GetBasePath(); return new(new Dictionary { [InputUIHints.SingleLine] = new SingleLineProps { - AdornmentText = completeBaseUrl.ToString().TrimEnd('/') + '/' + AdornmentText = completeBaseUrl.TrimEnd('/') + '/' }, + ["Refresh"] = true }); } } \ No newline at end of file diff --git a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs index 6b88338ae..12ae9e437 100644 --- a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs +++ b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs @@ -395,7 +395,7 @@ public class MongoDbStore(IMongoCollection collection, ITe if(typeof(Entity).IsAssignableFrom(typeof(TDocument))) { - var tenant = tenantAccessor.CurrentTenant; + var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; queryable = queryable.Where(x => (x as Entity)!.TenantId == tenantId); } @@ -405,7 +405,7 @@ public class MongoDbStore(IMongoCollection collection, ITe private void ApplyTenantId(TDocument document) { - var tenant = tenantAccessor.CurrentTenant; + var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; if (document is Entity tenantDocument) @@ -414,7 +414,7 @@ public class MongoDbStore(IMongoCollection collection, ITe private void ApplyTenantId(IEnumerable documents) { - var tenant = tenantAccessor.CurrentTenant; + var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; foreach (var document in documents) diff --git a/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs b/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs index 5f43ebb2a..2def1e4d6 100644 --- a/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs +++ b/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs @@ -18,6 +18,7 @@ public class MultitenantHttpRoutingFeature(IModule module) : FeatureBase(module) Module.Configure(feature => { feature.WithHttpEndpointRoutesProvider(); + feature.WithHttpEndpointBasePathProvider(); }); } diff --git a/src/modules/Elsa.Tenants.AspNetCore/Services/TenantPrefixHttpEndpointBasePathProvider.cs b/src/modules/Elsa.Tenants.AspNetCore/Services/TenantPrefixHttpEndpointBasePathProvider.cs new file mode 100644 index 000000000..7ccf3f809 --- /dev/null +++ b/src/modules/Elsa.Tenants.AspNetCore/Services/TenantPrefixHttpEndpointBasePathProvider.cs @@ -0,0 +1,33 @@ +using Elsa.Common.Multitenancy; +using Elsa.Http; +using Elsa.Http.Options; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; + +namespace Elsa.Tenants.AspNetCore.Services; + +[UsedImplicitly] +public class TenantPrefixHttpEndpointBasePathProvider( + IOptions options, + IEnumerable tenantResolvers, + ITenantAccessor tenantAccessor) : IHttpEndpointBasePathProvider +{ + public string GetBasePath() + { + var baseUrl = options.Value.BaseUrl.ToString(); + var basePath = options.Value.BasePath?.ToString().TrimStart('/'); + var routePrefixTenantResolverIsEnabled = tenantResolvers.Any(x => x is RoutePrefixTenantResolver); + + if(!routePrefixTenantResolverIsEnabled) + return (baseUrl + basePath).TrimEnd('/') + '/'; + + var tenant = tenantAccessor.Tenant; + var tenantPrefix = tenant?.GetRoutePrefix(); + + if(string.IsNullOrWhiteSpace(tenantPrefix)) + return (baseUrl + basePath).TrimEnd('/') + '/'; + + var completeBaseUrl = new Uri(baseUrl + tenantPrefix + "/") + basePath; + return completeBaseUrl.TrimEnd('/') + '/'; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs b/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs index 1c4c04cd1..876e52168 100644 --- a/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs +++ b/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs @@ -137,7 +137,7 @@ public class CachingWorkflowDefinitionStore(IWorkflowDefinitionStore decoratedSt private async Task GetOrCreateAsync(string key, Func> factory) { - var tenantId = tenantAccessor.CurrentTenant?.Id; + var tenantId = tenantAccessor.Tenant?.Id; var tenantIdPrefix = !string.IsNullOrEmpty(tenantId) ? $"{tenantId}:" : string.Empty; var internalKey = $"{tenantIdPrefix}{typeof(T).Name}:{key}"; return await cacheManager.GetOrCreateAsync(internalKey, async entry => diff --git a/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs b/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs index df7649ae7..f4761cdb8 100644 --- a/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs +++ b/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs @@ -58,7 +58,7 @@ public class CachingTriggerStore(ITriggerStore decoratedStore, ICacheManager cac private async Task GetOrCreateAsync(string key, Func> factory) { - var tenantId = tenantAccessor.CurrentTenant?.Id; + var tenantId = tenantAccessor.Tenant?.Id; var tenantIdPrefix = !string.IsNullOrEmpty(tenantId) ? $"{tenantId}:" : string.Empty; var internalKey = $"{tenantIdPrefix}{typeof(T).Name}:{key}"; return await cacheManager.GetOrCreateAsync(internalKey, async entry =>