mirror of https://github.com/abpframework/abp.git
32 changed files with 418 additions and 295 deletions
@ -0,0 +1,13 @@ |
|||
using Volo.Abp.AspNetCore.MultiTenancy; |
|||
|
|||
namespace Microsoft.AspNetCore.Builder |
|||
{ |
|||
public static class AbpAspNetCoreMultiTenancyApplicationBuilderExtensions |
|||
{ |
|||
public static IApplicationBuilder UseMultiTenancy(this IApplicationBuilder app) |
|||
{ |
|||
return app |
|||
.UseMiddleware<MultiTenancyMiddleware>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Volo.Abp.AspNetCore.MultiTenancy |
|||
{ |
|||
public class MultiTenancyMiddleware |
|||
{ |
|||
private readonly RequestDelegate _next; |
|||
|
|||
private readonly ITenantResolver _tenantResolver; |
|||
private readonly ITenantStore _tenantStore; |
|||
private readonly ICurrentTenantIdAccessor _currentTenantIdAccessor; |
|||
|
|||
public MultiTenancyMiddleware( |
|||
RequestDelegate next, |
|||
ITenantResolver tenantResolver, |
|||
ITenantStore tenantStore, |
|||
ICurrentTenantIdAccessor currentTenantIdAccessor) |
|||
{ |
|||
_next = next; |
|||
_tenantResolver = tenantResolver; |
|||
_tenantStore = tenantStore; |
|||
_currentTenantIdAccessor = currentTenantIdAccessor; |
|||
} |
|||
|
|||
public async Task Invoke(HttpContext httpContext) |
|||
{ |
|||
//TODO: Try-catch and return "unknown tenant" if found tenant is not in the store..?
|
|||
|
|||
var tenantIdOrName = _tenantResolver.ResolveTenantIdOrName(); |
|||
if (tenantIdOrName == null) |
|||
{ |
|||
await _next(httpContext); |
|||
return; |
|||
} |
|||
|
|||
var tenant = await FindTenantAsync(tenantIdOrName); |
|||
if (tenant == null) |
|||
{ |
|||
throw new AbpException("There is no tenant with given tenant id or name: " + tenantIdOrName); |
|||
} |
|||
|
|||
using (SetCurrent(tenant)) |
|||
{ |
|||
await _next(httpContext); |
|||
} |
|||
} |
|||
|
|||
private async Task<TenantInfo> FindTenantAsync(string tenantIdOrName) |
|||
{ |
|||
if (Guid.TryParse(tenantIdOrName, out var parsedTenantId)) |
|||
{ |
|||
return await _tenantStore.FindAsync(parsedTenantId); |
|||
} |
|||
else |
|||
{ |
|||
return await _tenantStore.FindAsync(tenantIdOrName); |
|||
} |
|||
} |
|||
|
|||
private IDisposable SetCurrent(TenantInfo tenant) |
|||
{ |
|||
var parentScope = _currentTenantIdAccessor.Current; |
|||
_currentTenantIdAccessor.Current = new TenantIdWrapper(tenant?.Id); |
|||
return new DisposeAction(() => |
|||
{ |
|||
_currentTenantIdAccessor.Current = parentScope; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System.Threading; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public class AsyncLocalCurrentTenantIdAccessor : ICurrentTenantIdAccessor, ISingletonDependency |
|||
{ |
|||
public TenantIdWrapper Current |
|||
{ |
|||
get => _currentScope.Value; |
|||
set => _currentScope.Value = value; |
|||
} |
|||
|
|||
private readonly AsyncLocal<TenantIdWrapper> _currentScope; |
|||
|
|||
public AsyncLocalCurrentTenantIdAccessor() |
|||
{ |
|||
_currentScope = new AsyncLocal<TenantIdWrapper>(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,114 +1,39 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public class CurrentTenant : ICurrentTenant, ITransientDependency |
|||
{ |
|||
public bool IsAvailable => Id.HasValue; |
|||
public virtual bool IsAvailable => Id.HasValue; |
|||
|
|||
public Guid? Id => GetCurrentTenant()?.Id; |
|||
public virtual Guid? Id => _currentTenantIdAccessor.Current?.TenantId; |
|||
|
|||
public string Name => GetCurrentTenant()?.Name; |
|||
private readonly ICurrentTenantIdAccessor _currentTenantIdAccessor; |
|||
|
|||
public ConnectionStrings ConnectionStrings => GetCurrentTenant()?.ConnectionStrings; |
|||
|
|||
private readonly TenantScopeProvider _tenantScopeProvider; |
|||
private readonly ITenantStore _tenantStore; |
|||
private readonly ILogger<CurrentTenant> _logger; |
|||
private readonly ITenantResolver _tenantResolver; |
|||
|
|||
public CurrentTenant( |
|||
TenantScopeProvider tenantScopeProvider, |
|||
ITenantStore tenantStore, |
|||
ILogger<CurrentTenant> logger, |
|||
ITenantResolver tenantResolver) |
|||
public CurrentTenant(ICurrentTenantIdAccessor currentTenantIdAccessor) |
|||
{ |
|||
_tenantScopeProvider = tenantScopeProvider; |
|||
_tenantStore = tenantStore; |
|||
_logger = logger; |
|||
_tenantResolver = tenantResolver; |
|||
_currentTenantIdAccessor = currentTenantIdAccessor; |
|||
} |
|||
|
|||
public IDisposable Change(Guid? id) |
|||
{ |
|||
if (id == null) |
|||
{ |
|||
return _tenantScopeProvider.EnterScope(null); |
|||
} |
|||
|
|||
var tenant = _tenantStore.Find(id.Value); |
|||
if (tenant == null) |
|||
{ |
|||
throw new AbpException("There is no tenant with given tenant id: " + id.Value); |
|||
} |
|||
|
|||
return _tenantScopeProvider.EnterScope(tenant); |
|||
} |
|||
|
|||
public IDisposable Change(string name) |
|||
{ |
|||
if (name == null) |
|||
{ |
|||
return _tenantScopeProvider.EnterScope(null); |
|||
} |
|||
|
|||
var tenant = _tenantStore.Find(name); |
|||
if (tenant == null) |
|||
{ |
|||
throw new AbpException("There is no tenant with given tenant name: " + name); |
|||
} |
|||
|
|||
return _tenantScopeProvider.EnterScope(tenant); |
|||
return SetCurrent(id); |
|||
} |
|||
|
|||
[CanBeNull] |
|||
protected virtual TenantInfo GetCurrentTenant() |
|||
public IDisposable Clear() |
|||
{ |
|||
if (_tenantScopeProvider.CurrentScope != null) |
|||
{ |
|||
return _tenantScopeProvider.CurrentScope.Tenant; |
|||
} |
|||
|
|||
//TODO: Get from ICurrentUser before resolvers and fail if resolvers find a different tenant!
|
|||
|
|||
return ResolveTenant(); |
|||
return Change(null); |
|||
} |
|||
|
|||
[CanBeNull] |
|||
protected virtual TenantInfo ResolveTenant() |
|||
private IDisposable SetCurrent(Guid? tenantId) |
|||
{ |
|||
var tenantIdOrName = _tenantResolver.ResolveTenantIdOrName(); |
|||
if (tenantIdOrName == null) |
|||
var parentScope = _currentTenantIdAccessor.Current; |
|||
_currentTenantIdAccessor.Current = new TenantIdWrapper(tenantId); |
|||
return new DisposeAction(() => |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
TenantInfo tenant; |
|||
|
|||
//Try to find by id
|
|||
if (Guid.TryParse(tenantIdOrName, out var tenantId)) |
|||
{ |
|||
tenant = _tenantStore.Find(tenantId); |
|||
if (tenant != null) |
|||
{ |
|||
return tenant; |
|||
} |
|||
} |
|||
|
|||
//Try to find by name
|
|||
tenant = _tenantStore.Find(tenantIdOrName); |
|||
if (tenant != null) |
|||
{ |
|||
return tenant; |
|||
} |
|||
|
|||
//Could not found!
|
|||
_logger.LogWarning($"Resolved tenancy id or name '{tenantIdOrName}' but could not find in the tenant store."); |
|||
return null; |
|||
_currentTenantIdAccessor.Current = parentScope; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,13 @@ |
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
/* Uses TenantScopeTenantInfoWrapper instead of TenantInfo because being null of Current is different that being null of Current.Tenant. |
|||
* A null Current indicates that we haven't set it explicitly. |
|||
* A null Current.Tenant indicates that we have set null tenant value explicitly. |
|||
* A non-null Current.Tenant indicates that we have set a tenant value explicitly. |
|||
*/ |
|||
|
|||
public interface ICurrentTenantIdAccessor |
|||
{ |
|||
TenantIdWrapper Current { get; set; } |
|||
} |
|||
} |
|||
@ -1,14 +1,12 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public interface ITenantStore |
|||
{ |
|||
[CanBeNull] |
|||
TenantInfo Find(string name); |
|||
Task<TenantInfo> FindAsync(string name); |
|||
|
|||
[CanBeNull] |
|||
TenantInfo Find(Guid id); |
|||
Task<TenantInfo> FindAsync(Guid id); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public class TenantIdWrapper |
|||
{ |
|||
/// <summary>
|
|||
/// Null indicates the host.
|
|||
/// Not null value for a tenant.
|
|||
/// </summary>
|
|||
public Guid? TenantId { get; } |
|||
|
|||
public TenantIdWrapper(Guid? tenantId) |
|||
{ |
|||
TenantId = tenantId; |
|||
} |
|||
} |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public class TenantScope |
|||
{ |
|||
/// <summary>
|
|||
/// Null indicates the host.
|
|||
/// Not null value for a tenant.
|
|||
/// </summary>
|
|||
[CanBeNull] |
|||
public TenantInfo Tenant { get; } |
|||
|
|||
public TenantScope([CanBeNull] TenantInfo tenant) |
|||
{ |
|||
Tenant = tenant; |
|||
} |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
/* This cass uses TenantScope instead of TenantInfo because being null of CurrentScope is different that being null of CurrentScope.Tenant. |
|||
* A null CurrentScope indicates that we haven't set any scope explicitly (and we can use tenant resolvers to determine the current tenant). |
|||
* A null CurrentScope.Tenant indicates that we have set null tenant value (maybe to switch to host) explicitly. |
|||
* A non-null CurrentScope.Tenant indicates that we have set a tenant value explicitly. |
|||
*/ |
|||
|
|||
public class TenantScopeProvider : ISingletonDependency |
|||
{ |
|||
public TenantScope CurrentScope |
|||
{ |
|||
get => _currentScope.Value; |
|||
private set => _currentScope.Value = value; |
|||
} |
|||
|
|||
private readonly AsyncLocal<TenantScope> _currentScope; |
|||
|
|||
public TenantScopeProvider() |
|||
{ |
|||
_currentScope = new AsyncLocal<TenantScope>(); |
|||
} |
|||
|
|||
public IDisposable EnterScope(TenantInfo tenant) |
|||
{ |
|||
var parentScope = CurrentScope; |
|||
CurrentScope = new TenantScope(tenant); |
|||
return new DisposeAction(() => |
|||
{ |
|||
CurrentScope = parentScope; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public static class TenantStoreExtensions |
|||
{ |
|||
[CanBeNull] |
|||
public static TenantInfo Find(this ITenantStore tenantStore, string name) |
|||
{ |
|||
return AsyncHelper.RunSync(() => tenantStore.FindAsync(name)); |
|||
} |
|||
|
|||
[CanBeNull] |
|||
public static TenantInfo Find(this ITenantStore tenantStore, Guid id) |
|||
{ |
|||
return AsyncHelper.RunSync(() => tenantStore.FindAsync(id)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using AutoMapper; |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public class AbpMultiTenancyDomainMappingProfile : Profile |
|||
{ |
|||
public AbpMultiTenancyDomainMappingProfile() |
|||
{ |
|||
CreateMap<Tenant, TenantInfo>() |
|||
.ForMember(ti => ti.ConnectionStrings, opts => |
|||
{ |
|||
opts.ResolveUsing(tenant => |
|||
{ |
|||
var connStrings = new ConnectionStrings(); |
|||
|
|||
foreach (var connectionString in tenant.ConnectionStrings) |
|||
{ |
|||
connStrings[connectionString.Name] = connectionString.Value; |
|||
} |
|||
|
|||
return connStrings; |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public interface ITenantRepository : IRepository<Tenant> |
|||
{ |
|||
Task<Tenant> FindByNameIncludeDetailsAsync(string name); |
|||
|
|||
Task<Tenant> FindWithIncludeDetailsAsync(Guid id); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public class TenantStore : ITenantStore, ITransientDependency |
|||
{ |
|||
private readonly ITenantRepository _tenantRepository; |
|||
private readonly IObjectMapper _objectMapper; |
|||
private readonly ICurrentTenant _currentTenant; |
|||
|
|||
public TenantStore( |
|||
ITenantRepository tenantRepository, |
|||
IObjectMapper objectMapper, |
|||
ICurrentTenant currentTenant) |
|||
{ |
|||
_tenantRepository = tenantRepository; |
|||
_objectMapper = objectMapper; |
|||
_currentTenant = currentTenant; |
|||
} |
|||
|
|||
public async Task<TenantInfo> FindAsync(string name) |
|||
{ |
|||
using (_currentTenant.Clear()) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!
|
|||
{ |
|||
var tenant = await _tenantRepository.FindByNameIncludeDetailsAsync(name); |
|||
if (tenant == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return _objectMapper.Map<Tenant, TenantInfo>(tenant); |
|||
} |
|||
} |
|||
|
|||
public async Task<TenantInfo> FindAsync(Guid id) |
|||
{ |
|||
using (_currentTenant.Clear()) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!
|
|||
{ |
|||
var tenant = await _tenantRepository.FindWithIncludeDetailsAsync(id); |
|||
if (tenant == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return _objectMapper.Map<Tenant, TenantInfo>(tenant); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
using Volo.Abp.MultiTenancy.EntityFrameworkCore; |
|||
|
|||
namespace Volo.Abp.MultiTenancy |
|||
{ |
|||
public class EfCoreTenantRepository : EfCoreRepository<IMultiTenancyDbContext, Tenant>, ITenantRepository |
|||
{ |
|||
public EfCoreTenantRepository(IDbContextProvider<IMultiTenancyDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public Task<Tenant> FindByNameIncludeDetailsAsync(string name) |
|||
{ |
|||
return DbSet |
|||
.Include(t => t.ConnectionStrings) //TODO: Why not creating a virtual Include method in EfCoreRepository and override to add included properties to be available for every query..?
|
|||
.FirstOrDefaultAsync(t => t.Name == name); |
|||
} |
|||
|
|||
public Task<Tenant> FindWithIncludeDetailsAsync(Guid id) |
|||
{ |
|||
return DbSet |
|||
.Include(t => t.ConnectionStrings) //TODO: Why not creating a virtual Include method in EfCoreRepository and override to add included properties to be available for every query..?
|
|||
.FirstOrDefaultAsync(t => t.Id == id); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue