feat: 添加多租户模块

This commit is contained in:
陈淳
2023-03-02 10:19:18 +08:00
parent 5cea38e95c
commit 0127b43374
21 changed files with 560 additions and 6 deletions

View File

@@ -0,0 +1,24 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 租户信息
/// </summary>
public class BasicTenantInfo
{
/// <summary>
/// 租户ID
/// </summary>
public Guid? TenantId { get; }
/// <summary>
/// 租户ID 名称
/// </summary>
public string Name { get; }
/// <summary/>
public BasicTenantInfo(Guid? tenantId, string? name = null)
{
TenantId = tenantId;
Name = name ?? string.Empty;
}
}

View File

@@ -0,0 +1,60 @@
using Yi.Framework.Core.Utils;
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 当前租户实现
/// </summary>
public class CurrentTenant : ICurrentTenant
{
private readonly ICurrentTenantAccessor _currentTenantAccessor;
/// <summary/>
public CurrentTenant(ICurrentTenantAccessor currentTenantAccessor)
{
_currentTenantAccessor = currentTenantAccessor;
}
/// <summary>
/// 是否有效
/// </summary>
public virtual bool IsAvailable => Id != Guid.Empty;
/// <summary>
/// 租户ID
/// </summary>
public virtual Guid Id => _currentTenantAccessor.Current?.TenantId ?? Guid.Empty;
/// <summary>
/// 租户名称
/// </summary>
public string? Name => _currentTenantAccessor.Current?.Name;
/// <summary>
/// 替换租户
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
/// <returns></returns>
public IDisposable Change(Guid? id, string? name = null)
{
return SetCurrent(id, name);
}
/// <summary>
/// 设置当前租户
/// </summary>
/// <returns></returns>
private IDisposable SetCurrent(Guid? tenantId, string? name = null)
{
BasicTenantInfo? parentScope = _currentTenantAccessor.Current;
_currentTenantAccessor.Current = new BasicTenantInfo(tenantId, name);
return new DisposeAction<ValueTuple<ICurrentTenantAccessor, BasicTenantInfo>>(static ((ICurrentTenantAccessor, BasicTenantInfo) state) =>
{
(ICurrentTenantAccessor currentTenantAccessor, BasicTenantInfo parentScope) = state;
currentTenantAccessor.Current = parentScope;
},
(_currentTenantAccessor, parentScope));
}
}

View File

@@ -0,0 +1,28 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 默认租户访问器实现
/// </summary>
public class DefaultCurrentTenantAccessor : ICurrentTenantAccessor
{
private ITenantResolver _tenantResolver;
/// <summary/>
public DefaultCurrentTenantAccessor(ITenantResolver tenantResolver)
{
_tenantResolver = tenantResolver;
TenantResolveResult? tenantResolveResult = _tenantResolver.ResolveTenantIdOrNameAsync().Result;
string? tenantIdStr = tenantResolveResult.TenantIdOrName;
Current = Guid.TryParse(tenantIdStr, out Guid tehnantId)
? new BasicTenantInfo(tehnantId)
: new BasicTenantInfo(Guid.Empty);
}
/// <summary>
/// 当前租户信息
/// </summary>
public BasicTenantInfo Current { get; set; }
}

View File

@@ -0,0 +1,42 @@
using Microsoft.Extensions.DependencyInjection;
using Yi.Framework.MultiTenancy.ResolveContributor;
namespace Yi.Framework.MultiTenancy.Extensions;
/// <summary>
/// 租户注入扩展方法
/// </summary>
public static class MultiTenancyExtensions
{
/// <summary>
/// 注入 租户
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddCurrentTenant(this IServiceCollection services)
{
services.Configure<TenantResolveOptions>(option =>
{
//添加租户解析器,默认添加从当前用户中获取
//添加从httpheader解析TenantId配置的租户id
option.TenantResolvers.Add(new HttpHeaderTenantResolveContributor());
//添加配置租户解析器解析TenantId配置的租户id
option.TenantResolvers.Add(new ConfigurationTenantResolveContributor());
});
//添加租户解析器注入
services.AddTransient<ITenantResolver, TenantResolver>();
//添加当前租户
services.AddTransient<ICurrentTenant, CurrentTenant>();
//添加默认访问器
services.AddTransient<ICurrentTenantAccessor, DefaultCurrentTenantAccessor>();
return services;
}
}

View File

@@ -0,0 +1,29 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 当前租户接口
/// </summary>
public interface ICurrentTenant
{
/// <summary>
/// 是否有效
/// </summary>
bool IsAvailable { get; }
/// <summary>
/// 租户ID
/// </summary>
Guid Id { get; }
/// <summary>
/// 租户名称
/// </summary>
string? Name { get; }
/// <summary>
/// 替换租户
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
IDisposable Change(Guid? id, string? name = null);
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 租户访问器
/// </summary>
public interface ICurrentTenantAccessor
{
/// <summary>
/// 当前租户信息
/// </summary>
BasicTenantInfo Current { get; set; }
}

View File

@@ -0,0 +1,21 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 解析器上下文
/// (作用于各个<see cref="ITenantResolveContributor.ResolveAsync(ITenantResolveContext)"/>)之间
/// </summary>
public interface ITenantResolveContext
{
/// <summary/>
IServiceProvider ServiceProvider { get; }
/// <summary>
/// 租户ID 或 名称
/// </summary>
string TenantIdOrName { get; set; }
/// <summary>
/// 是否已处理
/// </summary>
bool Handled { get; set; }
}

View File

@@ -0,0 +1,19 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 租户解析器贡献者
/// </summary>
public interface ITenantResolveContributor
{
/// <summary>
/// 贡献者名称
/// </summary>
string Name { get; }
/// <summary>
/// 解析
/// </summary>
/// <param name="context">解析器上下文</param>
/// <returns></returns>
Task ResolveAsync(ITenantResolveContext context);
}

View File

@@ -0,0 +1,13 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 租户解析器接口
/// </summary>
public interface ITenantResolver
{
/// <summary>
/// 解析租户Id或名称
/// </summary>
/// <returns></returns>
Task<TenantResolveResult> ResolveTenantIdOrNameAsync();
}

View File

@@ -0,0 +1,41 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Yi.Framework.Data.Entities;
using Yi.Framework.MultiTenancy;
namespace Yi.Framework.MultiTenancy.ResolveContributor;
/// <summary>
/// <see cref="IConfiguration"/>租户解析器贡献者
/// </summary>
public class ConfigurationTenantResolveContributor : TenantResolveContributorBase
{
/// <summary>
/// 租户解析器贡献者基类
/// </summary>
public override string Name => "Configuration";
/// <summary>
/// 解析
/// </summary>
/// <param name="context">解析器上下文</param>
/// <returns></returns>
public override Task ResolveAsync(ITenantResolveContext context)
{
IConfiguration? configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
string? tenantIdStr = configuration.GetValue<string>(nameof(IMultiTenant.TenantId));
if (Guid.TryParse(tenantIdStr, out Guid tenantId))
{
if (tenantId != Guid.Empty)
{
context.TenantIdOrName = tenantId.ToString();
context.Handled = true;
}
}
context.Handled = false;
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.Extensions.DependencyInjection;
using Yi.Framework.Core.CurrentUsers;
namespace Yi.Framework.MultiTenancy.ResolveContributor;
/// <summary>
/// 当前用户中获取租户
/// </summary>
public class CurrentUserTenantResolveContributor : TenantResolveContributorBase
{
/// <summary>
/// 贡献者名称
/// </summary>
public const string ContributorName = "CurrentUser";
/// <summary>
/// 贡献者名称
/// </summary>
public override string Name => ContributorName;
/// <summary>
/// 解析
/// </summary>
/// <param name="context">解析器上下文</param>
/// <returns></returns>
public override Task ResolveAsync(ITenantResolveContext context)
{
ICurrentUser currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
if (currentUser.TenantId != Guid.Empty)
{
context.Handled = true;
context.TenantIdOrName = currentUser.TenantId.ToString();
}
else
{
context.Handled = false;
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,52 @@

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Yi.Framework.Data.Entities;
using Yi.Framework.MultiTenancy;
namespace Yi.Framework.MultiTenancy.ResolveContributor;
/// <summary>
/// <see cref="HttpContext"/>租户解析器贡献者
/// </summary>
public class HttpHeaderTenantResolveContributor : TenantResolveContributorBase
{
/// <summary>
/// 贡献者名称
/// </summary>
public override string Name => "HttpHeader";
/// <summary>
/// 解析
/// </summary>
/// <param name="context">解析器上下文</param>
/// <returns></returns>
public override Task ResolveAsync(ITenantResolveContext context)
{
IHttpContextAccessor? httpContextAccessor = context.ServiceProvider.GetService<IHttpContextAccessor>();
//如果没有注入http对象直接跳出
if (httpContextAccessor is null)
{
return Task.CompletedTask;
}
HttpContext? httpContext = httpContextAccessor.HttpContext;
if (httpContext is not null)
{
string? tenantId = httpContext.Request.Headers
.Where(x => x.Key == nameof(IMultiTenant.TenantId))
.Select(x => x.Value.ToString())
.FirstOrDefault();
if (tenantId is not null)
{
if (Guid.TryParse(tenantId, out Guid tid))
{
context.TenantIdOrName = tid.ToString();
context.Handled = true;
}
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,35 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 解析器上下文
/// </summary>
public class TenantResolveContext : ITenantResolveContext
{
/// <summary/>
public TenantResolveContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
/// <summary/>
public IServiceProvider ServiceProvider { get; }
/// <summary>
/// 租户ID 或 名称
/// </summary>
public string TenantIdOrName { get; set; }
/// <summary>
/// 是否已处理
/// </summary>
public bool Handled { get; set; }
/// <summary>
/// 是否已经处理或者<see cref="TenantIdOrName"/>为有效值
/// </summary>
public bool HasResolvedTenantOrHost()
{
return Handled
|| TenantIdOrName != Guid.Empty.ToString() && !string.IsNullOrWhiteSpace(TenantIdOrName);
}
}

View File

@@ -0,0 +1,19 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 租户解析器贡献者基类
/// </summary>
public abstract class TenantResolveContributorBase : ITenantResolveContributor
{
/// <summary>
/// 贡献者名称
/// </summary>
public abstract string Name { get; }
/// <summary>
/// 解析
/// </summary>
/// <param name="context">解析器上下文</param>
/// <returns></returns>
public abstract Task ResolveAsync(ITenantResolveContext context);
}

View File

@@ -0,0 +1,27 @@

using Yi.Framework.MultiTenancy.ResolveContributor;
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 解析器属性
/// </summary>
public class TenantResolveOptions
{
/// <summary>
/// 构造函数
/// </summary>
public TenantResolveOptions()
{
TenantResolvers = new List<ITenantResolveContributor>
{
new CurrentUserTenantResolveContributor()
};
}
/// <summary>
/// 解析器贡献者。由这帮东西为框架提供 租户ID
/// </summary>
public List<ITenantResolveContributor> TenantResolvers { get; }
}

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.MultiTenancy;
/// <summary>
/// 租户解析器结果
/// </summary>
public class TenantResolveResult
{
/// <summary>
/// 租户ID 或 名称
/// </summary>
public string TenantIdOrName { get; set; } = Guid.Empty.ToString();
/// <summary>
/// 存储遍历过的<see cref="ITenantResolveContributor.Name"/>
/// </summary>
public List<string> AppliedResolvers { get; } = new List<string>();
}

View File

@@ -0,0 +1,49 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Yi.Framework.MultiTenancy;
/// <summary>
///
/// </summary>
public class TenantResolver : ITenantResolver
{
private readonly IServiceProvider _serviceProvider;
private readonly TenantResolveOptions _options;
/// <summary>
/// 构造函数
/// </summary>
public TenantResolver(IOptions<TenantResolveOptions> options, IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_options = options.Value;
}
/// <summary>
/// 解析租户Id或名称
/// </summary>
public virtual async Task<TenantResolveResult> ResolveTenantIdOrNameAsync()
{
TenantResolveResult? result = new TenantResolveResult();
using (IServiceScope? serviceScope = _serviceProvider.CreateScope())
{
TenantResolveContext? context = new TenantResolveContext(serviceScope.ServiceProvider);
foreach (ITenantResolveContributor tenantResolver in _options.TenantResolvers)
{
await tenantResolver.ResolveAsync(context);
result.AppliedResolvers.Add(tenantResolver.Name);
if (context.HasResolvedTenantOrHost())
{
result.TenantIdOrName = context.TenantIdOrName;
break;
}
}
}
return result;
}
}

View File

@@ -6,4 +6,9 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Yi.Framework.Core\Yi.Framework.Core.csproj" />
<ProjectReference Include="..\Yi.Framework.Data\Yi.Framework.Data.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,20 @@
namespace Yi.Framework.MultiTenancy
{
public class YiFrameworkMultiTenancyModule
{
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using StartupModules;
using Yi.Framework.MultiTenancy.Extensions;
namespace Yi.Framework.MultiTenancy
{
public class YiFrameworkMultiTenancyModule : IStartupModule
{
public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context)
{
}
public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context)
{
services.AddCurrentTenant();
}
}
}

View File

@@ -1,9 +1,15 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using StartupModules;
using Yi.Framework.Core.Attributes;
using Yi.Framework.Core;
using Yi.Framework.Ddd;
namespace Yi.Framework.FileManager
{
[DependsOn(
typeof(YiFrameworkDddModule)
)]
public class YiFrameworkFileManagerModule : IStartupModule
{
public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context)

View File

@@ -1,12 +1,14 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using StartupModules;
using Yi.Framework.Core;
using Yi.Framework.Core.Attributes;
using Yi.Framework.Ddd;
namespace Yi.Framework.OperLogManager
{
[DependsOn(typeof(YiFrameworkCoreModule))]
[DependsOn(
typeof(YiFrameworkDddModule)
)]
public class YiFrameworkOperLogManagerModule : IStartupModule
{
public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context)