mirror of https://github.com/abpframework/abp.git
164 changed files with 3417 additions and 1746 deletions
@ -1,28 +1,320 @@ |
|||
# 身份管理模块 |
|||
|
|||
身份模块基于Microsoft Identity库用于管理[组织单元](Organization-Units.md), 角色, 用户和他们的权限. |
|||
身份模块基于 [Microsoft Identity 库](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity) 用于管理角色,用户及其权限. |
|||
|
|||
> 参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. |
|||
## 如何安装 |
|||
|
|||
## Identity安全日志 |
|||
当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时,此模块将被预安装(作为 NuGet/NPM 包)。你可以继续用其作为包并轻松地获取更新,也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块。 |
|||
|
|||
安全日志可以记录账户的一些重要的操作或者改动, 你可以在在一些功能中保存安全日志. |
|||
### 源代码 |
|||
|
|||
你可以注入和使用 `IdentitySecurityLogManager` 或 `ISecurityLogManager` 来保存安全日志. 默认它会创建一个安全日志对象并填充常用的值. 如 `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant`等等. 当然你可以自定义这些值. |
|||
可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/identity) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它. |
|||
|
|||
## 用户界面 |
|||
|
|||
此模块提供了 [Blazor](../UI/Blazor/Overall.md), [Angular](../UI/Angular/Quick-Start.md) 和 [MVC / Razor Pages](../UI/AspNetCore/Overall.md) 的 UI 可选. |
|||
|
|||
### 菜单项 |
|||
|
|||
此模块在 *管理* 菜单下添加了一个 *身份管理* 菜单项: |
|||
|
|||
 |
|||
|
|||
这个菜单项和相关页面已获得授权. 这意味着当前用户必须拥有相关权限才能使其可见. `admin` 角色 (和拥有此角色的用户, 如 `admin` 用户) 已经拥有这些权限. 如果你想要使其他角色/用户也启用权限, 请打开 *角色* 或 *用户* 页面的 *权限* 对话框, 并检查如下所示的权限: |
|||
|
|||
 |
|||
|
|||
请参阅 [授权文档](../Authorization.md) 以了解权限系统. |
|||
|
|||
### 页面 |
|||
|
|||
本节介绍此模块提供的主要页面. |
|||
|
|||
#### 用户 |
|||
|
|||
此页用于查看用户列表. 您可以创建/编辑和删除用户, 将角色分配给用户. |
|||
|
|||
 |
|||
|
|||
一个用户可以有零个或多个角色. 用户从其角色继承权限. 此外, 你可以给用户直接分配权限 (通过点击 *操作* 按钮, 然后选择 *权限*) . |
|||
|
|||
#### 角色 |
|||
|
|||
角色用于按分组给用户分配权限. |
|||
|
|||
 |
|||
|
|||
角色除了角色名称之外还有2个属性: |
|||
|
|||
* `Default`: 如果某个角色被标记为 "default", 那么当新用户 (使用 [账户模块](Account.md)) 注册到应用程序时, 其将被作为默认角色分配给新用户. |
|||
* `Public`: 用户的公共角色可以被应用程序中的其他用户看到. 这个功能在身份模块中没有用到, 但被提供作为你可能希望在你自己的应用程序中使用的功能. |
|||
|
|||
## 其他功能 |
|||
|
|||
本节包括此模块提供的没有 UI 页面的其他功能. |
|||
|
|||
### 组织单元 |
|||
|
|||
组织单元 (OU) 可被用于 **按层级对用户和实体进行分组**. |
|||
|
|||
#### 组织单元实体 |
|||
|
|||
组织单元由 **OrganizationUnit** 实体表示. 它的基本属性是: |
|||
|
|||
- **TenantId**: 组织单元的租户Id. 对于宿主可以是 null. |
|||
- **ParentId**: 父级组织单元的Id. 如果这是一个根级组织单元,它可以是 null. |
|||
- **Code**: 对于租户唯一的层级字符串编码. |
|||
- **DisplayName**: 组织单元的显示名称. |
|||
|
|||
#### 组织树 |
|||
|
|||
由于组织单元可以有父级, 因此租户的所有组织单元是一个 **树** 结构. 这个树有一些规则; |
|||
|
|||
- 可以有多个根级 (`ParentId` 是 `null` 的) . |
|||
- 一个组织单元的第一级子项数量有限制 (因为下面解释的固定的组织单元编码单位长度) . |
|||
|
|||
#### 组织单元编码 |
|||
|
|||
组织单元编码是通过 `OrganizationUnitManager` 服务自动生成和维护的. 它是一个字符串, 像这样: |
|||
|
|||
"**00001.00042.00005**" |
|||
|
|||
通常用这种编码可以轻易地 (递归) 查询出数据库中组织单元的所有子项. 这个编码有一些规则 (当你使用 `OrganizationUnitManager` 时自动被应用的) : |
|||
|
|||
- 对 [租户](../Multi-Tenancy.md) 是 **唯一的**. |
|||
- 同一组织单元的全部子项都具有 **以父级组织单元编码开头的** 编码. |
|||
- 如示例中所示, 它是固定长度的且是基于树中组织单元的级别的. |
|||
- 尽管组织单元编码是唯一的, 但如果你移动了相关的组织单元, 它也可以被更改. |
|||
|
|||
请注意, 你必须根据Id引用一个组织单元, 而不是编码, 因为编码后续是可以被更改的. |
|||
|
|||
#### 组织单元管理 |
|||
|
|||
`OrganizationUnitManager` 类可以被 [注入](../Dependency-Injection.md) 并用来管理组织单元. 常见用例如下: |
|||
|
|||
- 创建, 更新和删除组织单元. |
|||
- 在组织单元树中移动一个组织单元. |
|||
- 获取关于组织单元树及其项的信息. |
|||
|
|||
### 身份安全日志 |
|||
|
|||
安全日志系统可以记录账户的一些重要的操作或者改动 (例如 *登录* 和 *更改密码*) . 如果需要, 你也可以保存安全日志. |
|||
|
|||
你可以注入和使用 `IdentitySecurityLogManager` 或 `ISecurityLogManager` 来写入安全日志. 默认它会创建一个日志对象并填充常用的值, 如 `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant` 等等. 当然你可以覆盖这些值. |
|||
|
|||
```cs |
|||
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() |
|||
{ |
|||
Identity = "IdentityServer"; |
|||
Action = "ChangePassword"; |
|||
Identity = "IdentityServer"; |
|||
Action = "ChangePassword"; |
|||
}); |
|||
``` |
|||
|
|||
通过配置 `AbpSecurityLogOptions` 来提供应用程序的名称或者禁用安全日志功能. 默认是**启用**状态. |
|||
通过配置 `AbpSecurityLogOptions` 来为日志提供应用程序的名称 (如果你有多个应用程序并且想要在日志中区分应用程序) 或者禁用安全日志功能. |
|||
|
|||
```cs |
|||
Configure<AbpSecurityLogOptions>(options => |
|||
{ |
|||
options.ApplicationName = "AbpSecurityTest"; |
|||
options.ApplicationName = "AbpSecurityTest"; |
|||
}); |
|||
``` |
|||
## 选项 |
|||
|
|||
`IdentityOptions` 是由 Microsoft [Identity 库](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity) 提供的标准 [选项类](../Options.md) . 所以, 你可以在 [模块](../Module-Development-Basics.md) 类的 `ConfigureServices` 方法中设置这些选项. |
|||
|
|||
**例如: 设置所需的密码最小长度** |
|||
|
|||
````csharp |
|||
Configure<IdentityOptions>(options => |
|||
{ |
|||
options.Password.RequiredLength = 5; |
|||
}); |
|||
```` |
|||
|
|||
ABP 允许你在运行时通过使用 [设置系统](../Settings.md) 更进一步地更改这些选项. 你可以 [注入](../Dependency-Injection.md) `ISettingManager` 并使用 `Set...` 方法中的一种来更改用户, 租户或全局所有用户的选项值. |
|||
|
|||
**例如: 更改当前租户所需的密码最小长度** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ISettingManager _settingManager; |
|||
|
|||
public MyService(ISettingManager settingManager) |
|||
{ |
|||
_settingManager = settingManager; |
|||
} |
|||
|
|||
public async Task ChangeMinPasswordLength(int minLength) |
|||
{ |
|||
await _settingManager.SetForCurrentTenantAsync( |
|||
IdentitySettingNames.Password.RequiredLength, |
|||
minLength.ToString() |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`IdentitySettingNames` 类 (在 `Volo.Abp.Identity.Settings` 命名空间中) 为设置名称定义了一些常量. |
|||
|
|||
## 分布式事件 |
|||
|
|||
此模块定义了如下 ETOs (事件传输对象) 以允许你去订阅模块中实体的改动. |
|||
|
|||
* `UserEto` 在 `IdentityUser` 实体更改完成时发布. |
|||
* `IdentityRoleEto` 在 `IdentityRole` 实体更改完成时发布. |
|||
* `IdentityClaimTypeEto` 在 `IdentityClaimType` 实体更改完成时发布. |
|||
* `OrganizationUnitEto` 在 `OrganizationUnit` 实体更改完成时发布. |
|||
|
|||
**例如: 当一个新用户被创建时接收通知** |
|||
|
|||
````csharp |
|||
public class MyHandler : |
|||
IDistributedEventHandler<EntityCreatedEto<UserEto>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityCreatedEto<UserEto> eventData) |
|||
{ |
|||
UserEto user = eventData.Entity; |
|||
// TODO: ... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`UserEto` 和 `IdentityRoleEto` 默认自动发布事件. 你应该自己配置其他的 Eto. 请参阅 [分布式事件总线文档](../Distributed-Event-Bus.md) 了解预定义事件的详细信息. |
|||
|
|||
> 订阅分布式事件对于分布式场景 (如微服务架构) 特别有用. 如果你正在构建单体式应用程序, 或者在运行了身份模块的同一进程内监听事件, 那么订阅 [本地事件](../Local-Event-Bus.md) 更高效, 更简单. |
|||
|
|||
## 内部构件 |
|||
|
|||
本节包括模块的一些内部详细信息, 您不是那么需要, 但在一些情况下可能需要使用. |
|||
|
|||
### 领域层 |
|||
|
|||
#### 聚合 |
|||
|
|||
##### 用户 |
|||
|
|||
用户通常是一个登录并使用应用程序的人. |
|||
|
|||
* `IdentityUser` (聚合根): 表示系统中的一个用户. |
|||
* `IdentityUserRole` (集合): 分配给用户的角色. |
|||
* `IdentityUserClaim` (集合): 用户的自定义声明. |
|||
* `IdentityUserLogin` (集合): 用户的外部登录. |
|||
* `IdentityUserToken` (集合): 用户的令牌 (用于 Microsoft Identity 服务). |
|||
|
|||
##### 角色 |
|||
|
|||
角色通常是分配给用户的一组权限. |
|||
|
|||
* `IdentityRole` (聚合根): 表示系统中的角色. |
|||
* `IdentityRoleClaim` (集合): 角色的自定义声明. |
|||
|
|||
##### 声明类型 |
|||
|
|||
声明类型是可以被分配给系统中其他实体 (如角色和用户) 的自定义声明的定义. |
|||
|
|||
* `IdentityClaimType` (聚合根): 表示声明类型的定义. 它包括一些用于定义声明类型和验证规则的属性 (例如, 必须的, 正则表达式, 描述, 值类型) . |
|||
|
|||
##### 身份安全日志 |
|||
|
|||
`IdentitySecurityLog` 对象表示系统中与授权相关的操作 (如 *登录*) . |
|||
|
|||
* `IdentitySecurityLog` (聚合根): 表示系统中的安全日志. |
|||
|
|||
##### 组织单元 |
|||
|
|||
组织单元是一个有层级结构的实体. |
|||
|
|||
* ```OrganizationUnit``` (聚合根): 表示系统中的组织单元. |
|||
* ```Roles``` (集合): 组织单元的角色. |
|||
|
|||
#### 仓储 |
|||
|
|||
此模块定义了以下自定义仓储: |
|||
|
|||
* `IIdentityUserRepository` |
|||
* `IIdentityRoleRepository` |
|||
* `IIdentityClaimTypeRepository` |
|||
* ```IIdentitySecurityLogRepository``` |
|||
* ```IOrganizationUnitRepository``` |
|||
|
|||
#### 领域服务 |
|||
|
|||
##### 用户管理 |
|||
|
|||
`IdentityUserManager` 常用于管理用户, 他们的权限, 声明, 密码, 电子邮件等等. 它派生自 Microsoft Identity 的 `UserManager<T>` 类, 其中 `T` 是 `IdentityUser`. |
|||
|
|||
##### 角色管理 |
|||
|
|||
`IdentityRoleManager` 常用于管理角色和他们的声明. 它派生自 Microsoft Identity 的 `RoleManager<T>` 类, 其中 `T` 是 `IdentityRole`. |
|||
|
|||
##### 声明类型管理 |
|||
|
|||
`IdenityClaimTypeManager` 常用于对聚合根 `IdentityClaimType` 执行某些操作. |
|||
|
|||
##### 组织单元管理 |
|||
|
|||
```OrganizationUnitManager``` 常用于对聚合根 `OrganizationUnit` 执行某些操作. |
|||
|
|||
##### 安全日志管理 |
|||
|
|||
```IdentitySecurityLogManager``` 常用于保存安全日志. |
|||
|
|||
### 服务层 |
|||
|
|||
#### 应用服务 |
|||
|
|||
* `IdentityUserAppService` (实现 `IIdentityUserAppService`): 实现了用户管理 UI 的用例. |
|||
* `IdentityRoleAppService` (实现 `IIdentityRoleAppService`): 实现了角色管理 UI 的用例. |
|||
* `IdentityClaimTypeAppService` (实现 `IIdentityClaimTypeAppService`): 实现了声明类型管理 UI 的用例. |
|||
* `IdentitySettingsAppService` (实现 `IIdentitySettingsAppService`): 用于获取和更新身份模块的设置. |
|||
* `IdentityUserLookupAppService` (实现 `IIdentityUserLookupAppService`): 用于根据 `id` 或 `userName` 获取用户信息. 它旨在由ABP内部使用. |
|||
* `ProfileAppService` (实现 `IProfileAppService`): 用于更改用户的简介和密码. |
|||
* ```IdentitySecurityLogAppService``` (实现 ```IIdentitySecurityLogAppService```): 实现了安全日志 UI 的用例. |
|||
* ```OrganizationUnitAppService``` (实现 ```OrganizationUnitAppService```): 实现了组织单元管理 UI 的用例. |
|||
|
|||
### 数据库提供程序 |
|||
|
|||
此模块为数据库提供 [Entity Framework Core](../Entity-Framework-Core.md) 和 [MongoDB](../MongoDB.md) 两种选择. |
|||
|
|||
#### EF Core |
|||
|
|||
NuGet 包 [Volo.Abp.Identity.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore) 实现了 EF Core 的集成. |
|||
|
|||
##### 数据库表 |
|||
|
|||
* **AbpRoles** |
|||
* AbpRoleClaims |
|||
* **AbpUsers** |
|||
* AbpUserClaims |
|||
* AbpUserLogins |
|||
* AbpUserRoles |
|||
* AbpUserTokens |
|||
* **AbpClaimTypes** |
|||
* **AbpOrganizationUnits** |
|||
* AbpOrganizationUnitRoles |
|||
* AbpUserOrganizationUnits |
|||
* **AbpSecurityLogs** |
|||
|
|||
#### MongoDB |
|||
|
|||
NuGet 包 [Volo.Abp.Identity.MongoDB](https://www.nuget.org/packages/Volo.Abp.Identity.MongoDB) 实现了 MongoDB 的集成. |
|||
|
|||
##### 数据库集合 |
|||
|
|||
* **AbpRoles** |
|||
* **AbpUsers** |
|||
* **AbpClaimTypes** |
|||
* **AbpOrganizationUnits** |
|||
* **AbpSecurityLogs** |
|||
|
|||
#### 常用数据库属性 |
|||
|
|||
你可以设置 `AbpIdentityDbProperties` 中的以下属性来更改数据库选项: |
|||
|
|||
* `DbTablePrefix` (`Abp` 作为默认值) 是表/集合名称的前缀. |
|||
* `DbSchema` (`null` 作为默认值) 是数据库架构. |
|||
* `ConnectionStringName` (`AbpIdentity` 作为默认值) 是此模块的 [连接字符串](../Connection-Strings.md) 名称. |
|||
|
|||
它们是静态属性. 你需要在开始运行应用程序前设置它们 (通常在 `Program.cs` 中). |
|||
|
|||
@ -1 +1,635 @@ |
|||
TODO.. |
|||
# Web应用程序开发教程 - 第五章: 授权 |
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","Blazor","BlazorServer","NG"], |
|||
"DB": ["EF","Mongo"] |
|||
} |
|||
```` |
|||
## 关于本教程 |
|||
|
|||
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: |
|||
|
|||
* **{{DB_Text}}** 做为ORM提供程序. |
|||
* **{{UI_Value}}** 做为UI框架. |
|||
|
|||
本教程分为以下部分: |
|||
|
|||
- [Part 1: 创建服务端](Part-1.md) |
|||
- [Part 2: 图书列表页面](Part-2.md) |
|||
- [Part 3: 创建,更新和删除图书](Part-2.md) |
|||
- [Part 4: 集成测试](Part-4.md) |
|||
- **Part 5: 授权**(本章) |
|||
- [Part 6: 作者: 领域层](Part-6.md) |
|||
- [Part 7: 作者: 数据库集成](Part-7.md) |
|||
- [Part 8: 作者: 应用服务层](Part-8.md) |
|||
- [Part 9: 作者: 用户页面](Part-9.md) |
|||
- [Part 10: 图书到作者的关系](Part-10.md) |
|||
|
|||
## 下载源码 |
|||
|
|||
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: |
|||
|
|||
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) |
|||
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) |
|||
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). |
|||
|
|||
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path |
|||
> `git config --system core.longpaths true` |
|||
|
|||
{{if UI == "MVC" && DB == "EF"}} |
|||
|
|||
### 视频教程 |
|||
|
|||
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=1WsfMITN_Jk&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=5" target="_blank">发布在YouTube</a>**. |
|||
|
|||
{{end}} |
|||
|
|||
## 权限 |
|||
|
|||
ABP框架提供了一个基于ASP.NET Core[授权基础架构](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction)的[授权系统](../Authorization.md). 基于标准授权基础架构的一个主要功能是添加了 **权限系统**, 这个系统允许定义权限并且根据角色, 用户或客户端启用/禁用权限. |
|||
|
|||
### 权限名称 |
|||
|
|||
权限必须有唯一的名称 (一个 `字符串`). 最好的方法是把它定义为一个 `常量`, 这样我们就可以重用这个权限名称了. |
|||
|
|||
打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissions` 类 (位于 `Permissions` 文件夹) 并替换为以下代码: |
|||
|
|||
````csharp |
|||
namespace Acme.BookStore.Permissions |
|||
{ |
|||
public static class BookStorePermissions |
|||
{ |
|||
public const string GroupName = "BookStore"; |
|||
|
|||
public static class Books |
|||
{ |
|||
public const string Default = GroupName + ".Books"; |
|||
public const string Create = Default + ".Create"; |
|||
public const string Edit = Default + ".Edit"; |
|||
public const string Delete = Default + ".Delete"; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
权限名称具有层次结构. 例如, "创建图书" 权限被定义为 `BookStore.Books.Create`. ABP不强制必须如此, 但这是一种有益的做法. |
|||
|
|||
### 权限定义 |
|||
|
|||
在使用权限前必须定义它们. |
|||
|
|||
打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissionDefinitionProvider` 类 (位于 `Permissions` 文件夹) 并替换为以下代码: |
|||
|
|||
````csharp |
|||
using Acme.BookStore.Localization; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Acme.BookStore.Permissions |
|||
{ |
|||
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider |
|||
{ |
|||
public override void Define(IPermissionDefinitionContext context) |
|||
{ |
|||
var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); |
|||
|
|||
var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); |
|||
booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); |
|||
booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit")); |
|||
booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete")); |
|||
} |
|||
|
|||
private static LocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<BookStoreResource>(name); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这个类定义了一个 **权限组** (在UI上分组权限, 下文会看到) 和 权限组中的**4个权限**. 而且, **创建**, **编辑** 和 **删除** 是 `BookStorePermissions.Books.Default` 权限的子权限. **仅当父权限被选择**时, 子权限才能被选择. |
|||
|
|||
最后, 编辑本地化文件 (`Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹中的 `en.json`) 定义上面使用的本地化键: |
|||
|
|||
````json |
|||
"Permission:BookStore": "Book Store", |
|||
"Permission:Books": "Book Management", |
|||
"Permission:Books.Create": "Creating new books", |
|||
"Permission:Books.Edit": "Editing the books", |
|||
"Permission:Books.Delete": "Deleting the books" |
|||
```` |
|||
|
|||
> 本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定. |
|||
|
|||
### 权限管理界面 |
|||
|
|||
完成权限定义后, 可以在**权限管理模态窗口**看到它们. |
|||
|
|||
在*管理 -> Identity -> 角色* 页面, 选择admin角色的 *权限* 操作, 打开权限管理模态窗口: |
|||
|
|||
 |
|||
|
|||
授予你希望的权限并保存. |
|||
|
|||
> **提示**: 如果运行 `Acme.BookStore.DbMigrator` 应用程序, 新权限会被自动授予admin. |
|||
|
|||
## 授权 |
|||
|
|||
现在, 你可以使用权限授权图书管理. |
|||
|
|||
### 应用层 和 HTTP API |
|||
|
|||
打开 the `BookAppService` 类, 设置策略名称为上面定义的权限名称. |
|||
|
|||
````csharp |
|||
using System; |
|||
using Acme.BookStore.Permissions; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore.Books |
|||
{ |
|||
public class BookAppService : |
|||
CrudAppService< |
|||
Book, //The Book entity |
|||
BookDto, //Used to show books |
|||
Guid, //Primary key of the book entity |
|||
PagedAndSortedResultRequestDto, //Used for paging/sorting |
|||
CreateUpdateBookDto>, //Used to create/update a book |
|||
IBookAppService //implement the IBookAppService |
|||
{ |
|||
public BookAppService(IRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
GetPolicyName = BookStorePermissions.Books.Default; |
|||
GetListPolicyName = BookStorePermissions.Books.Default; |
|||
CreatePolicyName = BookStorePermissions.Books.Create; |
|||
UpdatePolicyName = BookStorePermissions.Books.Edit; |
|||
DeletePolicyName = BookStorePermissions.Books.Delete; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
加入代码到构造器. 基类中的 `CrudAppService` 自动在CRUD操作中使用这些权限. 这不仅实现了 **应用服务** 的安全性, 也实现了 **HTTP API** 安全性, 因为如前解释的, HTTP API 自动使用这些服务. (参阅 [自动 API controllers](../API/Auto-API-Controllers.md)). |
|||
|
|||
> 在稍后开发作者管理功能时, 你将会看到声明式授权, 使用 `[Authorize(...)]` 特性. |
|||
|
|||
{{if UI == "MVC"}} |
|||
|
|||
### Razor 页面 |
|||
|
|||
虽然安全的 HTTP API和应用服务阻止未授权用户使用服务, 但他们依然可以导航到图书管理页面. 虽然当页面发起第一个访问服务器的AJAX请求时会收到授权异常, 但为了更好的用户体验和安全性, 我们应该对页面进行授权. |
|||
|
|||
打开 `BookStoreWebModule` 在 `ConfigureServices` 方法中加入以下代码: |
|||
|
|||
````csharp |
|||
Configure<RazorPagesOptions>(options => |
|||
{ |
|||
options.Conventions.AuthorizePage("/Books/Index", BookStorePermissions.Books.Default); |
|||
options.Conventions.AuthorizePage("/Books/CreateModal", BookStorePermissions.Books.Create); |
|||
options.Conventions.AuthorizePage("/Books/EditModal", BookStorePermissions.Books.Edit); |
|||
}); |
|||
```` |
|||
|
|||
现在未授权用户会被重定向至**登录页面**. |
|||
|
|||
#### 隐藏新建图书按钮 |
|||
|
|||
图书管理页面有一个 *新建图书* 按钮, 当用户没有 *图书新建* 权限时就不可见的. |
|||
|
|||
 |
|||
|
|||
打开 the `Pages/Books/Index.cshtml` 文件, 替换内容为以下代码: |
|||
|
|||
````html |
|||
@page |
|||
@using Acme.BookStore.Localization |
|||
@using Acme.BookStore.Permissions |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@using Microsoft.AspNetCore.Authorization |
|||
@using Microsoft.Extensions.Localization |
|||
@model IndexModel |
|||
@inject IStringLocalizer<BookStoreResource> L |
|||
@inject IAuthorizationService AuthorizationService |
|||
@section scripts |
|||
{ |
|||
<abp-script src="/Pages/Books/Index.js"/> |
|||
} |
|||
|
|||
<abp-card> |
|||
<abp-card-header> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<abp-card-title>@L["Books"]</abp-card-title> |
|||
</abp-column> |
|||
<abp-column size-md="_6" class="text-right"> |
|||
@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create)) |
|||
{ |
|||
<abp-button id="NewBookButton" |
|||
text="@L["NewBook"].Value" |
|||
icon="plus" |
|||
button-type="Primary"/> |
|||
} |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-table striped-rows="true" id="BooksTable"></abp-table> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
```` |
|||
|
|||
* 加入 `@inject IAuthorizationService AuthorizationService` 以访问授权服务. |
|||
* 使用 `@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))` 检查图书创建权限, 条件显示 *新建图书* 按钮. |
|||
|
|||
### JavaScript端 |
|||
|
|||
图书管理页面中的图书表格每行都有操作按钮. 操作按钮包括 *编辑* 和 *删除* 操作: |
|||
|
|||
 |
|||
|
|||
如果用户没有权限, 应该隐藏相关的操作. 表格行中的操作有一个 `visible` 属性, 可以设置为 `false` 隐藏操作项. |
|||
|
|||
打开 `Acme.BookStore.Web` 项目中的 `Pages/Books/Index.js`, 为 `编辑` 操作加入 `visible` 属性: |
|||
|
|||
````js |
|||
{ |
|||
text: l('Edit'), |
|||
visible: abp.auth.isGranted('BookStore.Books.Edit'), //CHECK for the PERMISSION |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
对 `Delete` 操作进行同样的操作: |
|||
|
|||
````js |
|||
visible: abp.auth.isGranted('BookStore.Books.Delete') |
|||
```` |
|||
|
|||
* `abp.auth.isGranted(...)` 检查前面定义的权限. |
|||
* `visible` 也可以是一个返回 `bool` 值的函数. 这个函数可以稍后根据某些条件计算. |
|||
|
|||
### 菜单项 |
|||
|
|||
即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项. |
|||
|
|||
打开 `BookStoreMenuContributor` 类, 找到下面的代码: |
|||
|
|||
````csharp |
|||
context.Menu.AddItem( |
|||
new ApplicationMenuItem( |
|||
"BooksStore", |
|||
l["Menu:BookStore"], |
|||
icon: "fa fa-book" |
|||
).AddItem( |
|||
new ApplicationMenuItem( |
|||
"BooksStore.Books", |
|||
l["Menu:Books"], |
|||
url: "/Books" |
|||
) |
|||
) |
|||
); |
|||
```` |
|||
|
|||
替换为以下代码: |
|||
|
|||
````csharp |
|||
var bookStoreMenu = new ApplicationMenuItem( |
|||
"BooksStore", |
|||
l["Menu:BookStore"], |
|||
icon: "fa fa-book" |
|||
); |
|||
|
|||
context.Menu.AddItem(bookStoreMenu); |
|||
|
|||
//CHECK the PERMISSION |
|||
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) |
|||
{ |
|||
bookStoreMenu.AddItem(new ApplicationMenuItem( |
|||
"BooksStore.Books", |
|||
l["Menu:Books"], |
|||
url: "/Books" |
|||
)); |
|||
} |
|||
```` |
|||
|
|||
你需要为 `ConfigureMenuAsync` 方法加入 `async` 关键字, 并重新组织返回值. 最终的 `BookStoreMenuContributor` 类应该如下: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
using Acme.BookStore.Localization; |
|||
using Acme.BookStore.MultiTenancy; |
|||
using Acme.BookStore.Permissions; |
|||
using Volo.Abp.TenantManagement.Web.Navigation; |
|||
using Volo.Abp.UI.Navigation; |
|||
|
|||
namespace Acme.BookStore.Web.Menus |
|||
{ |
|||
public class BookStoreMenuContributor : IMenuContributor |
|||
{ |
|||
public async Task ConfigureMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
if (context.Menu.Name == StandardMenus.Main) |
|||
{ |
|||
await ConfigureMainMenuAsync(context); |
|||
} |
|||
} |
|||
|
|||
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
if (!MultiTenancyConsts.IsEnabled) |
|||
{ |
|||
var administration = context.Menu.GetAdministration(); |
|||
administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName); |
|||
} |
|||
|
|||
var l = context.GetLocalizer<BookStoreResource>(); |
|||
|
|||
context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/")); |
|||
|
|||
var bookStoreMenu = new ApplicationMenuItem( |
|||
"BooksStore", |
|||
l["Menu:BookStore"], |
|||
icon: "fa fa-book" |
|||
); |
|||
|
|||
context.Menu.AddItem(bookStoreMenu); |
|||
|
|||
//CHECK the PERMISSION |
|||
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) |
|||
{ |
|||
bookStoreMenu.AddItem(new ApplicationMenuItem( |
|||
"BooksStore.Books", |
|||
l["Menu:Books"], |
|||
url: "/Books" |
|||
)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
{{else if UI == "NG"}} |
|||
|
|||
### Angular Guard 配置 |
|||
|
|||
UI的第一步是防止未认证用户看见"图书"菜单项并进入图书管理页面. |
|||
|
|||
打开 `/src/app/book/book-routing.module.ts` 替换为以下代码: |
|||
|
|||
````js |
|||
import { NgModule } from '@angular/core'; |
|||
import { Routes, RouterModule } from '@angular/router'; |
|||
import { AuthGuard, PermissionGuard } from '@abp/ng.core'; |
|||
import { BookComponent } from './book.component'; |
|||
|
|||
const routes: Routes = [ |
|||
{ path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] }, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class BookRoutingModule {} |
|||
```` |
|||
|
|||
* 从 `@abp/ng.core` 引入 `AuthGuard` 和 `PermissionGuard`. |
|||
* 在路由定义中添加 `canActivate: [AuthGuard, PermissionGuard]`. |
|||
|
|||
打开 `/src/app/route.provider.ts`, 在 `/books` 路由中添加 `requiredPolicy: 'BookStore.Books'`. `/books` 路由应该如以下配置: |
|||
|
|||
````js |
|||
{ |
|||
path: '/books', |
|||
name: '::Menu:Books', |
|||
parentName: '::Menu:BookStore', |
|||
layout: eLayoutType.application, |
|||
requiredPolicy: 'BookStore.Books', |
|||
} |
|||
```` |
|||
|
|||
### 隐藏新建图书按钮 |
|||
|
|||
当用户没有 *图书新建* 权限时, 图书管理页面上的 *新建图书* 按钮应该不可见. |
|||
|
|||
 |
|||
|
|||
打开 `/src/app/book/book.component.html` 文件, 替换创建按钮的HTML内容如下: |
|||
|
|||
````html |
|||
<!-- Add the abpPermission directive --> |
|||
<button *abpPermission="'BookStore.Books.Create'" id="create" class="btn btn-primary" type="button" (click)="createBook()"> |
|||
<i class="fa fa-plus mr-1"></i> |
|||
<span>{%{{{ '::NewBook' | abpLocalization }}}%}</span> |
|||
</button> |
|||
```` |
|||
|
|||
* 加入 `*abpPermission="'BookStore.Books.Create'"`, 当用户没有权限时隐藏按钮. |
|||
|
|||
### 隐藏编辑和删除操作 |
|||
|
|||
图书管理页面中的图书表格每行都有操作按钮. 操作按钮包括 *编辑* 和 *删除* 操作: |
|||
|
|||
 |
|||
|
|||
如果用户没有权限, 应该隐藏相关的操作. |
|||
|
|||
打开 `/src/app/book/book.component.html` 文件, 替换编辑和删除按钮的内容如下: |
|||
|
|||
````html |
|||
<!-- Add the abpPermission directive --> |
|||
<button *abpPermission="'BookStore.Books.Edit'" ngbDropdownItem (click)="editBook(row.id)"> |
|||
{%{{{ '::Edit' | abpLocalization }}}%} |
|||
</button> |
|||
|
|||
<!-- Add the abpPermission directive --> |
|||
<button *abpPermission="'BookStore.Books.Delete'" ngbDropdownItem (click)="delete(row.id)"> |
|||
{%{{{ '::Delete' | abpLocalization }}}%} |
|||
</button> |
|||
```` |
|||
|
|||
* 加入 `*abpPermission="'BookStore.Books.Create'"`, 当用户没有编辑权限时隐藏按钮. |
|||
* 加入 `*abpPermission="'BookStore.Books.Delete'"`, 当用户没有删除权限时隐藏按钮. |
|||
|
|||
{{else if UI == "Blazor"}} |
|||
|
|||
### Razor验证组件 |
|||
|
|||
打开 `Acme.BookStore.Blazor` 项目中的 `/Pages/Books.razor` 文件, 在` @page` 指令和命名空间引入(`@using` 行)后添加 `Authorize` 特性, 如下所示: |
|||
|
|||
````html |
|||
@page "/books" |
|||
@attribute [Authorize(BookStorePermissions.Books.Default)] |
|||
@using Acme.BookStore.Permissions |
|||
@using Microsoft.AspNetCore.Authorization |
|||
... |
|||
```` |
|||
|
|||
添加这个特性阻止未登录用户或未授权用户访问这个页面. 用户重试后, 会被重定向到登录页面. |
|||
|
|||
### 显示/隐藏操作 |
|||
|
|||
图书管理页面上的每一种图书都有 *新建* 按钮和 *编辑*, *删除* 操作. 如果用户没有相关权限, 这些按钮/操作应该被隐藏. |
|||
|
|||
基类 `AbpCrudPageBase` 已经具有这些操作需要的功能. |
|||
|
|||
#### 设置策略 (权限) 名称 |
|||
|
|||
加入以下代码到 `Books.razor` 文件结尾: |
|||
|
|||
````csharp |
|||
@code |
|||
{ |
|||
public Books() // Constructor |
|||
{ |
|||
CreatePolicyName = BookStorePermissions.Books.Create; |
|||
UpdatePolicyName = BookStorePermissions.Books.Edit; |
|||
DeletePolicyName = BookStorePermissions.Books.Delete; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
基类 `AbpCrudPageBase` 自动检查相关操作的权限. 如果需要手动检查, 它也定义了相应的属性. |
|||
|
|||
* `HasCreatePermission`: True, 如果用户具有新建实体的权限. |
|||
* `HasUpdatePermission`: True, 如果用户具有编辑/更新实体的权限. |
|||
* `HasDeletePermission`: True, 如果用户具有删除实体的权限. |
|||
|
|||
> **Blazor 提示**: 当添加少量代码到 `@code` 是没有问题的. 当添加的代码变长时, 建议使用代码后置方法以便于维护. 我们将在作者部分使用这个方法. |
|||
|
|||
#### 隐藏新建图书按钮 |
|||
|
|||
检查 *新建图书* 按钮权限: |
|||
|
|||
````xml |
|||
@if (HasCreatePermission) |
|||
{ |
|||
<Button Color="Color.Primary" |
|||
Clicked="OpenCreateModalAsync">@L["NewBook"]</Button> |
|||
} |
|||
```` |
|||
|
|||
#### 隐藏编辑/删除操作 |
|||
|
|||
`EntityAction` 组件定义了 `Visible` 属性 (参数) 以条件显示操作. |
|||
|
|||
更新 `EntityActions` 部分: |
|||
|
|||
````xml |
|||
<EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn"> |
|||
<EntityAction TItem="BookDto" |
|||
Text="@L["Edit"]" |
|||
Visible=HasUpdatePermission |
|||
Clicked="() => OpenEditModalAsync(context)" /> |
|||
<EntityAction TItem="BookDto" |
|||
Text="@L["Delete"]" |
|||
Visible=HasDeletePermission |
|||
Clicked="() => DeleteEntityAsync(context)" |
|||
ConfirmationMessage="()=>GetDeleteConfirmationMessage(context)" /> |
|||
</EntityActions> |
|||
```` |
|||
|
|||
#### 关于权限缓存 |
|||
|
|||
你可以运行和测试权限. 从admin角色中移除一个图书相关权限, 观察到相关按钮/操作从UI上消失. |
|||
|
|||
在客户端, **ABP框架缓存当前用户的权限** . 所以, 当你修改了你的权限, 你需要手工 **刷新页面**. 如果不刷新并试图使用被禁的操作, 你会从服务器收到一个HTTP 403 (forbidden) 响应. |
|||
|
|||
> 修改角色或用户的权限在服务端立即生效. 所以, 缓存系统不会导致安全问题. |
|||
|
|||
### 菜单项 |
|||
|
|||
即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项. |
|||
|
|||
打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreMenuContributor` 类, 找到以下代码: |
|||
|
|||
````csharp |
|||
context.Menu.AddItem( |
|||
new ApplicationMenuItem( |
|||
"BooksStore", |
|||
l["Menu:BookStore"], |
|||
icon: "fa fa-book" |
|||
).AddItem( |
|||
new ApplicationMenuItem( |
|||
"BooksStore.Books", |
|||
l["Menu:Books"], |
|||
url: "/books" |
|||
) |
|||
) |
|||
); |
|||
```` |
|||
|
|||
替换为以下代码: |
|||
|
|||
````csharp |
|||
var bookStoreMenu = new ApplicationMenuItem( |
|||
"BooksStore", |
|||
l["Menu:BookStore"], |
|||
icon: "fa fa-book" |
|||
); |
|||
|
|||
context.Menu.AddItem(bookStoreMenu); |
|||
|
|||
//CHECK the PERMISSION |
|||
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) |
|||
{ |
|||
bookStoreMenu.AddItem(new ApplicationMenuItem( |
|||
"BooksStore.Books", |
|||
l["Menu:Books"], |
|||
url: "/books" |
|||
)); |
|||
} |
|||
```` |
|||
|
|||
你需要为 `ConfigureMenuAsync` 方法加入 `async` 关键字并重新整理返回值. 最终的 `ConfigureMainMenuAsync` 方法如下: |
|||
|
|||
````csharp |
|||
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
var l = context.GetLocalizer<BookStoreResource>(); |
|||
|
|||
context.Menu.Items.Insert( |
|||
0, |
|||
new ApplicationMenuItem( |
|||
"BookStore.Home", |
|||
l["Menu:Home"], |
|||
"/", |
|||
icon: "fas fa-home" |
|||
) |
|||
); |
|||
|
|||
var bookStoreMenu = new ApplicationMenuItem( |
|||
"BooksStore", |
|||
l["Menu:BookStore"], |
|||
icon: "fa fa-book" |
|||
); |
|||
|
|||
context.Menu.AddItem(bookStoreMenu); |
|||
|
|||
//CHECK the PERMISSION |
|||
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) |
|||
{ |
|||
bookStoreMenu.AddItem(new ApplicationMenuItem( |
|||
"BooksStore.Books", |
|||
l["Menu:Books"], |
|||
url: "/books" |
|||
)); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
{{end}} |
|||
|
|||
## 下一章 |
|||
|
|||
查看本教程的[下一章](Part-6.md). |
|||
|
|||
@ -1 +1,278 @@ |
|||
TODO.. |
|||
# Web应用程序开发教程 - 第六章: 作者: 领域层 |
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","Blazor","BlazorServer","NG"], |
|||
"DB": ["EF","Mongo"] |
|||
} |
|||
```` |
|||
## 关于本教程 |
|||
|
|||
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: |
|||
|
|||
* **{{DB_Text}}** 做为ORM提供程序. |
|||
* **{{UI_Value}}** 做为UI框架. |
|||
|
|||
本教程分为以下部分: |
|||
|
|||
- [Part 1: 创建服务端](Part-1.md) |
|||
- [Part 2: 图书列表页面](Part-2.md) |
|||
- [Part 3: 创建,更新和删除图书](Part-2.md) |
|||
- [Part 4: 集成测试](Part-4.md) |
|||
- [Part 5: 授权](Part-5.md) |
|||
- **Part 6: 作者: 领域层**(本章) |
|||
- [Part 7: 作者: 数据库集成](Part-7.md) |
|||
- [Part 8: 作者: 应用服务层](Part-8.md) |
|||
- [Part 9: 作者: 用户页面](Part-9.md) |
|||
- [Part 10: 图书到作者的关系](Part-10.md) |
|||
|
|||
## 下载源码 |
|||
|
|||
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: |
|||
|
|||
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) |
|||
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) |
|||
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). |
|||
|
|||
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path |
|||
> `git config --system core.longpaths true` |
|||
|
|||
## 简介 |
|||
|
|||
在前面的章节中, 我们使用 ABP 框架轻松地构建了一些服务; |
|||
|
|||
* 使用 [CrudAppService](../Application-Services.md) 基类, 而不是为标准的增删改查操作手工开发应用服务. |
|||
* 使用 [generic repositories](../Repositories.md) 自动完成数据层功能. |
|||
|
|||
对于 "作者" 部分; |
|||
|
|||
* 我们将要展示在需要的情况下, 如何 **手工做一些事情**. |
|||
* 我们将要实现一些 **领域驱动设计 (DDD) 最佳实践**. |
|||
|
|||
> **开发将会逐层完成, 一次聚焦一层. 在真实项目中, 你会逐个功能(垂直)开发, 如同前面的教程. 通过这种方式, 你可以体验这两种方式** |
|||
|
|||
## 作者实体 |
|||
|
|||
在 `Acme.BookStore.Domain` 项目中创建 `Authors` 文件夹 (命名空间), 在其中加入 `Author` 类: |
|||
|
|||
````csharp |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Domain.Entities.Auditing; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class Author : FullAuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Name { get; private set; } |
|||
public DateTime BirthDate { get; set; } |
|||
public string ShortBio { get; set; } |
|||
|
|||
private Author() |
|||
{ |
|||
/* This constructor is for deserialization / ORM purpose */ |
|||
} |
|||
|
|||
internal Author( |
|||
Guid id, |
|||
[NotNull] string name, |
|||
DateTime birthDate, |
|||
[CanBeNull] string shortBio = null) |
|||
: base(id) |
|||
{ |
|||
SetName(name); |
|||
BirthDate = birthDate; |
|||
ShortBio = shortBio; |
|||
} |
|||
|
|||
internal Author ChangeName([NotNull] string name) |
|||
{ |
|||
SetName(name); |
|||
return this; |
|||
} |
|||
|
|||
private void SetName([NotNull] string name) |
|||
{ |
|||
Name = Check.NotNullOrWhiteSpace( |
|||
name, |
|||
nameof(name), |
|||
maxLength: AuthorConsts.MaxNameLength |
|||
); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 由 `FullAuditedAggregateRoot<Guid>` 继承使得实体支持[软删除](../Data-Filtering.md) (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了 [审计](../Entities.md) 属性. |
|||
* `Name` 属性的 `private set` 限制从类的外部设置这个属性. 有两种方法设置名字 (两种都进行了验证): |
|||
* 当新建一个作者时, 通过构造器. |
|||
* 使用 `ChangeName` 方法更新名字. |
|||
* `构造器` 和 `ChangeName` 方法的访问级别是 `internal`, 强制这些方法只能在领域层由 `AuthorManager` 使用. 稍后将对此进行解释. |
|||
* `Check` 类是一个ABP框架工具类, 用于检查方法参数 (如果参数非法会抛出 `ArgumentException`). |
|||
|
|||
`AuthorConsts` 是一个简单的类, 它位于 `Acme.BookStore.Domain.Shared` 项目的 `Authors` 命名空间 (文件夹)中: |
|||
|
|||
````csharp |
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public static class AuthorConsts |
|||
{ |
|||
public const int MaxNameLength = 64; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
在 `Acme.BookStore.Domain.Shared` 项目中创建这个类, 因为[数据传输类](../Data-Transfer-Objects.md) (DTOs) 稍后会再一次用到它. |
|||
|
|||
## AuthorManager: 领域服务 |
|||
|
|||
`Author` 构造器和 `ChangeName` 方法的访问级别是 `internal`, 所以它们只能在领域层使用. 在 `Acme.BookStore.Domain` 项目中的 `Authors` 文件夹 (命名空间)创建 `AuthorManager` 类: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Domain.Services; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class AuthorManager : DomainService |
|||
{ |
|||
private readonly IAuthorRepository _authorRepository; |
|||
|
|||
public AuthorManager(IAuthorRepository authorRepository) |
|||
{ |
|||
_authorRepository = authorRepository; |
|||
} |
|||
|
|||
public async Task<Author> CreateAsync( |
|||
[NotNull] string name, |
|||
DateTime birthDate, |
|||
[CanBeNull] string shortBio = null) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(name, nameof(name)); |
|||
|
|||
var existingAuthor = await _authorRepository.FindByNameAsync(name); |
|||
if (existingAuthor != null) |
|||
{ |
|||
throw new AuthorAlreadyExistsException(name); |
|||
} |
|||
|
|||
return new Author( |
|||
GuidGenerator.Create(), |
|||
name, |
|||
birthDate, |
|||
shortBio |
|||
); |
|||
} |
|||
|
|||
public async Task ChangeNameAsync( |
|||
[NotNull] Author author, |
|||
[NotNull] string newName) |
|||
{ |
|||
Check.NotNull(author, nameof(author)); |
|||
Check.NotNullOrWhiteSpace(newName, nameof(newName)); |
|||
|
|||
var existingAuthor = await _authorRepository.FindByNameAsync(newName); |
|||
if (existingAuthor != null && existingAuthor.Id != author.Id) |
|||
{ |
|||
throw new AuthorAlreadyExistsException(newName); |
|||
} |
|||
|
|||
author.ChangeName(newName); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `AuthorManager` 强制使用一种可控的方式创建作者和修改作者的名字. 应用层 (后面会介绍) 将会使用这些方法. |
|||
|
|||
> **DDD 提示**: 如非必须并且用于执行核心业务规则, 不要引入领域服务方法. 对于这个场景, 我们使用这个服务保证名字的唯一性. |
|||
|
|||
两个方法都检查是否存在同名用户, 如果存在, 抛出业务异常 `AuthorAlreadyExistsException`, 这个异常定义在 `Acme.BookStore.Domain` 项目 (`Authors` 文件夹中): |
|||
|
|||
````csharp |
|||
using Volo.Abp; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class AuthorAlreadyExistsException : BusinessException |
|||
{ |
|||
public AuthorAlreadyExistsException(string name) |
|||
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists) |
|||
{ |
|||
WithData("name", name); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`BusinessException` 是一个特殊的异常类型. 在需要时抛出领域相关异常是一个好的实践. ABP框架会自动处理它, 并且它也容易本地化. `WithData(...)` 方法提供额外的数据给异常对象, 这些数据将会在本地化中或出于其它一些目的被使用. |
|||
|
|||
打开 `Acme.BookStore.Domain.Shared` 项目中的 `BookStoreDomainErrorCodes` 并修改为: |
|||
|
|||
````csharp |
|||
namespace Acme.BookStore |
|||
{ |
|||
public static class BookStoreDomainErrorCodes |
|||
{ |
|||
public const string AuthorAlreadyExists = "BookStore:00001"; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这里定义了一个字符串, 表示应用程序抛出的错误码, 这个错误码可以被客户端应用程序处理. 为了用户, 你可能希望本地化它. 打开 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` , 加入以下项: |
|||
|
|||
````json |
|||
"BookStore:00001": "There is already an author with the same name: {name}" |
|||
```` |
|||
|
|||
当 `AuthorAlreadyExistsException` 被抛出, 终端用户将会在UI上看到组织好的错误消息. |
|||
|
|||
## IAuthorRepository |
|||
|
|||
`AuthorManager` 注入了 `IAuthorRepository`, 所以我们需要定义它. 在 `Acme.BookStore.Domain` 项目的 `Authors` 文件夹 (命名空间) 中创建这个新接口: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public interface IAuthorRepository : IRepository<Author, Guid> |
|||
{ |
|||
Task<Author> FindByNameAsync(string name); |
|||
|
|||
Task<List<Author>> GetListAsync( |
|||
int skipCount, |
|||
int maxResultCount, |
|||
string sorting, |
|||
string filter = null |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `IAuthorRepository` 扩展了标准 `IRepository<Author, Guid>` 接口, 所以所有的标准 [repository](../Repositories.md) 方法对于 `IAuthorRepository` 都是可用的. |
|||
* `FindByNameAsync` 在 `AuthorManager` 中用来根据姓名查询用户. |
|||
* `GetListAsync` 用于应用层以获得一个排序的, 经过过滤的作者列表, 显示在UI上. |
|||
|
|||
我们会在下一章实现这个repository. |
|||
|
|||
> 这两个方法似乎 **看上去没有必要**, 因为标准repositories已经是 `IQueryable`, 你可以直接使用它们, 而不是自定义方法. 在实际应用程序中, 这么做是没问题的. 但在这个 **学习指南**中, 解释如何在需要时创建自定义repository方法是有价值的. |
|||
|
|||
## 结论 |
|||
|
|||
这一章覆盖了图书管理程序作者相关功能的领域层. 在这一章中创建/更新的文件在下图中被高亮: |
|||
|
|||
 |
|||
|
|||
## 下一章 |
|||
|
|||
查看本教程的[下一章](Part-7.md). |
|||
|
|||
@ -1 +1,238 @@ |
|||
TODO.. |
|||
# Web应用程序开发教程 - 第七章: 数据库集成 |
|||
````json |
|||
//[doc-params] |
|||
{ |
|||
"UI": ["MVC","Blazor","BlazorServer","NG"], |
|||
"DB": ["EF","Mongo"] |
|||
} |
|||
```` |
|||
## 关于本教程 |
|||
|
|||
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: |
|||
|
|||
* **{{DB_Text}}** 做为ORM提供程序. |
|||
* **{{UI_Value}}** 做为UI框架. |
|||
|
|||
本教程分为以下部分: |
|||
|
|||
- [Part 1: 创建服务端](Part-1.md) |
|||
- [Part 2: 图书列表页面](Part-2.md) |
|||
- [Part 3: 创建,更新和删除图书](Part-2.md) |
|||
- [Part 4: 集成测试](Part-4.md) |
|||
- [Part 5: 授权](Part-5.md) |
|||
- [Part 6: 作者: 领域层](Part-6.md) |
|||
- **Part 7: 数据库集成**(本章) |
|||
- [Part 8: 作者: 应用服务层](Part-8.md) |
|||
- [Part 9: 作者: 用户页面](Part-9.md) |
|||
- [Part 10: 图书到作者的关系](Part-10.md) |
|||
|
|||
## 下载源码 |
|||
|
|||
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: |
|||
|
|||
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) |
|||
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) |
|||
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) |
|||
|
|||
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). |
|||
|
|||
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path |
|||
> `git config --system core.longpaths true` |
|||
|
|||
## 简介 |
|||
|
|||
这章阐述如何为前一章介绍的 `作者` 实体配置数据库集成. |
|||
|
|||
{{if DB=="EF"}} |
|||
|
|||
## DB Context |
|||
|
|||
打开 `Acme.BookStore.EntityFrameworkCore` 项目中的 `BookStoreDbContext` 加入 `DbSet` 属性: |
|||
|
|||
````csharp |
|||
public DbSet<Author> Authors { get; set; } |
|||
```` |
|||
|
|||
定位到相同项目中的 `BookStoreDbContext` 类中的 `OnModelCreating` 方法, 加入以下代码到方法的结尾: |
|||
|
|||
````csharp |
|||
builder.Entity<Author>(b => |
|||
{ |
|||
b.ToTable(BookStoreConsts.DbTablePrefix + "Authors", |
|||
BookStoreConsts.DbSchema); |
|||
|
|||
b.ConfigureByConvention(); |
|||
|
|||
b.Property(x => x.Name) |
|||
.IsRequired() |
|||
.HasMaxLength(AuthorConsts.MaxNameLength); |
|||
|
|||
b.HasIndex(x => x.Name); |
|||
}); |
|||
```` |
|||
|
|||
这和前面的 `Book` 实体做的一样, 所以不再赘述. |
|||
|
|||
## 创建数据库迁移 |
|||
|
|||
配置启动解决方案为使用 [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). 因为我们还没有修改数据库映射配置,所以需要创建一个新的迁移并对数据库应用变更. |
|||
|
|||
打开命令行终端, 切换当前目录为 `Acme.BookStore.EntityFrameworkCore` 项目目录, 输入以下命令: |
|||
|
|||
````bash |
|||
dotnet ef migrations add Added_Authors |
|||
```` |
|||
|
|||
这会在项目中添加一个迁移类: |
|||
|
|||
 |
|||
|
|||
你可以在同一个命令行终端中使用以下命令对数据库应用更改: |
|||
|
|||
````bash |
|||
dotnet ef database update |
|||
```` |
|||
|
|||
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreMigrationsDbContext` 和 `Update-Database -c BookStoreMigrationsDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* . |
|||
|
|||
{{else if DB=="Mongo"}} |
|||
|
|||
## DB Context |
|||
|
|||
打开 `Acme.BookStore.MongoDB` 项目 `MongoDb 文件夹`中的 `BookStoreMongoDbContext`, 在类中加入以下属性: |
|||
|
|||
````csharp |
|||
public IMongoCollection<Author> Authors => Collection<Author>(); |
|||
```` |
|||
|
|||
{{end}} |
|||
|
|||
## 实现 IAuthorRepository |
|||
|
|||
{{if DB=="EF"}} |
|||
|
|||
在 `Acme.BookStore.EntityFrameworkCore` 项目 (`Authors` 文件夹)中创建一个新类 `EfCoreAuthorRepository`, 粘贴以下代码: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Linq.Dynamic.Core; |
|||
using System.Threading.Tasks; |
|||
using Acme.BookStore.EntityFrameworkCore; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class EfCoreAuthorRepository |
|||
: EfCoreRepository<BookStoreDbContext, Author, Guid>, |
|||
IAuthorRepository |
|||
{ |
|||
public EfCoreAuthorRepository( |
|||
IDbContextProvider<BookStoreDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public async Task<Author> FindByNameAsync(string name) |
|||
{ |
|||
var dbSet = await GetDbSetAsync(); |
|||
return await dbSet.FirstOrDefaultAsync(author => author.Name == name); |
|||
} |
|||
|
|||
public async Task<List<Author>> GetListAsync( |
|||
int skipCount, |
|||
int maxResultCount, |
|||
string sorting, |
|||
string filter = null) |
|||
{ |
|||
var dbSet = await GetDbSetAsync(); |
|||
return await dbSet |
|||
.WhereIf( |
|||
!filter.IsNullOrWhiteSpace(), |
|||
author => author.Name.Contains(filter) |
|||
) |
|||
.OrderBy(sorting) |
|||
.Skip(skipCount) |
|||
.Take(maxResultCount) |
|||
.ToListAsync(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 继承自 `EfCoreRepository`, 所以继承了标准repository的方法实现. |
|||
* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率. |
|||
* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC` 或 `Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的. |
|||
|
|||
> 参阅 [EF Core 集成文档](../Entity-Framework-Core.md) 获得基于EF Core的repositories的更多信息. |
|||
|
|||
{{else if DB=="Mongo"}} |
|||
|
|||
在 `Acme.BookStore.MongoDB` 项目 (`Authors` 文件夹)中创建一个新类 `MongoDbAuthorRepository`, 粘贴以下代码: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Linq; |
|||
using System.Linq.Dynamic.Core; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Acme.BookStore.MongoDB; |
|||
using MongoDB.Driver; |
|||
using MongoDB.Driver.Linq; |
|||
using Volo.Abp.Domain.Repositories.MongoDB; |
|||
using Volo.Abp.MongoDB; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class MongoDbAuthorRepository |
|||
: MongoDbRepository<BookStoreMongoDbContext, Author, Guid>, |
|||
IAuthorRepository |
|||
{ |
|||
public MongoDbAuthorRepository( |
|||
IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider |
|||
) : base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public async Task<Author> FindByNameAsync(string name) |
|||
{ |
|||
var queryable = await GetMongoQueryableAsync(); |
|||
return await queryable.FirstOrDefaultAsync(author => author.Name == name); |
|||
} |
|||
|
|||
public async Task<List<Author>> GetListAsync( |
|||
int skipCount, |
|||
int maxResultCount, |
|||
string sorting, |
|||
string filter = null) |
|||
{ |
|||
var queryable = await GetMongoQueryableAsync(); |
|||
return await queryable |
|||
.WhereIf<Author, IMongoQueryable<Author>>( |
|||
!filter.IsNullOrWhiteSpace(), |
|||
author => author.Name.Contains(filter) |
|||
) |
|||
.OrderBy(sorting) |
|||
.As<IMongoQueryable<Author>>() |
|||
.Skip(skipCount) |
|||
.Take(maxResultCount) |
|||
.ToListAsync(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* 继承自 `MongoDbRepository`, 所以继承了标准repository的方法实现. |
|||
* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率. |
|||
* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC` 或 `Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的. |
|||
|
|||
> 参阅 [MongoDB 集成文档](../MongoDB.md) 获得基于MongoDB的repositories的更多信息. |
|||
|
|||
{{end}} |
|||
|
|||
## 下一章 |
|||
|
|||
查看本教程的[下一章](Part-8.md). |
|||
|
|||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
@ -0,0 +1,38 @@ |
|||
using System.Linq; |
|||
using System.Xml.Linq; |
|||
using Volo.Abp.Cli.Utils; |
|||
|
|||
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps; |
|||
|
|||
public class MakeProxyJsonFileEmbeddedStep : ProjectBuildPipelineStep |
|||
{ |
|||
public override void Execute(ProjectBuildContext context) |
|||
{ |
|||
foreach (var file in context.Files.Where(x => x.Name.EndsWith(".HttpApi.Client.csproj"))) |
|||
{ |
|||
using (var stream = StreamHelper.GenerateStreamFromString(file.Content)) |
|||
{ |
|||
var doc = XDocument.Load(stream); |
|||
|
|||
if (doc.Root == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var itemGroupNode = |
|||
new XElement("ItemGroup", |
|||
new XElement("EmbeddedResource", |
|||
new XAttribute("Include", @"**\*generate-proxy.json") |
|||
), |
|||
new XElement("Content", |
|||
new XAttribute("Remove", @"**\*generate-proxy.json") |
|||
) |
|||
); |
|||
|
|||
doc.Root.Add(itemGroupNode); |
|||
|
|||
file.SetContent(doc.ToString()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,10 +1,11 @@ |
|||
using System.Collections.Generic; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Http.Modeling; |
|||
|
|||
namespace Volo.Abp.Http.Client.ClientProxying; |
|||
|
|||
public interface IObjectToFormData<in TValue> |
|||
{ |
|||
Task<List<KeyValuePair<string, HttpContent>>> ConvertAsync(TValue value); |
|||
Task<List<KeyValuePair<string, HttpContent>>> ConvertAsync(ActionApiDescriptionModel actionApiDescription, ParameterApiDescriptionModel parameterApiDescription, TValue value); |
|||
} |
|||
|
|||
@ -0,0 +1,10 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Http.Modeling; |
|||
|
|||
namespace Volo.Abp.Http.Client.ClientProxying |
|||
{ |
|||
public interface IObjectToPath<in TValue> |
|||
{ |
|||
Task<string> ConvertAsync(ActionApiDescriptionModel actionApiDescription, ParameterApiDescriptionModel parameterApiDescription, TValue value); |
|||
} |
|||
} |
|||
@ -1,8 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Http.Modeling; |
|||
|
|||
namespace Volo.Abp.Http.Client.ClientProxying; |
|||
|
|||
public interface IObjectToQueryString<in TValue> |
|||
{ |
|||
Task<string> ConvertAsync(TValue value); |
|||
Task<string> ConvertAsync(ActionApiDescriptionModel actionApiDescription, ParameterApiDescriptionModel parameterApiDescription, TValue value); |
|||
} |
|||
|
|||
@ -0,0 +1,23 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http.Client.ClientProxying; |
|||
using Volo.Abp.Http.Modeling; |
|||
|
|||
namespace Volo.Abp.Http.DynamicProxying; |
|||
|
|||
public class TestObjectToPath : IObjectToPath<int>, ITransientDependency |
|||
{ |
|||
public Task<string> ConvertAsync(ActionApiDescriptionModel actionApiDescription, ParameterApiDescriptionModel parameterApiDescription, int value) |
|||
{ |
|||
if (actionApiDescription.Name == nameof(IRegularTestController.GetObjectandCountAsync)) |
|||
{ |
|||
if (value <= 0) |
|||
{ |
|||
value = 888; |
|||
} |
|||
return Task.FromResult(value.ToString()); |
|||
} |
|||
|
|||
return Task.FromResult<string>(null); |
|||
} |
|||
} |
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,11 +1,11 @@ |
|||
{ |
|||
"version": "5.0.0", |
|||
"version": "5.0.1", |
|||
"name": "@abp/aspnetcore.components.server.basictheme", |
|||
"publishConfig": { |
|||
"access": "public" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/aspnetcore.components.server.theming": "~5.0.0" |
|||
"@abp/aspnetcore.components.server.theming": "~5.0.1" |
|||
}, |
|||
"gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431" |
|||
} |
|||
|
|||
@ -1,12 +1,12 @@ |
|||
{ |
|||
"version": "5.0.0", |
|||
"version": "5.0.1", |
|||
"name": "@abp/aspnetcore.components.server.theming", |
|||
"publishConfig": { |
|||
"access": "public" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/bootstrap": "~5.0.0", |
|||
"@abp/font-awesome": "~5.0.0" |
|||
"@abp/bootstrap": "~5.0.1", |
|||
"@abp/font-awesome": "~5.0.1" |
|||
}, |
|||
"gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431" |
|||
} |
|||
|
|||
@ -1,14 +1,14 @@ |
|||
{ |
|||
"version": "5.0.0", |
|||
"version": "5.0.1", |
|||
"name": "@abp/blogging", |
|||
"publishConfig": { |
|||
"access": "public" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/aspnetcore.mvc.ui.theme.shared": "~5.0.0", |
|||
"@abp/owl.carousel": "~5.0.0", |
|||
"@abp/prismjs": "~5.0.0", |
|||
"@abp/tui-editor": "~5.0.0" |
|||
"@abp/aspnetcore.mvc.ui.theme.shared": "~5.0.1", |
|||
"@abp/owl.carousel": "~5.0.1", |
|||
"@abp/prismjs": "~5.0.1", |
|||
"@abp/tui-editor": "~5.0.1" |
|||
}, |
|||
"gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431" |
|||
} |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue