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 =>