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.
This commit is contained in:
Sipke Schoorstra 2024-10-17 19:47:55 +02:00
parent b8ab05ce2e
commit bdaae64e72
16 changed files with 93 additions and 23 deletions

View file

@ -14,7 +14,7 @@ public static class MultitenantConnectionStringServiceProviderExtensions
{
var tenantAccessor = serviceProvider.GetRequiredService<ITenantAccessor>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var tenant = tenantAccessor.CurrentTenant;
var tenant = tenantAccessor.Tenant;
var defaultConnectionString = configuration.GetConnectionString(name)!;
var connectionString = tenant?.GetConnectionString(name) ?? defaultConnectionString;
return connectionString;

View file

@ -3,8 +3,8 @@ namespace Elsa.Common.Multitenancy;
public interface ITenantAccessor
{
/// <summary>
/// Get the current <see cref="Tenant"/>.
/// Get the current <see cref="Multitenancy.Tenant"/>.
/// </summary>
/// <returns>Current tenant or null.</returns>
Tenant? CurrentTenant { get; set; }
Tenant? Tenant { get; set; }
}

View file

@ -8,7 +8,7 @@ public class DefaultTenantAccessor : ITenantAccessor
private static readonly AsyncLocal<Tenant?> CurrentTenantField = new();
/// <inheritdoc/>
public Tenant? CurrentTenant
public Tenant? Tenant
{
get => CurrentTenantField.Value;
set => CurrentTenantField.Value = value;

View file

@ -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();
}
}

View file

@ -512,7 +512,7 @@ public class Store<T>(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<T>(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;
}

View file

@ -46,7 +46,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema
Schema = !string.IsNullOrWhiteSpace(elsaDbContextOptions?.SchemaName) ? elsaDbContextOptions.SchemaName : ElsaSchema;
var tenantAccessor = serviceProvider.GetService<ITenantAccessor>();
TenantId = tenantAccessor?.CurrentTenant?.Id;
TenantId = tenantAccessor?.Tenant?.Id;
}
/// <inheritdoc/>

View file

@ -0,0 +1,6 @@
namespace Elsa.Http;
public interface IHttpEndpointBasePathProvider
{
string GetBasePath();
}

View file

@ -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<IServiceProvider, IHttpEndpointRoutesProvider> _httpEndpointRouteProvider = sp => sp.GetRequiredService<DefaultHttpEndpointRoutesProvider>();
private Func<IServiceProvider, IHttpEndpointBasePathProvider> _httpEndpointBasePathProvider = sp => sp.GetRequiredService<DefaultHttpEndpointBasePathProvider>();
/// <summary>
/// A delegate to configure <see cref="HttpActivityOptions"/>.
@ -106,6 +108,18 @@ public class HttpFeature(IModule module) : FeatureBase(module)
_httpEndpointRouteProvider = httpEndpointRouteProvider;
return this;
}
public HttpFeature WithHttpEndpointBasePathProvider<T>() where T : class, IHttpEndpointBasePathProvider
{
Services.TryAddScoped<T>();
return WithHttpEndpointBasePathProvider(sp => sp.GetRequiredService<T>());
}
public HttpFeature WithHttpEndpointBasePathProvider(Func<IServiceProvider, IHttpEndpointBasePathProvider> httpEndpointBasePathProvider)
{
_httpEndpointBasePathProvider = httpEndpointBasePathProvider;
return this;
}
/// <inheritdoc />
public override void Configure()
@ -178,6 +192,7 @@ public class HttpFeature(IModule module) : FeatureBase(module)
// Activity property options providers.
.AddScoped<IPropertyUIHandler, HttpContentTypeOptionsProvider>()
.AddScoped(_httpEndpointBasePathProvider)
// Port resolvers.
.AddScoped<IActivityResolver, SendHttpRequestActivityResolver>()

View file

@ -16,7 +16,7 @@ public class CachingHttpWorkflowLookupService(
/// <inheritdoc />
public async Task<HttpWorkflowLookupResult?> 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}";

View file

@ -0,0 +1,18 @@
using Elsa.Http.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
namespace Elsa.Http.Services;
[UsedImplicitly]
public class DefaultHttpEndpointBasePathProvider(IOptions<HttpActivityOptions> 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('/') + '/';
}
}

View file

@ -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;
/// <summary>
/// Provides additional options for the Path input field.
/// </summary>
public class HttpEndpointPathUIHandler(IOptions<HttpActivityOptions> options) : IPropertyUIHandler
public class HttpEndpointPathUIHandler(IHttpEndpointBasePathProvider httpEndpointBasePathProvider) : IPropertyUIHandler
{
/// <inheritdoc />
public ValueTask<IDictionary<string, object>> 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<string, object>
{
[InputUIHints.SingleLine] = new SingleLineProps
{
AdornmentText = completeBaseUrl.ToString().TrimEnd('/') + '/'
AdornmentText = completeBaseUrl.TrimEnd('/') + '/'
},
["Refresh"] = true
});
}
}

View file

@ -395,7 +395,7 @@ public class MongoDbStore<TDocument>(IMongoCollection<TDocument> 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<TDocument>(IMongoCollection<TDocument> 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<TDocument>(IMongoCollection<TDocument> collection, ITe
private void ApplyTenantId(IEnumerable<TDocument> documents)
{
var tenant = tenantAccessor.CurrentTenant;
var tenant = tenantAccessor.Tenant;
var tenantId = tenant?.Id;
foreach (var document in documents)

View file

@ -18,6 +18,7 @@ public class MultitenantHttpRoutingFeature(IModule module) : FeatureBase(module)
Module.Configure<HttpFeature>(feature =>
{
feature.WithHttpEndpointRoutesProvider<TenantPrefixHttpEndpointRoutesProvider>();
feature.WithHttpEndpointBasePathProvider<TenantPrefixHttpEndpointBasePathProvider>();
});
}

View file

@ -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<HttpActivityOptions> options,
IEnumerable<ITenantResolver> 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('/') + '/';
}
}

View file

@ -137,7 +137,7 @@ public class CachingWorkflowDefinitionStore(IWorkflowDefinitionStore decoratedSt
private async Task<T?> GetOrCreateAsync<T>(string key, Func<Task<T>> 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 =>

View file

@ -58,7 +58,7 @@ public class CachingTriggerStore(ITriggerStore decoratedStore, ICacheManager cac
private async Task<T?> GetOrCreateAsync<T>(string key, Func<Task<T>> 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 =>