Browse Source

Multitenancy completely refactored

pull/191/head
Halil İbrahim Kalkan 8 years ago
parent
commit
2aa25fc08d
  1. 14
      docs/Multi-Tenancy.md
  2. 1
      src/AbpDesk/AbpDesk.Web.Mvc/AbpDesk.Web.Mvc.csproj
  3. 20
      src/AbpDesk/AbpDesk.Web.Mvc/AbpDeskWebMvcModule.cs
  4. 13
      src/Volo.Abp.AspNetCore.MultiTenancy/Microsoft/AspNetCore/Builder/AbpAspNetCoreMultiTenancyApplicationBuilderExtensions.cs
  5. 5
      src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs
  6. 73
      src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/MultiTenancyMiddleware.cs
  7. 2
      src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Views/Shared/Components/AbpMenu/Default.cshtml
  8. 2
      src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs
  9. 21
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AsyncLocalCurrentTenantIdAccessor.cs
  10. 9
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ConfigurationStore/ConfigurationTenantStore.cs
  11. 103
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/CurrentTenant.cs
  12. 11
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ICurrentTenant.cs
  13. 13
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ICurrentTenantIdAccessor.cs
  14. 8
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ITenantStore.cs
  15. 79
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/MultiTenantConnectionStringResolver.cs
  16. 18
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantIdWrapper.cs
  17. 19
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantScope.cs
  18. 38
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantScopeProvider.cs
  19. 21
      src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantStoreExtensions.cs
  20. 1
      src/Volo.Abp.MultiTenancy.Domain/Volo.Abp.MultiTenancy.Domain.csproj
  21. 27
      src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/AbpMultiTenancyDomainMappingProfile.cs
  22. 7
      src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/AbpMultiTenancyDomainModule.cs
  23. 13
      src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/ITenantRepository.cs
  24. 4
      src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/Tenant.cs
  25. 52
      src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/TenantStore.cs
  26. 31
      src/Volo.Abp.MultiTenancy.EntityFrameworkCore/Volo/Abp/MultiTenancy/EfCoreTenantRepository.cs
  27. 1
      test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo.Abp.AspNetCore.MultiTenancy.Tests.csproj
  28. 7
      test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/App/AppModule.cs
  29. 2
      test/Volo.Abp.MultiTenancy.Tests/Volo.Abp.MultiTenancy.Tests.csproj
  30. 17
      test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/Data/MultiTenancy/MultiTenantConnectionStringResolver_Tests.cs
  31. 78
      test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/CurrentTenant_Tests.cs
  32. 3
      test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/MultiTenancyTestModule.cs

14
docs/Multi-Tenancy.md

@ -86,6 +86,10 @@ namespace MyCompany.MyProject
}
````
#### Change Current Tenant
TODO: ...
### Volo.Abp.MultiTenancy Package
Volo.Abp.MultiTenancy is the actual package that makes your application multi-tenant. Install it into your project using PMC:
@ -310,6 +314,16 @@ namespace MyCompany.MyProject
}
````
#### Multi-Tenancy Middleware
Volo.Abp.AspNetCore.MultiTenancy package includes the multi-tenancy middleware...
````C#
app.UseMultiTenancy();
````
TODO:...
#### Determining Current Tenant From Web Request
Volo.Abp.AspNetCore.MultiTenancy package adds following tenant resolvers to determine current tenant from current web request (ordered by priority). These resolvers are added and work out of the box:

1
src/AbpDesk/AbpDesk.Web.Mvc/AbpDesk.Web.Mvc.csproj

@ -27,7 +27,6 @@
<ProjectReference Include="..\..\Volo.Abp.Identity.HttpApi\Volo.Abp.Identity.HttpApi.csproj" />
<ProjectReference Include="..\..\Volo.Abp.IdentityServer.Domain\Volo.Abp.IdentityServer.Domain.csproj" />
<ProjectReference Include="..\..\Volo.Abp.IdentityServer.EntityFrameworkCore\Volo.Abp.IdentityServer.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\Volo.Abp.MultiTenancy.Domain\Volo.Abp.MultiTenancy.Domain.csproj" />
<ProjectReference Include="..\AbpDesk.Application.Contracts\AbpDesk.Application.Contracts.csproj" />
<ProjectReference Include="..\..\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" />
<ProjectReference Include="..\AbpDesk.EntityFrameworkCore\AbpDesk.EntityFrameworkCore.csproj" />

20
src/AbpDesk/AbpDesk.Web.Mvc/AbpDeskWebMvcModule.cs

@ -27,8 +27,6 @@ using Volo.Abp.Modularity;
using Volo.Abp.Ui.Navigation;
using Volo.Abp.VirtualFileSystem;
using Volo.Abp.IdentityServer.Jwt;
using Volo.Abp.MultiTenancy;
using Volo.Abp.MultiTenancy.ConfigurationStore;
namespace AbpDesk.Web.Mvc
{
@ -68,22 +66,6 @@ namespace AbpDesk.Web.Mvc
AbpDeskDbConfigurer.Configure(services, configuration);
//TODO: Getting from appsettings.json didn't worked somehow.
services.Configure<ConfigurationTenantStoreOptions>(options =>
{
options.Tenants = new[]
{
new TenantInfo(
Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"),
"acme"
),
new TenantInfo(
Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"),
"volosoft"
)
};
});
services.Configure<NavigationOptions>(options =>
{
options.MenuContributors.Add(new MainMenuContributor());
@ -154,6 +136,8 @@ namespace AbpDesk.Web.Mvc
app.UseStaticFiles();
app.UseVirtualFiles();
app.UseMultiTenancy();
app.UseIdentityServer();
app.UseAuthentication();

13
src/Volo.Abp.AspNetCore.MultiTenancy/Microsoft/AspNetCore/Builder/AbpAspNetCoreMultiTenancyApplicationBuilderExtensions.cs

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

5
src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyModule.cs

@ -4,7 +4,10 @@ using Volo.Abp.MultiTenancy;
namespace Volo.Abp.AspNetCore.MultiTenancy
{
[DependsOn(typeof(AbpMultiTenancyAbstractionsModule), typeof(AbpAspNetCoreModule))]
[DependsOn(
typeof(AbpMultiTenancyAbstractionsModule),
typeof(AbpAspNetCoreModule)
)]
public class AbpAspNetCoreMultiTenancyModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)

73
src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/MultiTenancyMiddleware.cs

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

2
src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Views/Shared/Components/AbpMenu/Default.cshtml

@ -37,7 +37,7 @@
@if (CurrentTenant.IsAvailable)
{
<span>@CurrentTenant.Name?.ToString()</span>
<span>@CurrentTenant.Id?.ToString()</span>
}
else
{

2
src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs

@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Builder
.UseMiddleware<AbpUnitOfWorkMiddleware>();
}
public static IApplicationBuilder UseAbpExceptionHandling(this IApplicationBuilder app)
public static IApplicationBuilder UseAbpExceptionHandling(this IApplicationBuilder app) //TODO: Should this go to
{
//Prevent multiple add
if (app.Properties.ContainsKey("_AbpExceptionHandlingMiddleware_Added")) //TODO: Constant

21
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AsyncLocalCurrentTenantIdAccessor.cs

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

9
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ConfigurationStore/ConfigurationTenantStore.cs

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
@ -16,14 +17,14 @@ namespace Volo.Abp.MultiTenancy.ConfigurationStore
_options = options.Value;
}
public TenantInfo Find(string name)
public Task<TenantInfo> FindAsync(string name)
{
return _options.Tenants.FirstOrDefault(t => t.Name == name);
return Task.FromResult(_options.Tenants.FirstOrDefault(t => t.Name == name));
}
public TenantInfo Find(Guid id)
public Task<TenantInfo> FindAsync(Guid id)
{
return _options.Tenants.FirstOrDefault(t => t.Id == id);
return Task.FromResult(_options.Tenants.FirstOrDefault(t => t.Id == id));
}
}
}

103
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/CurrentTenant.cs

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

11
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ICurrentTenant.cs

@ -1,6 +1,5 @@
using System;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace Volo.Abp.MultiTenancy
{
@ -11,16 +10,8 @@ namespace Volo.Abp.MultiTenancy
[CanBeNull]
Guid? Id { get; }
[CanBeNull]
string Name { get; }
[CanBeNull]
ConnectionStrings ConnectionStrings { get; }
[NotNull]
IDisposable Change(Guid? id);
[NotNull]
IDisposable Change([CanBeNull] string name);
IDisposable Clear();
}
}

13
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ICurrentTenantIdAccessor.cs

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

8
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ITenantStore.cs

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

79
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/MultiTenantConnectionStringResolver.cs

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.MultiTenancy
{
@ -9,52 +12,72 @@ namespace Volo.Abp.MultiTenancy
public class MultiTenantConnectionStringResolver : DefaultConnectionStringResolver
{
private readonly ICurrentTenant _currentTenant;
private readonly IServiceProvider _serviceProvider;
public MultiTenantConnectionStringResolver(
IOptionsSnapshot<DbConnectionOptions> options,
ICurrentTenant currentTenant)
ICurrentTenant currentTenant,
IServiceProvider serviceProvider)
: base(options)
{
_currentTenant = currentTenant;
_serviceProvider = serviceProvider;
}
public override string Resolve(string connectionStringName = null)
{
var tenantConnectionStrings = _currentTenant.ConnectionStrings;
//No current tenant, fallback to default logic
if (tenantConnectionStrings == null)
if (_currentTenant.Id == null)
{
return base.Resolve(connectionStringName);
}
//Requesting default connection string
if (connectionStringName == null)
using (var serviceScope = _serviceProvider.CreateScope())
{
return tenantConnectionStrings.Default ??
Options.ConnectionStrings.Default;
}
var tenantStore = serviceScope
.ServiceProvider
.GetRequiredService<ITenantStore>();
//Requesting specific connection string
var connString = tenantConnectionStrings.GetOrDefault(connectionStringName);
if (connString != null)
{
return connString;
}
var tenant = AsyncHelper.RunSync(() => tenantStore.FindAsync(_currentTenant.Id.Value)); //TODO: Can we avoid from RunSync?
/* Requested a specific connection string, but it's not specified for the tenant.
* - If it's specified in options, use it.
* - If not, use tenant's default conn string.
*/
var connStringInOptions = Options.ConnectionStrings.GetOrDefault(connectionStringName);
if (connStringInOptions != null)
{
return connStringInOptions;
}
if (tenant == null)
{
return base.Resolve(connectionStringName);
}
return tenantConnectionStrings.Default ??
Options.ConnectionStrings.Default;
if (tenant.ConnectionStrings == null)
{
return base.Resolve(connectionStringName);
}
//Requesting default connection string
if (connectionStringName == null)
{
return tenant.ConnectionStrings.Default ??
Options.ConnectionStrings.Default;
}
//Requesting specific connection string
var connString = tenant.ConnectionStrings.GetOrDefault(connectionStringName);
if (connString != null)
{
return connString;
}
/* Requested a specific connection string, but it's not specified for the tenant.
* - If it's specified in options, use it.
* - If not, use tenant's default conn string.
*/
var connStringInOptions = Options.ConnectionStrings.GetOrDefault(connectionStringName);
if (connStringInOptions != null)
{
return connStringInOptions;
}
return tenant.ConnectionStrings.Default ??
Options.ConnectionStrings.Default;
}
}
}
}

18
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantIdWrapper.cs

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

19
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantScope.cs

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

38
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantScopeProvider.cs

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

21
src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantStoreExtensions.cs

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

1
src/Volo.Abp.MultiTenancy.Domain/Volo.Abp.MultiTenancy.Domain.csproj

@ -14,6 +14,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\Volo.Abp.Data\Volo.Abp.Data.csproj" />
<ProjectReference Include="..\Volo.Abp.Ddd\Volo.Abp.Ddd.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Abstractions\Volo.Abp.MultiTenancy.Abstractions.csproj" />

27
src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/AbpMultiTenancyDomainMappingProfile.cs

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

7
src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/AbpMultiTenancyDomainModule.cs

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AutoMapper;
using Volo.Abp.Data;
using Volo.Abp.Modularity;
@ -8,10 +9,16 @@ namespace Volo.Abp.MultiTenancy
[DependsOn(typeof(AbpMultiTenancyDomainSharedModule))]
[DependsOn(typeof(AbpDataModule))]
[DependsOn(typeof(AbpDddModule))]
[DependsOn(typeof(AbpAutoMapperModule))]
public class AbpMultiTenancyDomainModule : AbpModule
{
public override void ConfigureServices(IServiceCollection services)
{
services.Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpMultiTenancyDomainMappingProfile>(validate: true);
});
services.AddAssemblyOf<AbpMultiTenancyDomainModule>();
}
}

13
src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/ITenantRepository.cs

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

4
src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/Tenant.cs

@ -8,9 +8,9 @@ namespace Volo.Abp.MultiTenancy
{
public class Tenant : AggregateRoot
{
public string Name { get; protected set; }
public virtual string Name { get; protected set; }
public List<TenantConnectionString> ConnectionStrings { get; protected set; }
public virtual List<TenantConnectionString> ConnectionStrings { get; protected set; }
protected Tenant()
{

52
src/Volo.Abp.MultiTenancy.Domain/Volo/Abp/MultiTenancy/TenantStore.cs

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

31
src/Volo.Abp.MultiTenancy.EntityFrameworkCore/Volo/Abp/MultiTenancy/EfCoreTenantRepository.cs

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

1
test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo.Abp.AspNetCore.MultiTenancy.Tests.csproj

@ -13,7 +13,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.MultiTenancy\Volo.Abp.AspNetCore.MultiTenancy.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.MultiTenancy.Domain\Volo.Abp.MultiTenancy.Domain.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Tests\Volo.Abp.AspNetCore.Tests.csproj" />
</ItemGroup>

7
test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/App/AppModule.cs

@ -13,8 +13,7 @@ namespace Volo.Abp.AspNetCore.App
{
[DependsOn(
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpMultiTenancyDomainModule)
typeof(AbpAspNetCoreTestBaseModule)
)]
public class AppModule : AbpModule
{
@ -29,7 +28,9 @@ namespace Volo.Abp.AspNetCore.App
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseMultiTenancy();
app.Run(async (ctx) =>
{
var currentTenant = ctx.RequestServices.GetRequiredService<ICurrentTenant>();

2
test/Volo.Abp.MultiTenancy.Tests/Volo.Abp.MultiTenancy.Tests.csproj

@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.MultiTenancy.Domain\Volo.Abp.MultiTenancy.Domain.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.MultiTenancy.Abstractions\Volo.Abp.MultiTenancy.Abstractions.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
</ItemGroup>

17
test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/Data/MultiTenancy/MultiTenantConnectionStringResolver_Tests.cs

@ -9,15 +9,18 @@ namespace Volo.Abp.Data.MultiTenancy
{
public class MultiTenantConnectionStringResolver_Tests : MultiTenancyTestBase
{
private readonly ICurrentTenant _currentTenant;
private readonly Guid _tenant1Id = Guid.NewGuid();
private readonly Guid _tenant2Id = Guid.NewGuid();
private readonly IConnectionStringResolver _connectionResolver;
private readonly ICurrentTenant _currentTenant;
public MultiTenantConnectionStringResolver_Tests()
{
_currentTenant = ServiceProvider.GetRequiredService<ICurrentTenant>();
_connectionResolver = ServiceProvider.GetRequiredService<IConnectionStringResolver>();
_connectionResolver.ShouldBeOfType<MultiTenantConnectionStringResolver>();
_currentTenant = ServiceProvider.GetRequiredService<ICurrentTenant>();
}
protected override void BeforeAddApplication(IServiceCollection services)
@ -32,7 +35,7 @@ namespace Volo.Abp.Data.MultiTenancy
{
options.Tenants = new[]
{
new TenantInfo(Guid.NewGuid(), "tenant1")
new TenantInfo(_tenant1Id, "tenant1")
{
ConnectionStrings =
{
@ -40,7 +43,7 @@ namespace Volo.Abp.Data.MultiTenancy
{"db1", "tenant1-db1-value"}
}
},
new TenantInfo(Guid.NewGuid(), "tenant2")
new TenantInfo(_tenant2Id, "tenant2")
};
});
}
@ -53,7 +56,7 @@ namespace Volo.Abp.Data.MultiTenancy
_connectionResolver.Resolve("db1").ShouldBe("db1-default-value");
//Overrided connection strings for tenant1
using (_currentTenant.Change("tenant1"))
using (_currentTenant.Change(_tenant1Id))
{
_connectionResolver.Resolve().ShouldBe("tenant1-default-value");
_connectionResolver.Resolve("db1").ShouldBe("tenant1-db1-value");
@ -64,7 +67,7 @@ namespace Volo.Abp.Data.MultiTenancy
_connectionResolver.Resolve("db1").ShouldBe("db1-default-value");
//Undefined connection strings for tenant2
using (_currentTenant.Change("tenant2"))
using (_currentTenant.Change(_tenant2Id))
{
_connectionResolver.Resolve().ShouldBe("default-value");
_connectionResolver.Resolve("db1").ShouldBe("db1-default-value");

78
test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/CurrentTenant_Tests.cs

@ -10,9 +10,8 @@ namespace Volo.Abp.MultiTenancy
{
private readonly ICurrentTenant _currentTenant;
private readonly string _tenantA = "A";
private readonly string _tenantB = "B";
private string _tenantToBeResolved;
private readonly Guid _tenantAId = Guid.NewGuid();
private readonly Guid _tenantBId = Guid.NewGuid();
public CurrentTenant_Tests()
{
@ -33,87 +32,36 @@ namespace Volo.Abp.MultiTenancy
{
options.Tenants = new[]
{
new TenantInfo(Guid.NewGuid(), _tenantA),
new TenantInfo(Guid.NewGuid(), _tenantB)
new TenantInfo(_tenantAId, "A"),
new TenantInfo(_tenantAId, "B")
};
});
}
protected override void AfterAddApplication(IServiceCollection services)
{
services.Configure<TenantResolveOptions>(options =>
{
options.TenantResolvers.Add(new ActionTenantResolveContributer(context =>
{
if (_tenantToBeResolved == _tenantA)
{
context.TenantIdOrName = _tenantA;
}
}));
options.TenantResolvers.Add(new ActionTenantResolveContributer(context =>
{
if (_tenantToBeResolved == _tenantB)
{
context.TenantIdOrName = _tenantB;
}
}));
});
}
[Fact]
public void Should_Get_Current_Tenant_From_Single_Resolver()
{
//Arrange
_tenantToBeResolved = _tenantA;
//Assert
Assert.NotNull(_currentTenant.Id);
_currentTenant.Name.ShouldBe(_tenantA);
}
[Fact]
public void Should_Get_Current_Tenant_From_Multiple_Resolvers()
public void Should_Get_Null_If_Not_Set()
{
//Arrange
_tenantToBeResolved = _tenantB;
//Assert
Assert.NotNull(_currentTenant.Id);
_currentTenant.Name.ShouldBe(_tenantB);
_currentTenant.Id.ShouldBeNull();
}
[Fact]
public void Should_Get_Changed_Tenant_If_Wanted()
public void Should_Get_Changed_Tenant_If()
{
_currentTenant.Id.ShouldBe(null);
_tenantToBeResolved = _tenantB;
Assert.NotNull(_currentTenant.Id);
_currentTenant.Name.ShouldBe(_tenantB);
using (_currentTenant.Change(_tenantA))
using (_currentTenant.Change(_tenantAId))
{
Assert.NotNull(_currentTenant.Id);
_currentTenant.Name.ShouldBe(_tenantA);
_currentTenant.Id.ShouldBe(_tenantAId);
using (_currentTenant.Change(_tenantB))
using (_currentTenant.Change(_tenantBId))
{
Assert.NotNull(_currentTenant.Id);
_currentTenant.Name.ShouldBe(_tenantB);
_currentTenant.Id.ShouldBe(_tenantBId);
}
Assert.NotNull(_currentTenant.Id);
_currentTenant.Name.ShouldBe(_tenantA);
_currentTenant.Id.ShouldBe(_tenantAId);
}
Assert.NotNull(_currentTenant.Id);
_currentTenant.Name.ShouldBe(_tenantB);
_currentTenant.Id.ShouldBeNull();
}
}
}

3
test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/MultiTenancyTestModule.cs

@ -2,7 +2,8 @@
namespace Volo.Abp.MultiTenancy
{
[DependsOn(typeof(AbpMultiTenancyDomainModule))]
//TODO: Renaming this project to Volo.Abp.MultiTenancy.Tests would be better!
[DependsOn(typeof(AbpMultiTenancyAbstractionsModule))]
public class MultiTenancyTestModule : AbpModule
{

Loading…
Cancel
Save