[Abp vNext 源码分析] - 19. 多租户
一、简介
ABP vNext 原生支持多租户体系,可以让开发人员快速地基于框架开发 SaaS 系统。ABP vNext 实现多租户的思路也非常简单,通过一个 TenantId
来分割各个租户的数据,并且在查询的时候使用统一的全局过滤器(类似于软删除)来筛选数据。
关于多租户体系的东西,基本定义与核心逻辑存放在 Volo.ABP.MultiTenancy 内部。针对 ASP.NET Core MVC 的集成则是由 Volo.ABP.AspNetCore.MultiTenancy 项目实现的,针对多租户的解析都在这个项目内部。租户数据的存储和管理都由 Volo.ABP.TenantManagement 模块提供,开发人员也可以直接使用该项目快速实现多租户功能。
二、源码分析
2.1 启动模块
AbpMultiTenancyModule
模块是启用整个多租户功能的核心模块,内部只进行了一个动作,就是从配置类当中读取多租户的基本信息,以 JSON Provider 为例,就需要在 appsettings.json
里面有 Tenants
节。
"Tenants": [
{
"Id": "446a5211-3d72-4339-9adc-845151f8ada0",
"Name": "tenant1"
},
{
"Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d",
"Name": "tenant2",
"ConnectionStrings": {
"Default": "...write tenant2's db connection string here..."
}
}
]
2.1.1 默认租户来源
这里的数据将会作为默认租户来源,也就是说在确认当前租户的时候,会从这里面的数据与要登录的租户进行比较,如果不存在则不允许进行操作。
public interface ITenantStore
{
Task<TenantConfiguration> FindAsync(string name);
Task<TenantConfiguration> FindAsync(Guid id);
TenantConfiguration Find(string name);
TenantConfiguration Find(Guid id);
}
默认的存储实现:
[Dependency(TryRegister = true)]
public class DefaultTenantStore : ITenantStore, ITransientDependency
{
// 直接从 Options 当中获取租户数据。
private readonly AbpDefaultTenantStoreOptions _options;
public DefaultTenantStore(IOptionsSnapshot<AbpDefaultTenantStoreOptions> options)
{
_options = options.Value;
}
public Task<TenantConfiguration> FindAsync(string name)
{
return Task.FromResult(Find(name));
}
public Task<TenantConfiguration> FindAsync(Guid id)
{
return Task.FromResult(Find(id));
}
public TenantConfiguration Find(string name)
{
return _options.Tenants?.FirstOrDefault(t => t.Name == name);
}
public TenantConfiguration Find(Guid id)
{
return _options.Tenants?.FirstOrDefault(t => t.Id == id);
}
}
除了从配置文件当中读取租户信息以外,开发人员也可以自己实现 ITenantStore
接口,比如说像 TenantManagement 一样,将租户信息存储到数据库当中。
2.1.2 基于数据库的租户存储
话接上文,我们说过在 Volo.ABP.TenantManagement 模块内部有提供另一种 ITenantStore
接口的实现,这个类型叫做 TenantStore
,内部逻辑也很简单,就是从仓储当中查找租户数据。
public class TenantStore : ITenantStore, ITransientDependency
{
private readonly ITenantRepository _tenantRepository;
private readonly IObjectMapper<AbpTenantManagementDomainModule> _objectMapper;
private readonly ICurrentTenant _currentTenant;
public TenantStore(
ITenantRepository tenantRepository,
IObjectMapper<AbpTenantManagementDomainModule> objectMapper,
ICurrentTenant currentTenant)
{
_tenantRepository = tenantRepository;
_objectMapper = objectMapper;
_currentTenant = currentTenant;
}
public async Task<TenantConfiguration> FindAsync(string name)
{
// 变更当前租户为租主。
using (_currentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!
{
// 通过仓储查询租户是否存在。
var tenant = await _tenantRepository.FindByNameAsync(name);
if (tenant == null)
{
return null;
}
// 将查询到的信息转换为核心库定义的租户信息。
return _objectMapper.Map<Tenant, TenantConfiguration>(tenant);
}
}
// ... 其他的代码已经省略。
}
可以看到,最后也是返回的一个 TenantConfiguration
类型。关于这个类型,是 ABP 在多租户核心库定义的一个基本类型之一,主要是用于规定持久化一个租户信息需要包含的属性。
[Serializable]
public class TenantConfiguration
{
// 租户的 Guid。
public Guid Id { get; set; }
// 租户的名称。
public string Name { get; set; }
// 租户对应的数据库连接字符串。
public ConnectionStrings ConnectionStrings { get; set; }
public TenantConfiguration()
{
}
public TenantConfiguration(Guid id, [NotNull] string name)
{
Check.NotNull(name, nameof(name));
Id = id;
Name = name;
ConnectionStrings = new ConnectionStrings();
}
}
2.2 租户的解析
ABP vNext 如果要判断当前的租户是谁,则是通过 AbpTenantResolveOptions
提供的一组 ITenantResolveContributor
进行处理的。
public class AbpTenantResolveOptions
{
// 会使用到的这组解析对象。
[NotNull]
public List<ITenantResolveContributor> TenantResolvers { get; }
public AbpTenantResolveOptions()
{
TenantResolvers = new List<ITenantResolveContributor>
{
// 默认的解析对象,会通过 Token 内字段解析当前租户。
new CurrentUserTenantResolveContributor()
};
}
}
这里的设计与权限一样,都是由一组 解析对象(解析器) 进行处理,在上层开放的入口只有一个 ITenantResolver
,内部通过 foreach
执行这组解析对象的 Resolve()
方法。
下面就是我们 ITenantResolver
的默认实现 TenantResolver
,你可以在任何时候调用它。比如说你在想要获得当前租户 Id 的时候。不过一般不推荐这样做,因为 ABP 已经给我们提供了 MultiTenancyMiddleware
中间件。
也就是说,在每次请求的时候,都会将这个 Id
通过 ICurrentTenant.Change()
进行变更,那么在这个请求执行完成之前,通过 ICurrentTenant
取得的 Id
都会是解析器解析出来的 Id。
public class TenantResolver : ITenantResolver, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
private readonly AbpTenantResolveOptions _options;
public TenantResolver(IOptions<AbpTenantResolveOptions> options, IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_options = options.Value;
}
public TenantResolveResult ResolveTenantIdOrName()
{
var result = new TenantResolveResult();
using (var serviceScope = _serviceProvider.CreateScope())
{
// 创建一个解析上下文,用于存储解析器的租户 Id 解析结果。
var context = new TenantResolveContext(serviceScope.ServiceProvider);
// 遍历执行解析器。
foreach (var tenantResolver in _options.TenantResolvers)
{
tenantResolver.Resolve(context);
result.AppliedResolvers.Add(tenantResolver.Name);
// 如果有某个解析器为上下文设置了值,则跳出。
if (context.HasResolvedTenantOrHost())
{
result.TenantIdOrName = context.TenantIdOrName;
break;
}
}
}
return result;
}
}
2.2.1 默认的解析对象
如果不使用 Volo.Abp.AspNetCore.MultiTenancy 模块,ABP vNext 会调用 CurrentUserTenantResolveContributor
解析当前操作的租户。
public class CurrentUserTenantResolveContributor : TenantResolveContributorBase
{
public const string ContributorName = "CurrentUser";
public override string Name => ContributorName;
public override void Resolve(ITenantResolveContext context)
{
// 从 Token 当中获取当前登录用户的信息。
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
if (currentUser.IsAuthenticated != true)
{
return;
}
// 设置解析上下文,确认当前的租户 Id。
context.Handled = true;
context.TenantIdOrName = currentUser.TenantId?.ToString();
}
}
在这里可以看到,如果从 Token 当中解析到了租户 Id,会将这个 Id 传递给 解析上下文。这个上下文在最开始已经遇到过了,如果 ABP vNext 在解析的时候发现租户 Id 被确认了,就不会执行剩下的解析器。
2.2.2 ABP 提供的其他解析器
ABP 在 Volo.Abp.AspNetCore.MultiTenancy 模块当中还提供了其他几种解析器,他们的作用分别如下。
解析器类型 | 作用 | 优先级 |
---|---|---|
QueryStringTenantResolveContributor |
通过 Query String 的 __tenant 参数确认租户。 |
2 |
RouteTenantResolveContributor |
通过路由判断当前租户。 | 3 |
HeaderTenantResolveContributor |
通过 Header 里面的 __tenant 确认租户。 |
4 |
CookieTenantResolveContributor |
通过携带的 Cookie 确认租户。 | 5 |
DomainTenantResolveContributor |
二级域名解析器,通过二级域名确定租户。 | 第二 |
2.2.3 域名解析器
这里比较有意思的是 DomainTenantResolveContributor
,开发人员可以通过 AbpTenantResolveOptions.AddDomainTenantResolver()
方法添加这个解析器。 域名解析器会通过解析二级域名来匹配对应的租户,例如我针对租户 A 分配了一个二级域名 http://a.system.com
,那么这个 a 就会被作为租户名称解析出来,最后传递给 ITenantResolver
解析器作为结果。
注意:
在使用 Header 作为租户信息提供者的时候,开发人员使用的是 NGINX 作为反向代理服务器 时,需要在对应的 config 文件内部配置
underscores_in_headers on;
选项。否则 ABP 所需要的__tenantId
将会被过滤掉,或者你可以指定一个没有下划线的 Key。
域名解析器的详细代码解释:
public class DomainTenantResolveContributor : HttpTenantResolveContributorBase
{
public const string ContributorName = "Domain";
public override string Name => ContributorName;
private static readonly string[] ProtocolPrefixes = { "http://", "https://" };
private readonly string _domainFormat;
// 使用指定的格式来确定租户前缀,例如 “{0}.abp.io”。
public DomainTenantResolveContributor(string domainFormat)
{
_domainFormat = domainFormat.RemovePreFix(ProtocolPrefixes);
}
protected override string GetTenantIdOrNameFromHttpContextOrNull(
ITenantResolveContext context,
HttpContext httpContext)
{
// 如果 Host 值为空,则不进行任何操作。
if (httpContext.Request?.Host == null)
{
return null;
}
// 解析具体的域名信息,并进行匹配。
var hostName = httpContext.Request.Host.Host.RemovePreFix(ProtocolPrefixes);
// 这里的 FormattedStringValueExtracter 类型是 ABP 自己实现的一个格式化解析器。
var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, ignoreCase: true);
context.Handled = true;
if (!extractResult.IsMatch)
{
return null;
}
return extractResult.Matches[0].Value;
}
}
从上述代码可以知道,域名解析器是基于 HttpTenantResolveContributorBase
基类进行处理的,这个抽象基类会取得当前请求的一个 HttpContext
,将这个传递与解析上下文一起传递给子类实现,由子类实现负责具体的解析逻辑。
public abstract class HttpTenantResolveContributorBase : TenantResolveContributorBase
{
public override void Resolve(ITenantResolveContext context)
{
// 获取当前请求的上下文。
var httpContext = context.GetHttpContext();
if (httpContext == null)
{
return;
}
try
{
ResolveFromHttpContext(context, httpContext);
}
catch (Exception e)
{
context.ServiceProvider
.GetRequiredService<ILogger<HttpTenantResolveContributorBase>>()
.LogWarning(e.ToString());
}
}
protected virtual void ResolveFromHttpContext(ITenantResolveContext context, HttpContext httpContext)
{
// 调用抽象方法,获取具体的租户 Id 或名称。
var tenantIdOrName = GetTenantIdOrNameFromHttpContextOrNull(context, httpContext);
if (!tenantIdOrName.IsNullOrEmpty())
{
// 获得到租户标识之后,填充到解析上下文。
context.TenantIdOrName = tenantIdOrName;
}
}
protected abstract string GetTenantIdOrNameFromHttpContextOrNull([NotNull] ITenantResolveContext context, [NotNull] HttpContext httpContext);
}
2.3 租户信息的传递
租户解析器通过一系列的解析对象,获取到了租户或租户 Id 之后,会将这些数据给哪些对象呢?或者说,ABP 在什么地方调用了 租户解析器,答案就是 中间件。
在 Volo.ABP.AspNetCore.MultiTenancy 模块的内部,提供了一个 MultiTenancyMiddleware
中间件。
开发人员如果需要使用 ASP.NET Core 的多租户相关功能,也可以引入该模块。并且在模块的 OnApplicationInitialization()
方法当中,使用 IApplicationBuilder.UseMultiTenancy()
进行启用。
这里在启用的时候,需要注意中间件的顺序和位置,不要放到最末尾进行处理。
public class MultiTenancyMiddleware : IMiddleware, ITransientDependency
{
private readonly ITenantResolver _tenantResolver;
private readonly ITenantStore _tenantStore;
private readonly ICurrentTenant _currentTenant;
private readonly ITenantResolveResultAccessor _tenantResolveResultAccessor;
public MultiTenancyMiddleware(
ITenantResolver tenantResolver,
ITenantStore tenantStore,
ICurrentTenant currentTenant,
ITenantResolveResultAccessor tenantResolveResultAccessor)
{
_tenantResolver = tenantResolver;
_tenantStore = tenantStore;
_currentTenant = currentTenant;
_tenantResolveResultAccessor = tenantResolveResultAccessor;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 通过租户解析器,获取当前请求的租户信息。
var resolveResult = _tenantResolver.ResolveTenantIdOrName();
_tenantResolveResultAccessor.Result = resolveResult;
TenantConfiguration tenant = null;
// 如果当前请求是属于租户请求。
if (resolveResult.TenantIdOrName != null)
{
// 查询指定的租户 Id 或名称是否存在,不存在则抛出异常。
tenant = await FindTenantAsync(resolveResult.TenantIdOrName);
if (tenant == null)
{
//TODO: A better exception?
throw new AbpException(
"There is no tenant with given tenant id or name: " + resolveResult.TenantIdOrName
);
}
}
// 在接下来的请求当中,将会通过 ICurrentTenant.Change() 方法变更当前租户,直到
// 请求结束。
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
{
await next(context);
}
}
private async Task<TenantConfiguration> FindTenantAsync(string tenantIdOrName)
{
// 如果可以格式化为 Guid ,则说明是租户 Id。
if (Guid.TryParse(tenantIdOrName, out var parsedTenantId))
{
return await _tenantStore.FindAsync(parsedTenantId);
}
else
{
return await _tenantStore.FindAsync(tenantIdOrName);
}
}
}
在取得了租户的标识(Id 或名称)之后,将会通过 ICurrentTenant.Change()
方法变更当前租户的信息,变更了当租户信息以后,在程序的其他任何地方使用 ICurrentTenant.Id
取得的数据都是租户解析器解析出来的数据。
下面就是这个当前租户的具体实现,可以看到这里采用了一个 经典手法-嵌套。这个手法在工作单元和数据过滤器有见到过,结合 DisposeAction()
在 using
语句块结束的时候把当前的租户 Id 值设置为父级 Id。即在同一个语句当中,可以通过嵌套 using
语句块来处理不同的租户。
using(_currentTenant.Change("A"))
{
Logger.LogInformation(_currentTenant.Id);
using(_currentTenant.Change("B"))
{
Logger.LogInformation(_currentTenant.Id);
}
}
具体的实现代码,这里的 ICurrentTenantAccessor
内部实现就是一个 AsyncLocal<BasicTenantInfo>
,用于在一个异步请求内部进行数据传递。
public class CurrentTenant : ICurrentTenant, ITransientDependency
{
public virtual bool IsAvailable => Id.HasValue;
public virtual Guid? Id => _currentTenantAccessor.Current?.TenantId;
public string Name => _currentTenantAccessor.Current?.Name;
private readonly ICurrentTenantAccessor _currentTenantAccessor;
public CurrentTenant(ICurrentTenantAccessor currentTenantAccessor)
{
_currentTenantAccessor = currentTenantAccessor;
}
public IDisposable Change(Guid? id, string name = null)
{
return SetCurrent(id, name);
}
private IDisposable SetCurrent(Guid? tenantId, string name = null)
{
var parentScope = _currentTenantAccessor.Current;
_currentTenantAccessor.Current = new BasicTenantInfo(tenantId, name);
return new DisposeAction(() =>
{
_currentTenantAccessor.Current = parentScope;
});
}
}
这里的 BasicTenantInfo
与 TenantConfiguraton
不同,前者仅用于在程序当中传递用户的基本信息,而后者是用于定于持久化的标准模型。
2.4 租户的使用
2.4.1 数据库过滤
租户的核心作用就是隔离不同客户的数据,关于过滤的基本逻辑则是存放在 AbpDbContext<TDbContext>
的。从下面的代码可以看到,在使用的时候会从注入一个 ICurrentTenant
接口,这个接口可以获得从租户解析器里面取得的租户 Id 信息。并且还有一个 IsMultiTenantFilterEnabled()
方法来判定当前 是否应用租户过滤器。
public abstract class AbpDbContext<TDbContext> : DbContext, IEfCoreDbContext, ITransientDependency
where TDbContext : DbContext
{
protected virtual Guid? CurrentTenantId => CurrentTenant?.Id;
protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled<IMultiTenant>() ?? false;
// ... 其他的代码。
public ICurrentTenant CurrentTenant { get; set; }
// ... 其他的代码。
protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() where TEntity : class
{
// 定义一个 Lambda 表达式。
Expression<Func<TEntity, bool>> expression = null;
// 如果聚合根/实体实现了软删除接口,则构建一个软删除过滤器。
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
expression = e => !IsSoftDeleteFilterEnabled || !EF.Property<bool>(e, "IsDeleted");
}
// 如果聚合根/实体实现了多租户接口,则构建一个多租户过滤器。
if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
{
// 筛选 TenantId 为 CurrentTenantId 的数据。
Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, "TenantId") == CurrentTenantId;
expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter);
}
return expression;
}
// ... 其他的代码。
}
2.4.2 种子数据构建
在 Volo.ABP.TenantManagement 模块当中,如果用户创建了一个租户,ABP 不只是在租户表插入一条新数据而已。它还会设置种子数据的 构造上下文,并且执行所有的 种子数据构建者(IDataSeedContributor
)。
[Authorize(TenantManagementPermissions.Tenants.Create)]
public virtual async Task<TenantDto> CreateAsync(TenantCreateDto input)
{
var tenant = await TenantManager.CreateAsync(input.Name);
await TenantRepository.InsertAsync(tenant);
using (CurrentTenant.Change(tenant.Id, tenant.Name))
{
//TODO: Handle database creation?
//TODO: Set admin email & password..?
await DataSeeder.SeedAsync(tenant.Id);
}
return ObjectMapper.Map<Tenant, TenantDto>(tenant);
}
这些构建者当中,就包括租户的超级管理员(admin)和角色构建,以及针对超级管理员角色进行权限赋值操作。
这里需要注意第二点,如果开发人员没有指定超级管理员用户和密码,那么还是会使用默认密码为租户生成超级管理员,具体原因看如下代码。
public class IdentityDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IIdentityDataSeeder _identityDataSeeder;
public IdentityDataSeedContributor(IIdentityDataSeeder identityDataSeeder)
{
_identityDataSeeder = identityDataSeeder;
}
public Task SeedAsync(DataSeedContext context)
{
return _identityDataSeeder.SeedAsync(
context["AdminEmail"] as string ?? "admin@abp.io",
context["AdminPassword"] as string ?? "1q2w3E*",
context.TenantId
);
}
}
所以开发人员要实现为不同租户 生成随机密码,那么就不能够使用 TenantManagement 提供的创建方法,而是需要自己编写一个应用服务进行处理。
2.4.3 权限的控制
如果开发人员使用了 ABP 提供的 Volo.Abp.PermissionManagement 模块,就会看到在它的种子数据构造者当中会对权限进行判定。因为有一些 超级权限 是租主才能够授予的,例如租户的增加、删除、修改等,这些超级权限在定义的时候就需要说明是否是数据租主独有的。
关于这点,可以参考租户管理模块在权限定义时,传递的 MultiTenancySides.Host
参数。
public class AbpTenantManagementPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var tenantManagementGroup = context.AddGroup(TenantManagementPermissions.GroupName, L("Permission:TenantManagement"));
var tenantsPermission = tenantManagementGroup.AddPermission(TenantManagementPermissions.Tenants.Default, L("Permission:TenantManagement"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Create, L("Permission:Create"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Update, L("Permission:Edit"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Delete, L("Permission:Delete"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageFeatures, L("Permission:ManageFeatures"), multiTenancySide: MultiTenancySides.Host);
tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageConnectionStrings, L("Permission:ManageConnectionStrings"), multiTenancySide: MultiTenancySides.Host);
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<AbpTenantManagementResource>(name);
}
}
下面是权限种子数据构造者的代码:
public class PermissionDataSeedContributor : IDataSeedContributor, ITransientDependency
{
protected ICurrentTenant CurrentTenant { get; }
protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
protected IPermissionDataSeeder PermissionDataSeeder { get; }
public PermissionDataSeedContributor(
IPermissionDefinitionManager permissionDefinitionManager,
IPermissionDataSeeder permissionDataSeeder,
ICurrentTenant currentTenant)
{
PermissionDefinitionManager = permissionDefinitionManager;
PermissionDataSeeder = permissionDataSeeder;
CurrentTenant = currentTenant;
}
public virtual Task SeedAsync(DataSeedContext context)
{
// 通过 GetMultiTenancySide() 方法判断当前执行
// 种子构造者的租户情况,是租主还是租户。
var multiTenancySide = CurrentTenant.GetMultiTenancySide();
// 根据条件筛选权限。
var permissionNames = PermissionDefinitionManager
.GetPermissions()
.Where(p => p.MultiTenancySide.HasFlag(multiTenancySide))
.Select(p => p.Name)
.ToArray();
// 将权限授予具体租户的角色。
return PermissionDataSeeder.SeedAsync(
RolePermissionValueProvider.ProviderName,
"admin",
permissionNames,
context.TenantId
);
}
}
而 ABP 在判断当前是租主还是租户的方法也很简单,如果当前租户 Id 为 NULL 则说明是租主,如果不为空则说明是具体租户。
public static MultiTenancySides GetMultiTenancySide(this ICurrentTenant currentTenant)
{
return currentTenant.Id.HasValue
? MultiTenancySides.Tenant
: MultiTenancySides.Host;
}
2.4.4 租户的独立设置
关于这块的内容,可以参考之前的 这篇文章 ,ABP 也为我们提供了各个租户独立的自定义参数在,这块功能是由 TenantSettingManagementProvider
实现的,只需要在设置参数值的时候提供租户的 ProviderName
即可。
例如:
settingManager.SetAsync("WeChatIsOpen", "true", TenantSettingValueProvider.ProviderName, tenantId.ToString(), false);
三、总结
其他相关文章,请参阅 文章目录 。
[Abp vNext 源码分析] - 19. 多租户的更多相关文章
- [Abp vNext 源码分析] - 文章目录
一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...
- [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)
一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...
- [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)
一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...
- [Abp vNext 源码分析] - 7. 权限与验证
一.简要说明 在上篇文章里面,我们在 ApplicationService 当中看到了权限检测代码,通过注入 IAuthorizationService 就可以实现权限检测.不过跳转到源码才发现,这个 ...
- [Abp vNext 源码分析] - 11. 用户的自定义参数与配置
一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 23 日晚 更新日期:暂无 ABP vNext 针对用户可编辑的配置,提供了单独的 Volo. ...
- [Abp vNext 源码分析] - 14. EntityFramework Core 的集成
一.简要介绍 在以前的文章里面,我们介绍了 ABP vNext 在 DDD 模块定义了仓储的接口定义和基本实现.本章将会介绍,ABP vNext 是如何将 EntityFramework Core 框 ...
- [Abp vNext 源码分析] - 23. 二进制大对象系统(BLOB)
一.简介 ABP vNext 在 v 2.9.x 版本当中添加了 BLOB 系统,主要用于存储大型二进制文件.ABP 抽象了一套通用的 BLOB 体系,开发人员在存储或读取二进制文件时,可以忽略具体实 ...
- [Abp vNext 源码分析] - 20. 电子邮件与短信支持
一.简介 ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包将短信和电子邮件作为基础设施进行了抽象,开发人员仅需要在使用的时候注入 ISmsSender ...
随机推荐
- Java多线程常见概念
进程和线程的区别 进程是资源分配的最小单位,线程是CPU调度的最小单位 线程不能看做独立应用,而进程可以 进程有独立的地址空间,互相不影响,线程只是进程的不同执行路径 线程没有独立的地址空间,多进程的 ...
- 吴裕雄--天生自然 JAVA开发学习:集合框架
import java.util.*; public class Test{ public static void main(String[] args) { List<String> l ...
- E. Alice and the Unfair Game(推导线段树)
题:https://codeforces.com/contest/1236/problem/E 粗自:https://www.cnblogs.com/YSFAC/p/11715522.html #in ...
- CCS|ANSI|中华人民共和国标准化法|国标|ISO|IEC|Ieeexplore|
国家的标准的有效期,标龄是5年.强制性标准是是指为保障人体的健康.人身.财产安全的标准和法律.行政法规定强制执行的标准,如药品标准.食品卫生标准. CCS:分类法简写图 国际标准,比如美国国家标准AN ...
- SpringBoot中使用task实现定时任务
定时任务实现的几种方式: Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务.使用这种方式可以让你的程序按照某一个频度执行, ...
- php7 安装mongodb扩展
下载 mongodb-1.6.0.tgz wget https://pecl.php.net/get/mongodb-1.6.0.tgz 版本太低的话有些语法不一样,起码1.5以上吧 进入 mo ...
- [LC] 520. Detect Capital
Given a word, you need to judge whether the usage of capitals in it is right or not. We define the u ...
- [LC] 167. Two Sum II - Input array is sorted
Given an array of integers that is already sorted in ascending order, find two numbers such that the ...
- C++ List的用法
Lists将元素按顺序储存在链表中. 与 向量(vectors)相比, 它允许快速的插入和删除,但是随机访问却比较慢. assign() 给list赋值 back() 返回最后一个元素 begin() ...
- scarky test