一、简介

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

这里的 BasicTenantInfoTenantConfiguraton 不同,前者仅用于在程序当中传递用户的基本信息,而后者是用于定于持久化的标准模型。

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. 多租户的更多相关文章

  1. [Abp vNext 源码分析] - 文章目录

    一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...

  2. [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)

    一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...

  3. [Abp vNext 源码分析] - 2. 模块系统的变化

    一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...

  4. [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)

    一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...

  5. [Abp vNext 源码分析] - 7. 权限与验证

    一.简要说明 在上篇文章里面,我们在 ApplicationService 当中看到了权限检测代码,通过注入 IAuthorizationService 就可以实现权限检测.不过跳转到源码才发现,这个 ...

  6. [Abp vNext 源码分析] - 11. 用户的自定义参数与配置

    一.简要说明 文章信息: 基于的 ABP vNext 版本:1.0.0 创作日期:2019 年 10 月 23 日晚 更新日期:暂无 ABP vNext 针对用户可编辑的配置,提供了单独的 Volo. ...

  7. [Abp vNext 源码分析] - 14. EntityFramework Core 的集成

    一.简要介绍 在以前的文章里面,我们介绍了 ABP vNext 在 DDD 模块定义了仓储的接口定义和基本实现.本章将会介绍,ABP vNext 是如何将 EntityFramework Core 框 ...

  8. [Abp vNext 源码分析] - 23. 二进制大对象系统(BLOB)

    一.简介 ABP vNext 在 v 2.9.x 版本当中添加了 BLOB 系统,主要用于存储大型二进制文件.ABP 抽象了一套通用的 BLOB 体系,开发人员在存储或读取二进制文件时,可以忽略具体实 ...

  9. [Abp vNext 源码分析] - 20. 电子邮件与短信支持

    一.简介 ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包将短信和电子邮件作为基础设施进行了抽象,开发人员仅需要在使用的时候注入 ISmsSender ...

随机推荐

  1. k8s miniKube 入门

    k8s miniKube 入门 miniKube 是单机版kubernetes, 可以配置运行在同一台主机上的服务和pod,并使用docker作为虚拟化工具 下载:直接下载可执行文件,复制到path ...

  2. [SDOI2015]道路修建(线段树)

    题意:给定2行n列的四连通带权网格图,支持修改边权和查询第[l,r]列的最小生成树 题解:这是一道好题,要么SDOI2019中n=2的20pts怎么会“我抄我自己”?(当然NOIP2018“我抄我自己 ...

  3. Flink(三) —— 运行架构

    Flink运行时组件 JobManager 作业管理器 TaskManager 任务管理器 ResourceManager 资源管理器 Dispatcher 分发器 任务提交流程 任务调度原理 Job ...

  4. Linux 运维命令及知识

    1.查找当前目录下所有以.tar结尾的文件然后移动到指定目录: find . -name “*.tar” -exec mv {}./backup/ ; 注解:find –name 主要用于查找某个文件 ...

  5. [LC] 146. LRU Cache

    Design and implement a data structure for Least Recently Used (LRU) cache. It should support the fol ...

  6. 吴裕雄--天生自然C语言开发:递归

    void recursion() { statements; ... ... ... recursion(); /* 函数调用自身 */ ... ... ... } int main() { recu ...

  7. vim模式及基础命令

    VIM基本介绍vi和在修改vim命令是linux中强大的文本编辑器,由于linux系统一切皆文件,而配置一个服务就是在修改其配置文件的参数vim其实是vi的升级版yum install -y vim ...

  8. 九成AI企业亏损,人工智能商业落地为何这么难?

    自1956年"人工智能"一词诞生于"达特茅斯会议"后,前者就始终在不断向前推进.虽然中间经历了不少低谷和寒潮,但总算挺了过来.60多年后,人工智能在当下呈现突飞 ...

  9. 《C程序设计语言》练习1-10

    #include<stdio.h> main() { int c; c=getchar(); while (c !=EOF) { if (c=='\t') { c='\\'; putcha ...

  10. Python常用的数据结构详解

    数据结构:通俗点说,就是储存大量数据的容器.这里主要介绍Python的4种基本数据结构:列表.字典.元组.集合. 格式如下: 列表:list = [val1,val2,val3,val4],用中括号: ...