18 changed files with 1527 additions and 126 deletions
@ -0,0 +1,5 @@ |
|||
document.querySelectorAll(".zoom").forEach((item) => { |
|||
item.addEventListener("click", function () { |
|||
this.classList.toggle("image-zoom-large"); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,18 @@ |
|||
/* 图片放大start */ |
|||
.shadow { |
|||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); |
|||
} |
|||
|
|||
.zoom { |
|||
transition: transform ease-in-out 0.5s; |
|||
cursor: zoom-in; |
|||
} |
|||
|
|||
.image-zoom-large { |
|||
transform: scale(1.9); |
|||
cursor: zoom-out; |
|||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); |
|||
z-index: 100; |
|||
position: relative; |
|||
} |
|||
/* 图片放大end https://shafish.cn/blog/mkdocs/#%E5%9B%9B%E9%83%A8%E7%BD%B2*/ |
|||
|
After Width: | Height: | Size: 58 KiB |
@ -1,41 +1,27 @@ |
|||
Title: Abp Vnext Pro |
|||
Title: Abp vNext Pro |
|||
|
|||
# Abp Vnext Pro |
|||
|
|||
<img width="140" align="right" src="/img/logo.svg"> |
|||
<img width="140" align="right" src="img/logo.svg"> |
|||
[](https://ci.appveyor.com/project/yuleyule66/cap/branch/master) |
|||
[](https://github.com/WangJunZzz/abp-vnext-pro/blob/main/LICENSE) |
|||
|
|||
## 介绍 |
|||
|
|||
- 完全免费开源 |
|||
- ABP vNext 的前身是 asp.net boilerplate(老版 abp),它不是一个简单的版本更新,而是完全基于.NET Core 的重写。 |
|||
- ABP vNext 社区版的 UI 不使用于实际项目开发。为此,结合 Vben Admin 打造开箱即用的 ABP vNext Pro。 |
|||
|
|||
## 演示 |
|||
|
|||
- [演示地址](http://116.205.177.85/login) |
|||
|
|||
## 系统功能 |
|||
|
|||
- [x] 用户管理 |
|||
- [x] 角色管理 |
|||
- [x] 审计日志 |
|||
- [x] 后台任务 |
|||
- [x] 集成事件 |
|||
- [x] SinglaR 消息通知(站内信) |
|||
- [x] 多语言 |
|||
- [x] 数据字典 |
|||
- [x] 容器化部署 |
|||
- [x] 单元测试 |
|||
- [x] ES 日志 |
|||
- [x] Setting 管理 |
|||
- [x] 多租户 |
|||
- [x] 文件管理 |
|||
- [x] 多语言管理 |
|||
|
|||
 |
|||
 |
|||
 |
|||
 |
|||
 |
|||
 |
|||
Abp vNext Pro 是一个基于 Vben Admin, Abp vNext 打造的一个开箱即用的中后台管理系统解决方案。觉得该项目对您有帮助,请再 Github 上给我一颗:octicons-heart-fill-24:{ .heart } |
|||
|
|||
[快速开始 :fontawesome-solid-paper-plane:](user-guide/zh/getting-started/quick-start.md){ .md-button } |
|||
|
|||
## Abp Vnext Pro |
|||
|
|||
[演示 :fontawesome-solid-paper-plane:](http://116.205.177.85/login){ .md-button } |
|||
[GitHub :fontawesome-solid-paper-plane:](https://github.com/WangJunZzz/abp-vnext-pro){ .md-button } |
|||
|
|||
## Abp Vnext Pro Suite |
|||
|
|||
> 基于 Abp Vnext Pro 开发的代码生成器,可自动生成前后端 CURD 代码,提供生产力。 |
|||
|
|||
[演示 :fontawesome-solid-paper-plane:](http://116.205.177.85:81/login){ .md-button } |
|||
[GitHub :fontawesome-solid-paper-plane:](https://github.com/WangJunZzz/abp-vnext-pro-suite){ .md-button } |
|||
|
|||
## 视频教程 |
|||
|
|||
[B 站 :fontawesome-solid-paper-plane:](https://www.bilibili.com/video/BV1pt4y1E7aZ/?spm_id_from=333.337.search-card.all.click&vd_source=963e10b4fc37556dd738bd98e1d46a0f){ .md-button } |
|||
|
|||
@ -0,0 +1,142 @@ |
|||
# 对象到对象映射 |
|||
|
|||
将对象映射到另一个对象是常用并且繁琐重复的工作,大部分情况下两个类都具有相同或相似的属性. ABP 提供了对象到对象映射的抽象并集成了[AutoMapper](http://automapper.org/)做为对象映射器. |
|||
|
|||
## AutoMapper 集成 |
|||
|
|||
[AutoMapper](http://automapper.org/) 是最流行的对象到对象映射库之一. [Volo.Abp.AutoMapper](https://www.nuget.org/packages/Volo.Abp.AutoMapper)程序包使用 AutoMapper 实现了 `IObjectMapper`. |
|||
|
|||
### 定义映射 |
|||
|
|||
AutoMapper 提供了多种定义类之间映射的方法. 有关详细信息请参阅[AutoMapper 的文档](https://docs.automapper.org). |
|||
|
|||
其中定义一种映射的方法是创建一个[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) 类. 例如: |
|||
|
|||
```csharp |
|||
public class MyProfile : Profile |
|||
{ |
|||
public MyProfile() |
|||
{ |
|||
CreateMap<User, UserDto>(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
然后使用`AbpAutoMapperOptions`注册配置文件: |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpAutoMapperModule))] |
|||
public class MyModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpAutoMapperOptions>(options => |
|||
{ |
|||
//Add all mappings defined in the assembly of the MyModule class |
|||
options.AddMaps<MyModule>(); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`AddMaps` 注册给定类的程序集中所有的配置类,通常使用模块类. 它还会注册 [attribute 映射](https://docs.automapper.org/en/stable/Attribute-mapping.html). |
|||
|
|||
### 配置验证 |
|||
|
|||
`AddMaps` 使用可选的 `bool` 参数控制[模块](Module-Development-Basics.md)的[配置验证](https://docs.automapper.org/en/stable/Configuration-validation.html): |
|||
|
|||
```csharp |
|||
options.AddMaps<MyModule>(validate: true); |
|||
``` |
|||
|
|||
如果此选项默认是 `false` , 但最佳实践建议启用. |
|||
可以使用 `AddProfile` 而不是 `AddMaps` 来控制每个配置文件类的配置验证: |
|||
|
|||
```csharp |
|||
options.AddProfile<MyProfile>(validate: true); |
|||
``` |
|||
|
|||
> 如果你有多个配置文件,并且只需要为其中几个启用验证,那么首先使用`AddMaps`而不进行验证,然后为你想要验证的每个配置文件使用`AddProfile`. |
|||
|
|||
### 映射对象扩展 |
|||
|
|||
[对象扩展系统](Object-Extensions.md) 允许为已存在的类定义额外属性. ABP 框架提供了一个映射定义扩展可以正确的映射两个对象的额外属性. |
|||
|
|||
```csharp |
|||
public class MyProfile : Profile |
|||
{ |
|||
public MyProfile() |
|||
{ |
|||
CreateMap<User, UserDto>() |
|||
.MapExtraProperties(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
如果两个类都是可扩展对象(实现了 `IHasExtraProperties` 接口),建议使用 `MapExtraProperties` 方法. 更多信息请参阅[对象扩展文档](Object-Extensions.md). |
|||
|
|||
### 其他有用的扩展方法 |
|||
|
|||
有一些扩展方法可以简化映射代码. |
|||
|
|||
#### 忽视审计属性 |
|||
|
|||
当你将一个对象映射到另一个对象时,通常会忽略审核属性. |
|||
|
|||
假设你需要将 `ProductDto` ([DTO](Data-Transfer-Objects.md))映射到 Product[实体](Entities.md),该实体是从 `AuditedEntity` 类继承的(该类提供了 `CreationTime`, `CreatorId`, `IHasModificationTime` 等属性). |
|||
|
|||
从 DTO 映射时你可能想忽略这些基本属性,可以使用 `IgnoreAuditedObjectPropertie()` 方法忽略所有审计属性(而不是手动逐个忽略它们): |
|||
|
|||
```csharp |
|||
public class MyProfile : Profile |
|||
{ |
|||
public MyProfile() |
|||
{ |
|||
CreateMap<ProductDto, Product>() |
|||
.IgnoreAuditedObjectProperties(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
还有更多扩展方法, 如 `IgnoreFullAuditedObjectProperties()` 和 `IgnoreCreationAuditedObjectProperties()`,你可以根据实体类型使用. |
|||
|
|||
> 请参阅[实体文档](Entities.md)中的"_基类和接口的审计属性_"部分了解有关审计属性的更多信息。 |
|||
|
|||
#### 忽视其他属性 |
|||
|
|||
在 AutoMapper 中,通常可以编写这样的映射代码来忽略属性: |
|||
|
|||
```csharp |
|||
public class MyProfile : Profile |
|||
{ |
|||
public MyProfile() |
|||
{ |
|||
CreateMap<SimpleClass1, SimpleClass2>() |
|||
.ForMember(x => x.CreationTime, map => map.Ignore()); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
我们发现它的长度是不必要的并且创建了 `Ignore()` 扩展方法: |
|||
|
|||
```csharp |
|||
public class MyProfile : Profile |
|||
{ |
|||
public MyProfile() |
|||
{ |
|||
CreateMap<SimpleClass1, SimpleClass2>() |
|||
.Ignore(x => x.CreationTime); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 使用 |
|||
|
|||
```csharp |
|||
// 注入IObjectMapper |
|||
public virtual async Task<LanguageDto> GetAsync(string cultureName) |
|||
{ |
|||
var entity = await _languageRepository.FindAsync(cultureName); |
|||
return ObjectMapper.Map<Language, LanguageDto>(entity); |
|||
} |
|||
``` |
|||
@ -0,0 +1,37 @@ |
|||
## 跨域(CORS) |
|||
|
|||
- 允许指定策略 |
|||
|
|||
```json title="appsetting.json" |
|||
"App": { |
|||
// 逗号分隔 |
|||
"CorsOrigins": "http://*.com,http://localhost:4200" |
|||
}, |
|||
``` |
|||
|
|||
- 配置跨域 |
|||
|
|||
```csharp |
|||
private void ConfigureCors(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
context.Services.AddCors(options => |
|||
{ |
|||
options.AddPolicy(DefaultCorsPolicyName, builder => |
|||
{ |
|||
builder |
|||
.WithOrigins( |
|||
configuration["App:CorsOrigins"] |
|||
.Split(",", StringSplitOptions.RemoveEmptyEntries) |
|||
.Select(o => o.RemovePostFix("/")) |
|||
.ToArray() |
|||
) |
|||
.WithAbpExposedHeaders() |
|||
.SetIsOriginAllowedToAllowWildcardSubdomains() |
|||
.AllowAnyHeader() |
|||
.AllowAnyMethod() |
|||
.AllowCredentials(); |
|||
}); |
|||
}); |
|||
} |
|||
``` |
|||
@ -0,0 +1,167 @@ |
|||
# 当前用户 |
|||
|
|||
在 Web 应用程序中检索有关已登录用户的信息是很常见的. 当前用户是与 Web 应用程序中的当前请求相关的活动用户. |
|||
|
|||
## ICurrentUser |
|||
|
|||
`ICurrentUser` 是主要的服务,用于获取有关当前活动的用户信息. |
|||
|
|||
示例: [注入] `ICurrentUser` 到服务中: |
|||
|
|||
```csharp |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ICurrentUser _currentUser; |
|||
|
|||
public MyService(ICurrentUser currentUser) |
|||
{ |
|||
_currentUser = currentUser; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
Guid? userId = _currentUser.Id; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
公共基类已经将此服务作为基本属性注入. 例如你可以直接在应用服务中使用 `CurrentUser` 属性: |
|||
|
|||
```csharp |
|||
using System; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyAppService : ApplicationService |
|||
{ |
|||
public void Foo() |
|||
{ |
|||
Guid? userId = CurrentUser.Id; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 属性 |
|||
|
|||
以下是 `ICurrentUser` 接口的基本属性: |
|||
|
|||
- **IsAuthenticated** 如果当前用户已登录(已认证),则返回 `true`. 如果用户尚未登录,则 `Id` 和 `UserName` 将返回 `null`. |
|||
- **Id** (Guid?): 当前用户的 Id,如果用户未登录,返回 `null`. |
|||
- **UserName** (string): 当前用户的用户名称. 如果用户未登录,返回 `null`. |
|||
- **TenantId** (Guid?): 当前用户的租户 Id. 对于[多租户](Multi-Tenancy.md) 应用程序很有用. 如果当前用户未分配给租户,返回 `null`. |
|||
- **Email** (string): 当前用户的电子邮件地址. 如果当前用户尚未登录或未设置电子邮件地址,返回 `null`. |
|||
- **EmailVerified** (bool): 如果当前用户的电子邮件地址已经过验证,返回 `true`. |
|||
- **PhoneNumber** (string): 当前用户的电话号码. 如果当前用户尚未登录或未设置电话号码,返回 `null`. |
|||
- **PhoneNumberVerified** (bool): 如果当前用户的电话号码已经过验证,返回 `true`. |
|||
- **Roles** (string[]): 当前用户的角色. 返回当前用户角色名称的字符串数组. |
|||
|
|||
### Methods |
|||
|
|||
`ICurrentUser` 是在 `ICurrentPrincipalAccessor` 上实现的(请参阅以下部分),并可以处理声明. 实际上所有上述属性都是从当前经过身份验证的用户的声明中检索的. |
|||
|
|||
如果你有自定义声明或获取其他非常见声明类型, `ICurrentUser` 有一些直接使用声明的方法. |
|||
|
|||
- **FindClaim**: 获取给定名称的声明,如果未找到返回 `null`. |
|||
- **FindClaims**: 获取具有给定名称的所有声明(允许具有相同名称的多个声明值). |
|||
- **GetAllClaims**: 获取所有声明. |
|||
- **IsInRole**: 一种检查当前用户是否在指定角色中的简化方法. |
|||
|
|||
除了这些标准方法,还有一些扩展方法: |
|||
|
|||
- **FindClaimValue**: 获取具有给定名称的声明的值,如果未找到返回 `null`. 它有一个泛型重载将值强制转换为特定类型. |
|||
- **GetId**: 返回当前用户的 `Id`. 如果当前用户没有登录它会抛出一个异常(而不是返回`null`). 仅在你确定用户已经在你的代码上下文中进行了身份验证时才使用此选项. |
|||
|
|||
### 验证和授权 |
|||
|
|||
`ICurrentUser` 的工作方式与用户的身份验证或授权方式无关. 它可以与使用当前主体的任何身份验证系统无缝地配合使用(请参阅下面的部分). |
|||
|
|||
## ICurrentPrincipalAccessor |
|||
|
|||
`ICurrentPrincipalAccessor` 是当需要当前用户的 Principal 时使用的服务(由 ABP 框架和你的应用程序代码使用). |
|||
|
|||
对于 Web 应用程序, 它获取当前 `HttpContext` 的 `User` 属性,对于非 Web 应用程序它将返回 `Thread.CurrentPrincipal`. |
|||
|
|||
> 通常你不需要这种低级别的 `ICurrentPrincipalAccessor` 服务,直接使用上述的 `ICurrentUser` 即可. |
|||
|
|||
### 基本用法 |
|||
|
|||
你可以注入 `ICurrentPrincipalAccessor` 并且使用 `Principal` 属性获取当前 principal: |
|||
|
|||
```csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; |
|||
|
|||
public MyService(ICurrentPrincipalAccessor currentPrincipalAccessor) |
|||
{ |
|||
_currentPrincipalAccessor = currentPrincipalAccessor; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
var allClaims = _currentPrincipalAccessor.Principal.Claims.ToList(); |
|||
//... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 更改当前 Principal |
|||
|
|||
除了某些高级场景外,你不需要设置或更改当前 Principal. 如果需要可以使用 `ICurrentPrincipalAccessor` 的 `Change` 方法. 它接受一个 `ClaimsPrincipal` 对象并使其成为作用域的"当前"对象. |
|||
|
|||
示例: |
|||
|
|||
```csharp |
|||
public class MyAppService : ApplicationService |
|||
{ |
|||
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; |
|||
|
|||
public MyAppService(ICurrentPrincipalAccessor currentPrincipalAccessor) |
|||
{ |
|||
_currentPrincipalAccessor = currentPrincipalAccessor; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
var newPrincipal = new ClaimsPrincipal( |
|||
new ClaimsIdentity( |
|||
new Claim[] |
|||
{ |
|||
new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString()), |
|||
new Claim(AbpClaimTypes.UserName, "john"), |
|||
new Claim("MyCustomCliam", "42") |
|||
} |
|||
) |
|||
); |
|||
|
|||
using (_currentPrincipalAccessor.Change(newPrincipal)) |
|||
{ |
|||
var userName = CurrentUser.UserName; //returns "john" |
|||
//... |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
始终在 `using` 语句中使用 `Change` 方法,在 `using` 范围结束后它将恢复为原始值. |
|||
|
|||
这可以是一种模拟用户登录的应用程序代码范围的方法,但是请尝试谨慎使用它. |
|||
|
|||
## AbpClaimTypes |
|||
|
|||
`AbpClaimTypes` 是一个静态类它定义了标准声明的名称被 ABP 框架使用. |
|||
|
|||
- `UserName`, `UserId`, `Role` 和 `Email` 属性的默认值是通常[System.Security.Claims.ClaimTypes](https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes)类设置的, 但你可以改变它们. |
|||
|
|||
- 其他属性,如 `EmailVerified`, `PhoneNumber`, `TenantId` ...是由 ABP 框架通过尽可能遵循标准名称来定义的. |
|||
|
|||
建议使用这个类的属性来代替声明名称的魔术字符串. |
|||
@ -0,0 +1,169 @@ |
|||
# 数据过滤 |
|||
|
|||
[Volo.Abp.Data](https://www.nuget.org/packages/Volo.Abp.Data) 包定义了在查询数据库时自动过滤数据的服务. |
|||
|
|||
## 预定义的过滤 |
|||
|
|||
### ISoftDelete |
|||
|
|||
将实体标记为已删除,并不是物理删除. 实现 `ISoftDelete` 接口将你的实体"软删除". |
|||
|
|||
示例: |
|||
|
|||
```csharp |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class Book : AggregateRoot<Guid>, ISoftDelete |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public bool IsDeleted { get; set; } //Defined by ISoftDelete |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`ISoftDelete` 定义了 `IsDeleted` 属性. 当你使用仓储删除一条记录时, ABP 会自动将 `IsDeleted` 设置为 true,并将删除操作替换为修改操作(如果需要,也可以手动将 `IsDeleted` 设置为 true). 在查询数据库时会自动过滤软删除的实体. |
|||
|
|||
> `ISoftDelete` 过滤默认启用, 想要真正的从数据库删除实体需要显示的禁用过滤. 参见下面提到的 `IDataFilter` 服务. |
|||
|
|||
### IMultiTenant |
|||
|
|||
[多租户]是创建 SaaS 应用程序的有效方法. 多租户应用程序通常需要在租户间隔离数据. 实现 `IMultiTenant` 接口使你的实体支持 "多租户". |
|||
|
|||
示例: |
|||
|
|||
```csharp |
|||
namespace Acme.BookStore |
|||
{ |
|||
public class Book : AggregateRoot<Guid>, ISoftDelete, IMultiTenant |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public bool IsDeleted { get; set; } //Defined by ISoftDelete |
|||
|
|||
public Guid? TenantId { get; set; } //Defined by IMultiTenant |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`IMultiTenant` 接口定义了 `TenantId` 属性用于自动过滤当前租户实体. 更多信息参见[多租户]文档. |
|||
|
|||
## IDataFilter 服务: 启用/禁用 数据过滤 |
|||
|
|||
你可以使用 `IDataFilter` 服务控制数据过滤. |
|||
|
|||
示例: |
|||
|
|||
```csharp |
|||
namespace Acme.BookStore |
|||
{ |
|||
public class MyBookService : ITransientDependency |
|||
{ |
|||
private readonly IDataFilter _dataFilter; |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
|
|||
public MyBookService( |
|||
IDataFilter dataFilter, |
|||
IRepository<Book, Guid> bookRepository) |
|||
{ |
|||
_dataFilter = dataFilter; |
|||
_bookRepository = bookRepository; |
|||
} |
|||
|
|||
public async Task<List<Book>> GetAllBooksIncludingDeletedAsync() |
|||
{ |
|||
//Temporary disable the ISoftDelete filter |
|||
using (_dataFilter.Disable<ISoftDelete>()) |
|||
{ |
|||
return await _bookRepository.GetListAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- [注入] `IDataFilter` 服务到你的类中. |
|||
- 在 `using` 语句中使用 `Disable` 方法创建一个代码块,其中禁用了 `ISoftDelete` 过滤器(始终与 `using` 搭配使用,确保代码块执行后将过滤重置为之前的状态). |
|||
|
|||
`IDataFilter.Enable` 方法可以启用过滤. 可以嵌套使用 `Enable` 和 `Disable` 方法定义内部作用域. |
|||
|
|||
## AbpDataFilterOptions |
|||
|
|||
`AbpDataFilterOptions` 用于设置数据过滤系统. |
|||
|
|||
下面的示例代码在默认情况下禁用了 `ISoftDelete` 过滤,除非显示启用,在查询数据库时会包含标记为已删除的实体: |
|||
|
|||
```csharp |
|||
Configure<AbpDataFilterOptions>(options => |
|||
{ |
|||
options.DefaultStates[typeof(ISoftDelete)] = new DataFilterState(isEnabled: false); |
|||
}); |
|||
``` |
|||
|
|||
> 更改全局过滤的默认值需要小心,特别是在你使用预构建的模块时该模块可能是在默认启用软删除过滤的情况下开发的. 但你可以安全的为自己定义的数据过滤执行此操作. |
|||
|
|||
## 自定义数据过滤 |
|||
|
|||
定义和实现新的过滤很大程序上取决与数据库提供者. ABP 为所有的数据库提供者实现了预构建的过滤. |
|||
|
|||
首先为过滤定义一个接口 (如 `ISoftDelete` 和 `IMultiTenant`) 然后用实体实现它. |
|||
|
|||
示例: |
|||
|
|||
```csharp |
|||
public interface IIsActive |
|||
{ |
|||
bool IsActive { get; } |
|||
} |
|||
``` |
|||
|
|||
`IIsActive` 接口可以过滤活跃/消极数据,任何[实体]都可以实现它: |
|||
|
|||
```csharp |
|||
public class Book : AggregateRoot<Guid>, IIsActive |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public bool IsActive { get; set; } //Defined by IIsActive |
|||
} |
|||
``` |
|||
|
|||
## EntityFramework Core |
|||
|
|||
ABP 使用[EF Core 的全局过滤](https://docs.microsoft.com/en-us/ef/core/querying/filters)系统用于[EF Core 集成]. 所以它很好的集成到 EF Core 中,即使你直接使用 `DbContext` 它也可以正常工作. |
|||
|
|||
实现自定义过滤的最佳方法是为重写你的 `DbContext` 的 `ShouldFilterEntity` 和 `CreateFilterExpression` 方法. 示例: |
|||
|
|||
```csharp |
|||
protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled<IIsActive>() ?? false; |
|||
|
|||
protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) |
|||
{ |
|||
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return base.ShouldFilterEntity<TEntity>(entityType); |
|||
} |
|||
|
|||
protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() |
|||
{ |
|||
var expression = base.CreateFilterExpression<TEntity>(); |
|||
|
|||
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) |
|||
{ |
|||
Expression<Func<TEntity, bool>> isActiveFilter = |
|||
e => !IsActiveFilterEnabled || EF.Property<bool>(e, "IsActive"); |
|||
expression = expression == null |
|||
? isActiveFilter |
|||
: CombineExpressions(expression, isActiveFilter); |
|||
} |
|||
|
|||
return expression; |
|||
} |
|||
``` |
|||
|
|||
- 添加 `IsActiveFilterEnabled` 属性用于检查是否启用了 `IIsActive` . 内部使用了之前介绍到的 `IDataFilter` 服务. |
|||
- 重写 `ShouldFilterEntity` 和 `CreateFilterExpression` 方法检查给定实体是否实现 `IIsActive` 接口,在必要时组合表达式. |
|||
@ -0,0 +1,65 @@ |
|||
# 种子数据 |
|||
|
|||
## 介绍 |
|||
|
|||
- 使用数据库的某些应用程序(或模块),可能需要有一些初始数据才能够正常启动和运行. 例如管理员用户和角色必须在一开始就可用. 否则你就无法登录到应用程序创建新用户和角色. |
|||
- 数据种子也可用于测试的目的,你的自动测试可以假定数据库中有一些可用的初始数据. |
|||
|
|||
## IDataSeedContributor |
|||
|
|||
将数据种子化到数据库需要实现`IDataSeedContributor`接口. |
|||
|
|||
示例: 如果没有图书,则向数据库播种一个初始图书 |
|||
|
|||
```csharp |
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreDataSeedContributor : IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
private readonly IGuidGenerator _guidGenerator; |
|||
private readonly ICurrentTenant _currentTenant; |
|||
|
|||
public BookStoreDataSeedContributor( |
|||
IRepository<Book, Guid> bookRepository, |
|||
IGuidGenerator guidGenerator, |
|||
ICurrentTenant currentTenant) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
_guidGenerator = guidGenerator; |
|||
_currentTenant = currentTenant; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
using (_currentTenant.Change(context?.TenantId)) |
|||
{ |
|||
if (await _bookRepository.GetCountAsync() > 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var book = new Book( |
|||
id: _guidGenerator.Create(), |
|||
name: "The Hitchhiker's Guide to the Galaxy", |
|||
type: BookType.ScienceFiction, |
|||
publishDate: new DateTime(1979, 10, 12), |
|||
price: 42 |
|||
); |
|||
|
|||
await _bookRepository.InsertAsync(book); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `IDataSeedContributor` 定义了 `SeedAsync` 方法用于执行 数据种子逻辑. |
|||
- 通常检查数据库是否已经存在种子数据. |
|||
- 你可以注入服务,检查数据播种所需的任何逻辑. |
|||
|
|||
> 数据种子贡献者由 ABP 框架自动发现,并作为数据播种过程的一部分执行. |
|||
|
|||
## 模块化 |
|||
|
|||
一个应用程序可以具有多个种子数据贡献者(`IDataSeedContributor`)类. 任何可重用模块也可以实现此接口播种其自己的初始数据. |
|||
@ -0,0 +1,159 @@ |
|||
# 多租户 |
|||
|
|||
## 定义实体 |
|||
|
|||
你可以在你的实体中实现 IMultiTenant 接口来实现多租户,例如: |
|||
|
|||
```C# |
|||
|
|||
namespace MyCompany.MyProject |
|||
{ |
|||
public class Product : AggregateRoot, IMultiTenant |
|||
{ |
|||
public Guid? TenantId { get; set; } //IMultiTenant 定义了 TenantId 属性 |
|||
|
|||
public string Name { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
实现 IMultiTenant 接口,需要在实体中定义一个 TenantId 的属性 |
|||
|
|||
## 获取当前租户 |
|||
|
|||
你的代码中可能需要获取当前租户(先不管它具体是怎么取得的).对于这种情况你可以注入并使用 ICurrentTenant 接口.例如: |
|||
|
|||
```C# |
|||
|
|||
namespace MyCompany.MyProject |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ICurrentTenant _currentTenant; |
|||
|
|||
public MyService(ICurrentTenant currentTenant) |
|||
{ |
|||
_currentTenant = currentTenant; |
|||
} |
|||
|
|||
public void DoIt() |
|||
{ |
|||
var tenantId = _currentTenant.Id; |
|||
//在你的代码中使用tenantId |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 改变当前租户 |
|||
|
|||
```csharp |
|||
|
|||
namespace MultiTenancyDemo.Products |
|||
{ |
|||
public class ProductManager : DomainService |
|||
{ |
|||
private readonly IRepository<Product, Guid> _productRepository; |
|||
|
|||
public ProductManager(IRepository<Product, Guid> productRepository) |
|||
{ |
|||
_productRepository = productRepository; |
|||
} |
|||
|
|||
public async Task<long> GetProductCountAsync(Guid? tenantId) |
|||
{ |
|||
using (CurrentTenant.Change(tenantId)) |
|||
{ |
|||
return await _productRepository.GetCountAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 确定当前租户 |
|||
|
|||
多租户的应用程序运行的时候首先要做的就是确定当前租户. |
|||
Volo.Abp.MultiTenancy 只提供了用于确定当前租户的抽象(称为租户解析器),但是并没有现成的实现. |
|||
Volo.Abp.AspNetCore.MultiTenancy 已经实现了从当前 Web 请求(从子域名,请求头,cookie,路由...等)中确定当前租户.本文后面会介绍 Volo.Abp.AspNetCore.MultiTenancy. |
|||
|
|||
## 自定义租户解析器 |
|||
|
|||
你可以像下面这样,在你模块的 ConfigureServices 方法中将自定义解析器并添加到 AbpTenantResolveOptions 中: |
|||
|
|||
```C# |
|||
|
|||
namespace MyCompany.MyProject |
|||
{ |
|||
[DependsOn(typeof(AbpMultiTenancyModule))] |
|||
public class MyModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpTenantResolveOptions>(options => |
|||
{ |
|||
options.TenantResolvers.Add(new MyCustomTenantResolveContributor()); |
|||
}); |
|||
|
|||
//... |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`MyCustomTenantResolveContributor`必须像下面这样实现 ITenantResolveContributor 接口: |
|||
|
|||
```C# |
|||
namespace MyCompany.MyProject |
|||
{ |
|||
public class MyCustomTenantResolveContributor : ITenantResolveContributor |
|||
{ |
|||
public override Task ResolveAsync(ITenantResolveContext context) |
|||
{ |
|||
context.TenantIdOrName = ... //从其他地方获取租户id或租户名字... |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
如果能确定租户 id 或租户名字可以在租户解析器中设置 TenantIdOrName.如果不能确定,那就空着让下一个解析器来确定它. |
|||
|
|||
## 租户信息 |
|||
|
|||
ITenantStore 跟 TenantConfiguration 类一起工作,并且包含了几个租户属性: |
|||
|
|||
- Id:租户的唯一 Id. |
|||
- Name: 租户的唯一名称. |
|||
- ConnectionStrings:如果这个租户有专门的数据库来存储数据.它可以提供数据库的字符串(它可以具有默认的连接字符串和每个模块的连接字符串). |
|||
|
|||
## 多租户中间件 |
|||
|
|||
Volo.Abp.AspNetCore.MultiTenancy 包含了多租户中间件... |
|||
|
|||
```C# |
|||
app.UseMultiTenancy(); |
|||
``` |
|||
|
|||
## 从 Web 请求中确定当前租户 |
|||
|
|||
Volo.Abp.AspNetCore.MultiTenancy 添加了下面这些租户解析器,从当前 Web 请求(按优先级排序)中确定当前租户. |
|||
|
|||
- CurrentUserTenantResolveContributor: 如果当前用户已登录,从当前用户的声明中获取租户 Id. 出于安全考虑,应该始终将其做为第一个 Contributor. |
|||
- QueryStringTenantResolveContributor: 尝试从 query string 参数中获取当前租户,默认参数名为"\_\_tenant". |
|||
- RouteTenantResolveContributor:尝试从当前路由中获取(URL 路径),默认是变量名是"\_\_tenant".所以,如果你的路由中定义了这个变量,就可以从路由中确定当前租户. |
|||
- HeaderTenantResolveContributor: 尝试从 HTTP header 中获取当前租户,默认的 header 名称是"\_\_tenant". |
|||
- CookieTenantResolveContributor: 尝试从当前 cookie 中获取当前租户.默认的 Cookie 名称是"\_\_tenant". |
|||
|
|||
> 如果你使用 nginx 作为反向代理服务器,请注意如果`TenantKey`包含下划线或其他特殊字符可能存在问题, 请参考: |
|||
> http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers > http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers |
|||
|
|||
可以使用 AbpAspNetCoreMultiTenancyOptions 修改默认的参数名"\_\_tenant".例如: |
|||
|
|||
```C# |
|||
services.Configure<AbpAspNetCoreMultiTenancyOptions>(options => |
|||
{ |
|||
options.TenantKey = "MyTenantKey"; |
|||
}); |
|||
``` |
|||
@ -0,0 +1,176 @@ |
|||
# 授权 |
|||
|
|||
授权用于在应用程序中判断是否允许用户执行某些特定的操作. |
|||
|
|||
ABP 扩展了[ASP.NET Core 授权](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/introduction), 将权限添加为自动[策略](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies)并且使授权系统在 [应用服务](Application-Services.md) 同样可用. |
|||
|
|||
## Authorize Attribute |
|||
|
|||
ASP.NET Core 定义了 [Authorize](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/simple)特性用于在控制器,控制器方法以及页面上授权. 现在 ABP 将它带到了[应用服务](Application-Services.md). |
|||
|
|||
示例: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
[Authorize] |
|||
public class AuthorAppService : ApplicationService, IAuthorAppService |
|||
{ |
|||
public Task<List<AuthorDto>> GetListAsync() |
|||
{ |
|||
... |
|||
} |
|||
|
|||
[AllowAnonymous] |
|||
public Task<AuthorDto> GetAsync(Guid id) |
|||
{ |
|||
... |
|||
} |
|||
|
|||
[Authorize("BookStore_Author_Create")] |
|||
public Task CreateAsync(CreateAuthorDto input) |
|||
{ |
|||
... |
|||
} |
|||
} |
|||
} |
|||
|
|||
``` |
|||
|
|||
- `Authorize`用户必须登陆到应用程序才可以访问 `AuthorAppService` 中的方法. 所以`GetListAsync` 方法仅可用于通过身份验证的用户. |
|||
- `AllowAnonymous` 禁用身份验证. 所以 `GetAsync` 方法任何人都可以访问,包括未授权的用户. |
|||
- `[Authorize("BookStore_Author_Create")]` 定义了一个策略 (参阅 [基于策略的授权](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies)),它用于检查当前用户的权限."BookStore_Author_Create" 是一个策略名称. 如果你想要使用策略的授权方式,需要在 ASP.NET Core 授权系统中预先定义它. |
|||
|
|||
## 定义权限 |
|||
|
|||
创建一个继承自 `PermissionDefinitionProvider` 的类,如下所示: |
|||
|
|||
```csharp |
|||
using Volo.Abp.Authorization.Permissions; |
|||
|
|||
namespace Acme.BookStore.Permissions |
|||
{ |
|||
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider |
|||
{ |
|||
public override void Define(IPermissionDefinitionContext context) |
|||
{ |
|||
var myGroup = context.AddGroup("BookStore"); |
|||
|
|||
myGroup.AddPermission("BookStore_Author_Create"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> ABP 会自动发现这个类,不需要进行配置! |
|||
|
|||
在 `Define` 方法中添加权限组或者获取已存在的权限组,并向权限组中添加权限. |
|||
在定义权限后就可以在 ASP.NET Core 权限系统中当做策略名称使用. 在角色的权限管理模态框中同样可以看到: |
|||
{: .zoom} |
|||
|
|||
## 多租户 |
|||
|
|||
在定义新权限时可以设置多租户选项. 有下面三个值: |
|||
|
|||
- Host: 权限仅适用于宿主. |
|||
- Tenant: 权限仅适用于租户. |
|||
- Both (默认): 权限适用与宿主和租户. |
|||
|
|||
> 如果你的应用程序不是多租户的,可以忽略这个选项. |
|||
|
|||
`AddPermission` 方法的第三个参数用于设置多租户选项: |
|||
|
|||
```csharp |
|||
myGroup.AddPermission( |
|||
"BookStore_Author_Create", |
|||
LocalizableString.Create<BookStoreResource>("Permission:BookStore_Author_Create"), |
|||
multiTenancySide: MultiTenancySides.Tenant //set multi-tenancy side! |
|||
); |
|||
``` |
|||
|
|||
## 前端权限 |
|||
|
|||
### 菜单权限 |
|||
|
|||
```ts |
|||
import type { AppRouteModule } from "/@/router/types"; |
|||
import { LAYOUT } from "/@/router/constant"; |
|||
import { t } from "/@/hooks/web/useI18n"; |
|||
const tenant: AppRouteModule = { |
|||
path: "/tenant", |
|||
name: "Tenant", |
|||
component: LAYOUT, |
|||
meta: { |
|||
orderNo: 30, |
|||
icon: "ant-design:contacts-outlined", |
|||
title: t("routes.tenant.tenantManagement"), |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: "Tenant", |
|||
name: "Tenant", |
|||
component: () => import("/@/views/tenants/Tenant.vue"), |
|||
meta: { |
|||
title: t("routes.tenant.tenantList"), |
|||
icon: "ant-design:switcher-filled", |
|||
policy: "AbpTenantManagement.Tenants", //菜单权限 |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
export default tenant; |
|||
``` |
|||
|
|||
### 按钮权限 |
|||
|
|||
```ts |
|||
<template> |
|||
<div> |
|||
<BasicTable @register="registerTable" size="small"> |
|||
<template #action="{ record }"> |
|||
<TableAction |
|||
:actions="[ |
|||
{ |
|||
icon: 'ant-design:edit-outlined', |
|||
auth: 'AbpIdentity.Users.Update', // 按钮权限 |
|||
label: t('common.editText'), |
|||
onClick: handleEdit.bind(null, record), |
|||
}, |
|||
]" |
|||
:dropDownActions="[ |
|||
{ |
|||
auth: 'AbpIdentity.Users.Delete', // 按钮权限 |
|||
label: t('common.delText'), |
|||
onClick: handleDelete.bind(null, record), |
|||
}, |
|||
{ |
|||
auth: 'System.Users.Enable', // 按钮权限 |
|||
label: !record.isActive |
|||
? t('common.enabled') |
|||
: t('common.disEnabled'), |
|||
onClick: handleLock.bind(null, record), |
|||
}, |
|||
]" |
|||
/> |
|||
</template> |
|||
</BasicTable> |
|||
<CreateAbpUser |
|||
@register="registerCreateAbpUserModal" |
|||
@reload="reload" |
|||
:bodyStyle="{ 'padding-top': '0' }" |
|||
/> |
|||
<EditAbpUser |
|||
@register="registerEditAbpUserModal" |
|||
@reload="reload" |
|||
:bodyStyle="{ 'padding-top': '0' }" |
|||
/> |
|||
</div> |
|||
</template> |
|||
``` |
|||
@ -0,0 +1,102 @@ |
|||
# 缓存 |
|||
|
|||
ABP 框架扩展了 [ASP.NET Core 的分布式缓存系统](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). |
|||
ABP vNext Pro 已集成 Redis 做为缓存。 |
|||
|
|||
## 配置 |
|||
|
|||
```json title="appsetting.json" |
|||
"Redis": |
|||
{ |
|||
"Configuration": "localhost,password=1q2w3E*,defaultdatabase=1" |
|||
} |
|||
``` |
|||
|
|||
### AbpDistributedCacheOptions |
|||
|
|||
示例:为应用程序设置缓存键前缀 |
|||
|
|||
```cs title="AbpProHttpApiHostModule.cs" |
|||
Configure<AbpDistributedCacheOptions>(options => |
|||
{ |
|||
options.KeyPrefix = "MyApp1"; |
|||
}); |
|||
``` |
|||
|
|||
### 可用选项 |
|||
|
|||
- `HideErrors` (`bool`, 默认: `true`): 启用/禁用隐藏从缓存服务器写入/读取值时的错误. |
|||
- `KeyPrefix` (`string`, 默认: `null`): 如果你的缓存服务器由多个应用程序共同使用, 则可以为应用程序的缓存键设置一个前缀. 在这种情况下, 不同的应用程序不能覆盖彼此的缓存内容. |
|||
- `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): 用于设置保存缓内容却没有指定选项时, 默认的分布式缓存选项 (例如 `AbsoluteExpiration` 和 `SlidingExpiration`). `SlidingExpiration`的默认值设置为 20 分钟. |
|||
|
|||
## 使用方式 |
|||
|
|||
示例: 在缓存中存储图书名称和价格 |
|||
|
|||
```csharp |
|||
namespace MyProject |
|||
{ |
|||
public class BookCacheItem |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
你可以注入 `IDistributedCache<BookCacheItem>` 服务用于 get/set `BookCacheItem` 对象. |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class BookService : ITransientDependency |
|||
{ |
|||
private readonly IDistributedCache<BookCacheItem> _cache; |
|||
|
|||
public BookService(IDistributedCache<BookCacheItem> cache) |
|||
{ |
|||
_cache = cache; |
|||
} |
|||
|
|||
public async Task<BookCacheItem> GetAsync(Guid bookId) |
|||
{ |
|||
return await _cache.GetOrAddAsync( |
|||
bookId.ToString(), //缓存键 |
|||
async () => await GetBookFromDatabaseAsync(bookId), |
|||
() => new DistributedCacheEntryOptions |
|||
{ |
|||
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) |
|||
} |
|||
); |
|||
} |
|||
|
|||
private Task<BookCacheItem> GetBookFromDatabaseAsync(Guid bookId) |
|||
{ |
|||
//TODO: 从数据库获取数据 |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- 示例服务代码中的 `GetOrAddAsync()` 方法从缓存中获取图书项. `GetOrAddAsync`是 ABP 框架在 ASP.NET Core 分布式缓存方法中添增的附加方法. |
|||
- 如果没有在缓存中找到图书,它会调用工厂方法 (本示例中是 `GetBookFromDatabaseAsync`)从原始数据源中获取图书项. |
|||
- `GetOrAddAsync` 有一个可选参数 `DistributedCacheEntryOptions` , 可用于设置缓存的生命周期. |
|||
|
|||
## 批量操作 |
|||
|
|||
ABP 的分布式缓存接口定义了以下批量操作方法,当你需要在一个方法中调用多次缓存操作时,这些方法可以提高性能 |
|||
|
|||
- `SetManyAsync` 和 `SetMany` 方法可以用来向缓存中设置多个值. |
|||
- `GetManyAsync` 和 `GetMany` 方法可以用来从缓存中获取多个值. |
|||
- `GetOrAddManyAsync` 和 `GetOrAddMany` 方法可以用来从缓存中获取并添加缺少的值. |
|||
- `RefreshManyAsync` 和 `RefreshMany` 方法可以来用重置多个值的滚动过期时间. |
|||
- `RemoveManyAsync` 和 `RemoveMany` 方法可以用来从缓存中删除多个值. |
|||
|
|||
> 这些不是标准的 ASP.NET Core 缓存方法, 所以某些提供程序可能不支持. [ABP Redis 集成包]实现了它们. 如果提供程序不支持,会回退到 `SetAsync` 和 `GetAsync` ... 方法(循环调用). |
|||
@ -0,0 +1,190 @@ |
|||
# 异常处理 |
|||
|
|||
ABP 提供了用于处理 Web 应用程序异常的标准模型. |
|||
|
|||
- 自动处理所有异常.如果是 API/AJAX 请求,会向客户端返回一个标准格式化后的错误消息 . |
|||
- 自动隐藏内部详细错误并返回标准错误消息. |
|||
- 为异常消息的本地化提供一种可配置的方式. |
|||
- 自动为标准异常设置\*HTTP 状态代码,并提供可配置选项,以映射自定义异常. |
|||
|
|||
## 自动处理异常 |
|||
|
|||
当满足下面任意一个条件时,`AbpExceptionFilter` 会处理此异常: |
|||
|
|||
- 当 controller action 方法返回类型是 object result(而不是 view result)并有异常抛出时. |
|||
- 当一个请求为 AJAX(Http 请求头中`X-Requested-With`为`XMLHttpRequest`)时. |
|||
- 当客户端接受的返回类型为`application/json`(Http 请求头中`accept` 为`application/json`)时. |
|||
|
|||
## 错误消息格式 |
|||
|
|||
```json |
|||
{ |
|||
"error": { |
|||
"code": "App:010042", |
|||
"message": "This topic is locked and can not add a new message", |
|||
"details": "A more detailed info about the error..." |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- 错误代码(code)是异常信息中一个有唯一值并可选的字符串值.抛出的异常应实现`IHasErrorCode` 接口来填充该字段. |
|||
- 错误的详细信息(Details) 是可选属性.抛出的异常应实现`IHasErrorDetails` 接口来填充该字段. |
|||
|
|||
### 验证错误 |
|||
|
|||
当抛出的异常实现`IHasValidationErrors` 接口时,validationErrors 是一个可被填充的标准字段.示例 JSON 如下: |
|||
|
|||
```json |
|||
{ |
|||
"error": { |
|||
"code": "App:010046", |
|||
"message": "Your request is not valid, please correct and try again!", |
|||
"validationErrors": [ |
|||
{ |
|||
"message": "Username should be minimum length of 3.", |
|||
"members": ["userName"] |
|||
}, |
|||
{ |
|||
"message": "Password is required", |
|||
"members": ["password"] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`AbpValidationException`已经实现了`IHasValidationErrors`接口,当请求输入无效时,框架会自动抛出此错误. 因此,除非你有自定义的验证逻辑,否则不需要处理验证错误. |
|||
|
|||
## 业务异常 |
|||
|
|||
大多数异常都是业务异常.可以通过使用`IBusinessException` 接口来标记异常为业务异常. |
|||
|
|||
`BusinessException` 除了实现`IHasErrorCode`,`IHasErrorDetails` ,`IHasLogLevel` 接口外,还实现了`IBusinessException` 接口.其默认日志级别为`Warning`. |
|||
|
|||
通常你会将一个错误代码关联至特定的业务异常.例如: |
|||
|
|||
```C# |
|||
throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer); |
|||
``` |
|||
|
|||
`QaErrorCodes.CanNotVoteYourOwnAnswer` 是一个字符串常量. 建议使用下面的错误代码格式: |
|||
|
|||
``` |
|||
<code-namespace>:<error-code> |
|||
``` |
|||
|
|||
code-namespace,应在指定的模块/应用层中保证其唯一.例如: |
|||
|
|||
``` |
|||
Volo.Qa:010002 |
|||
``` |
|||
|
|||
`Volo.Qa`在这是作为`code-namespace`. `code-namespace` 同样可以在 本地化 异常信息时使用. |
|||
|
|||
- 你可以直接抛出一个 `BusinessException` 异常,或者需要时可以从该类派生你自己的 Exception 类型. |
|||
- 对于`BusinessException` 类型,其所有属性都是可选的.但是通常会设置`ErrorCode`或`Message`属性. |
|||
|
|||
## 使用错误代码 |
|||
|
|||
通过使用错误代码的方式来处理本地化,而不是在抛出异常的时候. |
|||
|
|||
首先,在模块配置代码中将 code-namespace 映射至 本地化资源: |
|||
|
|||
```C# |
|||
services.Configure<AbpExceptionLocalizationOptions>(options => |
|||
{ |
|||
options.MapCodeNamespace("Volo.Qa", typeof(QaResource)); |
|||
}); |
|||
``` |
|||
|
|||
然后`Volo.Qa`命名空间下的所有异常都将被对应的本地化资源进行本地化处理. 本地化资源中应包含对应错误代码的文本. 例如: |
|||
|
|||
```json |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Volo.Qa:010002": "You can not vote your own answer!" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
最后就可以抛出一个包含错误代码的业务异常了: |
|||
|
|||
```C# |
|||
throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer); |
|||
``` |
|||
|
|||
- 抛出所有实现`IHasErrorCode` 接口的异常都具有相同的行为.因此,对错误代码的本地化,并不是`BusinessException`类所特有的. |
|||
- 为错误消息定义本地化文本并不是必须的. 如果未定义,ABP 会将默认的错误消息发送给客户端. 而不使用异常的`Message`属性. 如果你想要发送异常的`Message`,使用`UserFriendlyException`(或使用实现`IUserFriendlyException`接口的异常类型) |
|||
|
|||
### 使用消息的格式化参数 |
|||
|
|||
如果有参数化的错误消息,则可以使用异常的`Data`属性进行设置.例如: |
|||
|
|||
```C# |
|||
throw new BusinessException("App:010046") |
|||
{ |
|||
Data = |
|||
{ |
|||
{"UserName", "john"} |
|||
} |
|||
}; |
|||
|
|||
``` |
|||
|
|||
另外有一种更为快捷的方式: |
|||
|
|||
```C# |
|||
throw new BusinessException("App:010046") |
|||
.WithData("UserName", "john"); |
|||
``` |
|||
|
|||
下面就是一个包含`UserName` 参数的错误消息: |
|||
|
|||
```json |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"App:010046": "Username should be unique. '{UserName}' is already taken!" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `WithData` 支持有多个参数的链式调用 (如`.WithData(...).WithData(...)`). |
|||
|
|||
## HTTP 状态代码映射 |
|||
|
|||
ABP 尝试按照以下规则,自动映射常见的异常类型的 HTTP 状态代码: |
|||
|
|||
- 对于 `AbpAuthorizationException`: |
|||
- 用户没有登录,返回 `401` (未认证). |
|||
- 用户已登录,但是当前访问未授权,返回 `403` (未授权). |
|||
- 对于 `AbpValidationException` 返回 `400` (错误的请求) . |
|||
- 对于 `EntityNotFoundException`返回 `404` (未找到). |
|||
- 对于 `IBusinessException` 和 `IUserFriendlyException` (它是`IBusinessException`的扩展) 返回`403` (未授权) . |
|||
- 对于 `NotImplementedException` 返回 `501` (未实现) . |
|||
- 对于其他异常 (基础架构中未定义的) 返回 `500` (服务器内部错误) . |
|||
|
|||
`IHttpExceptionStatusCodeFinder` 是用来自动判断 HTTP 状态代码.默认的实现是`DefaultHttpExceptionStatusCodeFinder`.可以根据需要对其进行更换或扩展. |
|||
|
|||
#### 自定义映射 |
|||
|
|||
可以重写 HTTP 状态代码的自动映射,示例如下: |
|||
|
|||
```C# |
|||
services.Configure<AbpExceptionHttpStatusCodeOptions>(options => |
|||
{ |
|||
options.Map("Volo.Qa:010002", HttpStatusCode.Conflict); |
|||
}); |
|||
``` |
|||
|
|||
## 发送异常详情到客户端 |
|||
|
|||
你可以通过 `AbpExceptionHandlingOptions` 类的 `SendExceptionsDetailsToClients` 属性异常发送到客户端: |
|||
|
|||
```csharp |
|||
services.Configure<AbpExceptionHandlingOptions>(options => |
|||
{ |
|||
options.SendExceptionsDetailsToClients = true; |
|||
}); |
|||
``` |
|||
@ -0,0 +1,128 @@ |
|||
# Serilog |
|||
|
|||
> ABP 框架没有实现任何日志基础设施. 它使用 ASP.NET Core 日志系统. |
|||
|
|||
## 日志等级 |
|||
|
|||
!!! info "Debug --> Information --> Warning --> Error --> Fatal" |
|||
|
|||
## 如何集成 |
|||
|
|||
```cs title="Program.cs" |
|||
public class Program |
|||
{ |
|||
private static IHostBuilder CreateHostBuilder(string[] args) => |
|||
Host.CreateDefaultBuilder(args) |
|||
.ConfigureWebHostDefaults() |
|||
// 使用Serilog |
|||
.UseSerilog((context, loggerConfiguration) => |
|||
{ |
|||
SerilogToEsExtensions.SetSerilogConfiguration( |
|||
loggerConfiguration, |
|||
context.Configuration); |
|||
}).UseAutofac(); |
|||
} |
|||
``` |
|||
|
|||
```cs title="SerilogToEsExtensions.cs" |
|||
public static void SetSerilogConfiguration(LoggerConfiguration loggerConfiguration, IConfiguration configuration) |
|||
{ |
|||
// 默认读取 configuration 中 "Serilog" 节点下的配置 |
|||
loggerConfiguration |
|||
.ReadFrom.Configuration(configuration) |
|||
.Enrich.FromLogContext(); |
|||
// 如果要再日志加上自定义字段 |
|||
loggerConfiguration.Enrich.WithProperty("Application", applicationName); |
|||
} |
|||
``` |
|||
|
|||
## 配置 |
|||
|
|||
```json title="appsetting.json" |
|||
"Serilog": { |
|||
"Using": [ |
|||
"Serilog.Sinks.Console", |
|||
"Serilog.Sinks.File" |
|||
], |
|||
"MinimumLevel": { |
|||
// 默认全局日志级别 |
|||
"Default": "Information", |
|||
"Override": { |
|||
//名称空间为 Microsoft 日志级别 |
|||
"Microsoft": "Information", |
|||
//名称空间为 Volo.Abp 日志级别 |
|||
"Volo.Abp": "Information", |
|||
//名称空间为 Hangfire 日志级别 |
|||
"Hangfire": "Information", |
|||
//名称空间为 DotNetCore.CAP 日志级别 |
|||
"DotNetCore.CAP": "Information", |
|||
//名称空间为 Serilog.AspNetCore 日志级别 |
|||
"Serilog.AspNetCore": "Information", |
|||
//名称空间为 Microsoft.EntityFrameworkCore 日志级别 |
|||
"Microsoft.EntityFrameworkCore": "Warning", |
|||
//名称空间为 Microsoft.AspNetCore 日志级别 |
|||
"Microsoft.AspNetCore": "Information" |
|||
} |
|||
}, |
|||
"WriteTo": [ |
|||
{ |
|||
// 输出到控制台日志 |
|||
"Name": "Console" |
|||
}, |
|||
{ |
|||
// 输出到文件 |
|||
"Name": "File", |
|||
"Args": { |
|||
"path": "logs/logs-.txt", |
|||
// 按天输出 |
|||
"rollingInterval": "Day" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 写入 ElasticSearch |
|||
|
|||
> AbpPro 已经集成 ElasticSearch 只需要通过配置文件启用即可。 |
|||
|
|||
- Enabled:是否启用 |
|||
- Url:es 地址 |
|||
- IndexFormat:es 索引 |
|||
- UserName:用户名 |
|||
- Password:密码 |
|||
|
|||
```json title="appsetting.json" |
|||
"ElasticSearch": { |
|||
"Enabled": "false", |
|||
"Url": "http://es.cn", |
|||
// 索引名必须小写 |
|||
"IndexFormat": "lion.abppro.development.{0:yyyy.MM.dd}", |
|||
"UserName": "elastic", |
|||
"Password": "aVVhjQ95RP7nbwNy" |
|||
}, |
|||
``` |
|||
|
|||
## 使用 |
|||
|
|||
```cs title="SampleAppService.cs" |
|||
public class SampleAppService : AbpProAppService,ISampleAppService |
|||
{ |
|||
private readonly ILogger<SampleAppService> _logger; |
|||
|
|||
public SampleAppService(ILogger<SampleAppService> logger) |
|||
{ |
|||
_logger = logger; |
|||
} |
|||
|
|||
public async Task TestAsync() |
|||
{ |
|||
_logger.LogDebug("LogDebug"); |
|||
_logger.LogInformation("LogInformation"); |
|||
_logger.LogWarning("LogWarning"); |
|||
_logger.LogError("LogError"); |
|||
_logger.LogTrace("LogTrace"); |
|||
await Task.CompletedTask; |
|||
} |
|||
} |
|||
``` |
|||
@ -0,0 +1,41 @@ |
|||
# 登录 |
|||
|
|||
ABP vNext Pro 没有集成 IdentityServer4 或者 OpenIddict,而是直接使用默认的 Asp Net Core Identity。 |
|||
|
|||
- 减少系统复杂度 |
|||
- 大部分(IdentityServer4|OpenIddict)功能用不上 |
|||
|
|||
## 登录接口 |
|||
|
|||
```cs title="AccountAppService.cs" |
|||
public virtual async Task<LoginOutput> LoginAsync(LoginInput input) |
|||
{ |
|||
var result = await _signInManager.PasswordSignInAsync(input.Name, input.Password, false, true); |
|||
if (result.IsNotAllowed) |
|||
{ |
|||
throw new BusinessException(BasicManagementErrorCodes.UserLockedOut); |
|||
} |
|||
if (!result.Succeeded) |
|||
{ |
|||
throw new BusinessException(BasicManagementErrorCodes.UserOrPasswordMismatch); |
|||
} |
|||
var user = await _userManager.FindByNameAsync(input.Name); |
|||
return await BuildResult(user); |
|||
} |
|||
``` |
|||
|
|||
## 配置 AccessToken |
|||
|
|||
```json title="appsetting.json" |
|||
"Jwt": { |
|||
"Audience": "Lion.AbpPro", |
|||
"SecurityKey": "dzehzRz9a8asdfasfdadfasdfasdfafsdadfasbasdf=", |
|||
"Issuer": "Lion.AbpPro", |
|||
"ExpirationTime": 30 |
|||
} |
|||
``` |
|||
|
|||
- Audience:接收对象 |
|||
- Issuer:签发主体 |
|||
- SecurityKey:密钥 |
|||
- ExpirationTime:过期时间(单位小时) |
|||
Loading…
Reference in new issue