@ -1,3 +0,0 @@ |
|||
# CMS Kit Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# CMS Kit: Blogging |
|||
|
|||
TODO... |
|||
@ -0,0 +1,3 @@ |
|||
# CMS Kit: Comments |
|||
|
|||
TODO... |
|||
@ -0,0 +1,79 @@ |
|||
# 内容管理系统套件模块 |
|||
|
|||
此模块为您的应用程序提供内容管理系统 (Content Management System, CMS) 功能. 它提供 **核心构建块** 和完整工作的 **子系统**, 以创建启用 CMS 功能的您自己的网站, 或出于任何目的使用网站中的构建块. |
|||
|
|||
> **此模块目前仅适用于 MVC / Razor 页面 UI**. 虽然没有官方的 Blazor 软件包, 但它也可以在 Blazor 服务器 UI 中工作, 因为实际上 Blazor 服务器 UI 实际上是一个运行在 ASP.NET Core MVC / Razor 页面应用程序的混合型应用程序. |
|||
|
|||
目前提供以下功能: |
|||
|
|||
* 提供 [**页面**](Pages.md) 管理系统来管理具有动态 URL 的动态页面. |
|||
* 提供 [**博客**](Blogging.md) 系统来创建发表具有多种博客支持的博客文章. |
|||
* 提供 [**标签**](Tags.md) 系统来标记任何资源, 如博客文章. |
|||
* 提供 [**评论**](Comments.md) 系统来添加对任何资源的评论功能, 如博客文章或产品评价页面. |
|||
* 提供 [**反应**](Reactions.md) 系统来添加对任何资源的反应 (表情符号) 功能, 如博客文章或评论. |
|||
* 提供 [**评级**](Ratings.md) 系统来添加对任何资源的评级功能. |
|||
* 提供 [**菜单**](Menus.md) 系统来动态管理公共菜单. |
|||
|
|||
点击功能以了解和学习如何去使用它. |
|||
|
|||
所有功能均可单独使用. 如果你禁用了一个功能, 则在 [全局功能](../../Global-Features.md) 系统的帮助下, 该功能会从你的应用程序甚至数据库表中完全消失. |
|||
|
|||
## 预备要求 |
|||
- 此模块依赖于 [Blob 存储](../../Blob-Storing.md) 模块来保存媒体内容. |
|||
> 确保 `BlobStoring` 模块已安装并至少正确地配置了一个提供程序. 请查阅 [文档](../../Blob-Storing.md) 了解更多信息. |
|||
|
|||
- CMS Kit 使用 [分布式缓存](../../Caching.md) 来提高响应速度. |
|||
> 强烈建议在分布式/集群部署中为实现数据一致性使用分布式缓存, 如 [Redis](../../Redis-Cache.md). |
|||
|
|||
## 如何安装 |
|||
|
|||
可以使用 [ABP CLI](../../CLI.md) 的 `add-module` 命令为解决方案安装模块. 您可以使用以下命令在命令行中安装 CMS Kit 模块: |
|||
|
|||
```bash |
|||
abp add-module Volo.CmsKit |
|||
``` |
|||
|
|||
> 默认情况下, Cms-Kit `GlobalFeature` 被禁用. 因此初始迁移将为空. 所以, 当你使用 EF Core 安装时,你可以添加 `--skip-db-migrations` 命令来跳过迁移. 启用 Cms-Kit 全局功能后, 请添加新的迁移. |
|||
|
|||
安装过程完成后, 在您的解决方案 `Domain.Shared` 项目中打开 `GlobalFeatureConfigurator` 类, 并将以下代码写入 `Configure` 方法中, 以启用 CMS Kit 模块的全部功能. |
|||
|
|||
```csharp |
|||
GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => |
|||
{ |
|||
cmsKit.EnableAll(); |
|||
}); |
|||
``` |
|||
|
|||
你可能更愿意逐个启用这些功能, 而不是启用全部功能. 以下示例仅启用了 [标签](Tags.md) 和 [评论](Comments.md) 功能: |
|||
|
|||
````csharp |
|||
GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => |
|||
{ |
|||
cmsKit.Tags.Enable(); |
|||
cmsKit.Comments.Enable(); |
|||
}); |
|||
```` |
|||
|
|||
> 如果你使用 EF Core, 不要忘记添加一个新的迁移并更新你的数据库. |
|||
|
|||
## 软件包 |
|||
|
|||
此模块遵循 [模块开发最佳实践指南](https://docs.abp.io/zh-Hans/abp/latest/Best-Practices/Index), 由多个 NuGet 和 NPM 软件包组成. 如果你想了解软件包及其之间的关系, 请参阅指南. |
|||
|
|||
CMS Kit 软件包专为各种使用场景而设计. 如果您查阅了 [CMS Kit 软件包](https://www.nuget.org/packages?q=Volo.CmsKit) 您将看到一些有 `Admin` 和 `Public` 后缀的软件包. 该模块有两个应用程序层, 原因是他们可能被用于不同类型的应用程序. 这些应用程序层仅使用一个领域层. |
|||
|
|||
- `Volo.CmsKit.Admin.*` 软件包包括管理员 (后台) 应用程序所必须的功能. |
|||
- `Volo.CmsKit.Public.*` 软件包包括被用于用户阅读博客文章和发表评论的公共网站上的功能. |
|||
- `Volo.CmsKit.*` (不带 Admin/Public 后缀) 软件包称为统一包. 统一包分别是添加 Admin 和 Public (相关层的) 软件包的快照. 如果您有一个用于管理和公共网站的单应用程序, 您可以使用这些软件包. |
|||
|
|||
## 内部结构 |
|||
|
|||
### 表/集合 前缀&架构 |
|||
|
|||
所有表/集合使用 `Cms` 作为默认前缀. 如果需要更改表的前缀或者设置一个架构名称 (如果你的数据库提供程序支持), 请在 `CmsKitDbProperties` 类中设置静态属性. |
|||
|
|||
### 连接字符串 |
|||
|
|||
此模块使用 `CmsKit` 作为连接字符串的名称. 如果您未使用此名称定义连接字符串, 它将回退为 `Default` 连接字符串. |
|||
|
|||
有关详细信息, 请参阅 [连接字符串](https://docs.abp.io/en/abp/latest/Connection-Strings) 文档. |
|||
@ -0,0 +1,3 @@ |
|||
# CMS Kit: Pages |
|||
|
|||
TODO... |
|||
@ -0,0 +1,3 @@ |
|||
# CMS Kit: Pages |
|||
|
|||
TODO... |
|||
@ -0,0 +1,3 @@ |
|||
# Rating System |
|||
|
|||
TODO... |
|||
@ -0,0 +1,3 @@ |
|||
# Reaction System |
|||
|
|||
TODO... |
|||
@ -0,0 +1,3 @@ |
|||
# Tag Management |
|||
|
|||
TODO... |
|||
@ -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` 中). |
|||
|
|||
@ -0,0 +1,107 @@ |
|||
# 租户管理模块 |
|||
|
|||
[多租户](../Multi-Tenancy.md) 是 ABP 框架的核心功能之一. 它提供了构建 SaaS (软件及服务) 解决方案的基础架构. ABP 的多租户系统通过提供 `ITenantStore` 接口来抽象租户存储的位置. 您需要做的就是实现该接口. |
|||
|
|||
**租户管理模块是 `ITenantStore` 接口的实现. 它将租户存储在数据库中. 它还提供了 UI 来管理租户及其 [功能](../Features.md).** |
|||
|
|||
> 请参阅 [多租户](../Multi-Tenancy.md) 文档,以了解 ABP 框架的多租户系统. 本文重点介绍租户管理模块. |
|||
|
|||
### 关于商业版 SaaS 模块 |
|||
|
|||
[SaaS 模块](https://commercial.abp.io/modules/Volo.Saas) 是此模块的替代实现, 具有更多的功能和可能性. 它作为 [ABP 商业版](https://commercial.abp.io/) 订阅的一部分分发. |
|||
|
|||
## 如何安装 |
|||
|
|||
当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块. |
|||
|
|||
### 源代码 |
|||
|
|||
可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/identity) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它. |
|||
|
|||
## 用户界面 |
|||
|
|||
此模块将 "管理 -> 租户管理 -> 租户" 菜单项添加到应用程序的主菜单, 这将打开如下页面: |
|||
|
|||
 |
|||
|
|||
在这个页面中, 你可以查看所有租户. 你可以按照如下所示创建一个新的租户: |
|||
|
|||
 |
|||
|
|||
在这个模态框中; |
|||
|
|||
* **租户名称**: 租户唯一的名称. 如果为租户使用二级域名, 这将是二级域名名称. |
|||
* **管理员电子邮件地址**: 租户管理员的电子邮件地址. |
|||
* **管理员密码**: 租户管理员的密码. |
|||
|
|||
当您点击租户附近的 *操作* 按钮时, 您将看见可以执行的操作: |
|||
|
|||
 |
|||
|
|||
### 管理租户功能 |
|||
|
|||
功能操作会打开一个模态框, 来对相关租户进行启用/禁用/设置 [功能](../Features.md). 这有一个模态框的例子: |
|||
|
|||
 |
|||
|
|||
### 管理主机功能 |
|||
|
|||
如果您使用应用程序的功能也在主机端, *管理主机功能* 按钮则用于设置主机端的功能. |
|||
|
|||
## 内部结构 |
|||
|
|||
如果要 [自定义](../Customizing-Application-Modules-Guide.md) 此模块而不更改 [其源代码](https://github.com/abpframework/abp/tree/dev/modules/tenant-management), 则可以将此节作为参考. |
|||
|
|||
### 领域层 |
|||
|
|||
#### 聚合 |
|||
|
|||
* `Tenant` |
|||
|
|||
#### 仓储 |
|||
|
|||
* `ITenantRepository` |
|||
|
|||
#### 领域服务 |
|||
|
|||
* `TenantManager` |
|||
|
|||
### 应用层 |
|||
|
|||
#### 应用服务 |
|||
|
|||
* `TenantAppService` |
|||
|
|||
#### 权限 |
|||
|
|||
- `AbpTenantManagement.Tenants`: 租户管理. |
|||
- `AbpTenantManagement.Tenants.Create`: 创建一个新的租户. |
|||
- `AbpTenantManagement.Tenants.Update`: 编辑一个已存在的租户. |
|||
- `AbpTenantManagement.Tenants.Delete`: 删除一个已存在的租户. |
|||
- `AbpTenantManagement.Tenants.ManageFeatures`: 管理租户的功能. |
|||
|
|||
### EF Core 集成 |
|||
|
|||
* `TenantManagementDbContext` (实现了 `ITenantManagementDbContext`) |
|||
|
|||
**数据库表:** |
|||
|
|||
* `AbpTenants` |
|||
* `AbpTenantConnectionStrings` |
|||
|
|||
### MongoDB 集成 |
|||
|
|||
* `TenantManagementMongoDbContext` (实现了 `ITenantManagementMongoDbContext`) |
|||
|
|||
**数据库集合:** |
|||
|
|||
* `AbpTenants` (也包括连接字符串) |
|||
|
|||
## 注意事项 |
|||
|
|||
ABP 框架允许租户有专用的数据库, 以允许 *每个租户用一个数据库*. 此模块具有使该实现成为可能 (查阅其源代码) 的基础架构, 但是它没有实现应用程序层和 UI 功能以使其开箱即用. 您可以自己实现这些功能, 也可以考虑使用已完全实现这些功能并提供了更多业务功能的 [ABP 商业版 Saas 模块](https://docs.abp.io/en/commercial/latest/modules/saas). |
|||
|
|||
## 另请参见 |
|||
|
|||
* [多租户](../Multi-Tenancy.md) |
|||
* [ABP 商业版 SaaS 模块](https://docs.abp.io/en/commercial/latest/modules/saas) |
|||
@ -1,3 +0,0 @@ |
|||
# 租户管理模块 |
|||
|
|||
TODO |
|||
@ -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). |
|||
|
|||
@ -1 +1,574 @@ |
|||
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-7.md) |
|||
- [Part 8: 作者: 应用服务层](本章) |
|||
- [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` |
|||
|
|||
## 简介 |
|||
|
|||
这章阐述如何为前一章介绍的 `作者` 实体创建应用服务层. |
|||
|
|||
## IAuthorAppService |
|||
|
|||
我们首先创建 [应用服务](../Application-Services.md) 接口和相关的 [DTO](../Data-Transfer-Objects.md)s. 在 `Acme.BookStore.Application.Contracts` 项目的 `Authors` 命名空间 (文件夹) 创建一个新接口 `IAuthorAppService`: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public interface IAuthorAppService : IApplicationService |
|||
{ |
|||
Task<AuthorDto> GetAsync(Guid id); |
|||
|
|||
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input); |
|||
|
|||
Task<AuthorDto> CreateAsync(CreateAuthorDto input); |
|||
|
|||
Task UpdateAsync(Guid id, UpdateAuthorDto input); |
|||
|
|||
Task DeleteAsync(Guid id); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `IApplicationService` 是一个常规接口, 所有应用服务都继承自它, 所以 ABP 框架可以识别它们. |
|||
* 在 `Author` 实体中定义标准方法用于CRUD操作. |
|||
* `PagedResultDto` 是一个ABP框架中预定义的 DTO 类. 它拥有一个 `Items` 集合 和一个 `TotalCount` 属性, 用于返回分页结果. |
|||
* 优先从 `CreateAsync` 方法返回 `AuthorDto` (新创建的作者), 虽然在这个程序中没有这么做 - 这里只是展示一种不同用法. |
|||
|
|||
这个类使用下面定义的DTOs (为你的项目创建它们). |
|||
|
|||
### AuthorDto |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class AuthorDto : EntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public DateTime BirthDate { get; set; } |
|||
|
|||
public string ShortBio { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `EntityDto<T>` 只有一个类型为指定泛型参数的 `Id` 属性. 你可以自己创建 `Id` 属性, 而不是继承自 `EntityDto<T>`. |
|||
|
|||
### GetAuthorListDto |
|||
|
|||
````csharp |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class GetAuthorListDto : PagedAndSortedResultRequestDto |
|||
{ |
|||
public string Filter { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `Filter` 用于搜索作者. 它可以是 `null` (或空字符串) 以获得所有用户. |
|||
* `PagedAndSortedResultRequestDto` 具有标准分页和排序属性: `int MaxResultCount`, `int SkipCount` 和 `string Sorting`. |
|||
|
|||
> ABP 框架拥有这些基本的DTO类以简化并标准化你的DTOs. 参阅 [DTO 文档](../Data-Transfer-Objects.md) 获得所有DTO类的详细信息. |
|||
|
|||
### CreateAuthorDto |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class CreateAuthorDto |
|||
{ |
|||
[Required] |
|||
[StringLength(AuthorConsts.MaxNameLength)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
public DateTime BirthDate { get; set; } |
|||
|
|||
public string ShortBio { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
数据标记特性可以用来验证DTO. 参阅 [验证文档](../Validation.md) 获得详细信息. |
|||
|
|||
### UpdateAuthorDto |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public class UpdateAuthorDto |
|||
{ |
|||
[Required] |
|||
[StringLength(AuthorConsts.MaxNameLength)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
public DateTime BirthDate { get; set; } |
|||
|
|||
public string ShortBio { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> 我们可以在创建和更新操作间分享 (重用) 相同的DTO. 虽然可以这么做, 但我们推荐为这些操作创建不同的DTOs, 因为我们发现随着时间的推移, 它们通常会变得有差异. 所以, 与紧耦合相比, 代码重复也是合理的. |
|||
|
|||
## AuthorAppService |
|||
|
|||
是时候实现 `IAuthorAppService` 接口了. 在 `Acme.BookStore.Application` 项目的 `Authors` 命名空间 (文件夹) 中创建一个新类 `AuthorAppService` : |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Acme.BookStore.Permissions; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
[Authorize(BookStorePermissions.Authors.Default)] |
|||
public class AuthorAppService : BookStoreAppService, IAuthorAppService |
|||
{ |
|||
private readonly IAuthorRepository _authorRepository; |
|||
private readonly AuthorManager _authorManager; |
|||
|
|||
public AuthorAppService( |
|||
IAuthorRepository authorRepository, |
|||
AuthorManager authorManager) |
|||
{ |
|||
_authorRepository = authorRepository; |
|||
_authorManager = authorManager; |
|||
} |
|||
|
|||
//...SERVICE METHODS WILL COME HERE... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `[Authorize(BookStorePermissions.Authors.Default)]` 是一个检查权限(策略)的声明式方法, 用来给当前用户授权. 参阅 [授权文档](../Authorization.md) 获得详细信息. `BookStorePermissions` 类在后文会被更新, 现在不需要担心编译错误. |
|||
* 由 `BookStoreAppService` 派生, 这个类是一个简单基类, 可以做为模板. 它继承自标准的 `ApplicationService` 类. |
|||
* 实现上面定义的 `IAuthorAppService` . |
|||
* 注入 `IAuthorRepository` 和 `AuthorManager` 以使用服务方法. |
|||
|
|||
现在, 我们逐个介绍服务方法. 复制这些方法到 `AuthorAppService` 类. |
|||
|
|||
### GetAsync |
|||
|
|||
````csharp |
|||
public async Task<AuthorDto> GetAsync(Guid id) |
|||
{ |
|||
var author = await _authorRepository.GetAsync(id); |
|||
return ObjectMapper.Map<Author, AuthorDto>(author); |
|||
} |
|||
```` |
|||
|
|||
这个方法根据 `Id` 获得 `Author` 实体, 使用 [对象到对象映射](../Object-To-Object-Mapping.md) 转换为 `AuthorDto`. 这需要配置AutoMapper, 后面会介绍. |
|||
|
|||
### GetListAsync |
|||
|
|||
````csharp |
|||
public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input) |
|||
{ |
|||
if (input.Sorting.IsNullOrWhiteSpace()) |
|||
{ |
|||
input.Sorting = nameof(Author.Name); |
|||
} |
|||
|
|||
var authors = await _authorRepository.GetListAsync( |
|||
input.SkipCount, |
|||
input.MaxResultCount, |
|||
input.Sorting, |
|||
input.Filter |
|||
); |
|||
|
|||
var totalCount = input.Filter == null |
|||
? await _authorRepository.CountAsync() |
|||
: await _authorRepository.CountAsync( |
|||
author => author.Name.Contains(input.Filter)); |
|||
|
|||
return new PagedResultDto<AuthorDto>( |
|||
totalCount, |
|||
ObjectMapper.Map<List<Author>, List<AuthorDto>>(authors) |
|||
); |
|||
} |
|||
```` |
|||
|
|||
* 为处理客户端没有设置的情况, 在方法的开头设置默认排序是 "根据作者名". |
|||
* 使用 `IAuthorRepository.GetListAsync` 从数据库中获得分页的, 排序的和过滤的作者列表. 我们已经在教程的前一章中实现了它. 再一次强调, 实际上不需要创建这个方法, 因为我们可以从数据库中直接查询, 这里只是演示如何创建自定义repository方法. |
|||
* 直接查询 `AuthorRepository` , 得到作者的数量. 如果客户端发送了过滤条件, 会得到过滤后的作者数量. |
|||
* 最后, 通过映射 `Author` 列表到 `AuthorDto` 列表, 返回分页后的结果. |
|||
|
|||
### CreateAsync |
|||
|
|||
````csharp |
|||
[Authorize(BookStorePermissions.Authors.Create)] |
|||
public async Task<AuthorDto> CreateAsync(CreateAuthorDto input) |
|||
{ |
|||
var author = await _authorManager.CreateAsync( |
|||
input.Name, |
|||
input.BirthDate, |
|||
input.ShortBio |
|||
); |
|||
|
|||
await _authorRepository.InsertAsync(author); |
|||
|
|||
return ObjectMapper.Map<Author, AuthorDto>(author); |
|||
} |
|||
```` |
|||
|
|||
* `CreateAsync` 需要 `BookStorePermissions.Authors.Create` 权限 (另外包括 `AuthorAppService` 类声明的 `BookStorePermissions.Authors.Default` 权限). |
|||
* 使用 `AuthorManager` (领域服务) 创建新作者. |
|||
* 使用 `IAuthorRepository.InsertAsync` 插入新作者到数据库. |
|||
* 使用 `ObjectMapper` 返回 `AuthorDto` , 代表新创建的作者. |
|||
|
|||
> **DDD提示**: 一些开发者可能会发现可以在 `_authorManager.CreateAsync` 插入新实体. 我们认为把它留给应用层是更好的设计, 因为应用层更了解应该何时插入实体到数据库(在插入实体前可能需要额外的工作. 如果在领域层插入, 可能需要额外的更新操作). 但是, 你拥有最终的决定权. |
|||
|
|||
### UpdateAsync |
|||
|
|||
````csharp |
|||
[Authorize(BookStorePermissions.Authors.Edit)] |
|||
public async Task UpdateAsync(Guid id, UpdateAuthorDto input) |
|||
{ |
|||
var author = await _authorRepository.GetAsync(id); |
|||
|
|||
if (author.Name != input.Name) |
|||
{ |
|||
await _authorManager.ChangeNameAsync(author, input.Name); |
|||
} |
|||
|
|||
author.BirthDate = input.BirthDate; |
|||
author.ShortBio = input.ShortBio; |
|||
|
|||
await _authorRepository.UpdateAsync(author); |
|||
} |
|||
```` |
|||
|
|||
* `UpdateAsync` 需要额外的 `BookStorePermissions.Authors.Edit` 权限. |
|||
* 使用 `IAuthorRepository.GetAsync` 从数据库中获得作者实体. 如果给定的id没有找到作者, `GetAsync` 抛出 `EntityNotFoundException`, 这在web应用程序中导致一个 `404` HTTP 状态码. 在更新操作中先获取实体再更新它, 是一个好的实践. |
|||
* 如果客户端请求, 使用 `AuthorManager.ChangeNameAsync` (领域服务方法) 修改作者姓名. |
|||
* 因为没有任何业务逻辑, 直接更新 `BirthDate` 和 `ShortBio`, 它们可以接受任何值. |
|||
* 最后, 调用 `IAuthorRepository.UpdateAsync` 更新实体到数据库. |
|||
|
|||
{{if DB == "EF"}} |
|||
|
|||
> **EF Core 提示**: Entity Framework Core 拥有 **change tracking** 系统并在unit of work 结束时 **自动保存** 任何修改到实体 (你可以简单地认为APB框架在方法结束时自动调用 `SaveChanges`). 所以, 即使你在方法结束时没有调用 `_authorRepository.UpdateAsync(...)` , 它依然可以工作. 如果你不考虑以后修改EF Core, 你可以移除这一行. |
|||
|
|||
{{end}} |
|||
|
|||
### DeleteAsync |
|||
|
|||
````csharp |
|||
[Authorize(BookStorePermissions.Authors.Delete)] |
|||
public async Task DeleteAsync(Guid id) |
|||
{ |
|||
await _authorRepository.DeleteAsync(id); |
|||
} |
|||
```` |
|||
|
|||
* `DeleteAsync` 需要额外的 `BookStorePermissions.Authors.Delete` 权限. |
|||
* 直接使用repository的 `DeleteAsync` 方法. |
|||
|
|||
## 权限定义 |
|||
|
|||
你还不能编译代码, 因为它需要 `BookStorePermissions` 类定义中一些常数. |
|||
|
|||
打开 `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"; |
|||
} |
|||
|
|||
// *** ADDED a NEW NESTED CLASS *** |
|||
public static class Authors |
|||
{ |
|||
public const string Default = GroupName + ".Authors"; |
|||
public const string Create = Default + ".Create"; |
|||
public const string Edit = Default + ".Edit"; |
|||
public const string Delete = Default + ".Delete"; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
然后打开同一项目中的 `BookStorePermissionDefinitionProvider`, 在 `Define` 方法的结尾加入以下行: |
|||
|
|||
````csharp |
|||
var authorsPermission = bookStoreGroup.AddPermission( |
|||
BookStorePermissions.Authors.Default, L("Permission:Authors")); |
|||
|
|||
authorsPermission.AddChild( |
|||
BookStorePermissions.Authors.Create, L("Permission:Authors.Create")); |
|||
|
|||
authorsPermission.AddChild( |
|||
BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit")); |
|||
|
|||
authorsPermission.AddChild( |
|||
BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete")); |
|||
```` |
|||
|
|||
最后, 在 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` 加入以下项, 用以本地化权限名称: |
|||
|
|||
````csharp |
|||
"Permission:Authors": "Author Management", |
|||
"Permission:Authors.Create": "Creating new authors", |
|||
"Permission:Authors.Edit": "Editing the authors", |
|||
"Permission:Authors.Delete": "Deleting the authors" |
|||
```` |
|||
|
|||
## 对象到对象映射 |
|||
|
|||
`AuthorAppService` 使用 `ObjectMapper` 将 `Author` 对象 转换为 `AuthorDto` 对象. 所以, 我们需要在 AutoMapper 配置中定义映射. |
|||
|
|||
打开 `Acme.BookStore.Application` 项目中的 `BookStoreApplicationAutoMapperProfile` 类, 加入以下行到构造函数: |
|||
|
|||
````csharp |
|||
CreateMap<Author, AuthorDto>(); |
|||
```` |
|||
|
|||
## 种子数据 |
|||
|
|||
如同图书管理部分所做的, 在数据库中生成一些初始作者实体. 不仅当第一次运行应用程序时是有用的, 对自动化测试也是很有用的. |
|||
|
|||
打开 `Acme.BookStore.Domain` 项目中的 `BookStoreDataSeederContributor`, 修改文件内容如下: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Acme.BookStore.Authors; |
|||
using Acme.BookStore.Books; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreDataSeederContributor |
|||
: IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
private readonly IAuthorRepository _authorRepository; |
|||
private readonly AuthorManager _authorManager; |
|||
|
|||
public BookStoreDataSeederContributor( |
|||
IRepository<Book, Guid> bookRepository, |
|||
IAuthorRepository authorRepository, |
|||
AuthorManager authorManager) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
_authorRepository = authorRepository; |
|||
_authorManager = authorManager; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
if (await _bookRepository.GetCountAsync() <= 0) |
|||
{ |
|||
await _bookRepository.InsertAsync( |
|||
new Book |
|||
{ |
|||
Name = "1984", |
|||
Type = BookType.Dystopia, |
|||
PublishDate = new DateTime(1949, 6, 8), |
|||
Price = 19.84f |
|||
}, |
|||
autoSave: true |
|||
); |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book |
|||
{ |
|||
Name = "The Hitchhiker's Guide to the Galaxy", |
|||
Type = BookType.ScienceFiction, |
|||
PublishDate = new DateTime(1995, 9, 27), |
|||
Price = 42.0f |
|||
}, |
|||
autoSave: true |
|||
); |
|||
} |
|||
|
|||
// ADDED SEED DATA FOR AUTHORS |
|||
|
|||
if (await _authorRepository.GetCountAsync() <= 0) |
|||
{ |
|||
await _authorRepository.InsertAsync( |
|||
await _authorManager.CreateAsync( |
|||
"George Orwell", |
|||
new DateTime(1903, 06, 25), |
|||
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." |
|||
) |
|||
); |
|||
|
|||
await _authorRepository.InsertAsync( |
|||
await _authorManager.CreateAsync( |
|||
"Douglas Adams", |
|||
new DateTime(1952, 03, 11), |
|||
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
{{if DB=="EF"}} |
|||
|
|||
你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据. |
|||
|
|||
{{else if DB=="Mongo"}} |
|||
|
|||
你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据. |
|||
|
|||
{{end}} |
|||
|
|||
## 测试作者应用服务 |
|||
|
|||
最后, 你可以为 `IAuthorAppService` 写一些测试. 在 `Acme.BookStore.Application.Tests` 项目的 `Authors` 命名空间(文件夹)中加入一个名为 `AuthorAppService_Tests` 新类: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ {{if DB=="Mongo"}} |
|||
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} |
|||
public class AuthorAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IAuthorAppService _authorAppService; |
|||
|
|||
public AuthorAppService_Tests() |
|||
{ |
|||
_authorAppService = GetRequiredService<IAuthorAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_All_Authors_Without_Any_Filter() |
|||
{ |
|||
var result = await _authorAppService.GetListAsync(new GetAuthorListDto()); |
|||
|
|||
result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); |
|||
result.Items.ShouldContain(author => author.Name == "George Orwell"); |
|||
result.Items.ShouldContain(author => author.Name == "Douglas Adams"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_Filtered_Authors() |
|||
{ |
|||
var result = await _authorAppService.GetListAsync( |
|||
new GetAuthorListDto {Filter = "George"}); |
|||
|
|||
result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); |
|||
result.Items.ShouldContain(author => author.Name == "George Orwell"); |
|||
result.Items.ShouldNotContain(author => author.Name == "Douglas Adams"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Create_A_New_Author() |
|||
{ |
|||
var authorDto = await _authorAppService.CreateAsync( |
|||
new CreateAuthorDto |
|||
{ |
|||
Name = "Edward Bellamy", |
|||
BirthDate = new DateTime(1850, 05, 22), |
|||
ShortBio = "Edward Bellamy was an American author..." |
|||
} |
|||
); |
|||
|
|||
authorDto.Id.ShouldNotBe(Guid.Empty); |
|||
authorDto.Name.ShouldBe("Edward Bellamy"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Allow_To_Create_Duplicate_Author() |
|||
{ |
|||
await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () => |
|||
{ |
|||
await _authorAppService.CreateAsync( |
|||
new CreateAuthorDto |
|||
{ |
|||
Name = "Douglas Adams", |
|||
BirthDate = DateTime.Now, |
|||
ShortBio = "..." |
|||
} |
|||
); |
|||
}); |
|||
} |
|||
|
|||
//TODO: Test other methods... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
完成应用服务方法的测试, 它们应该很容易理解. |
|||
|
|||
## 下一章 |
|||
|
|||
查看本教程的[下一章](Part-9.md). |
|||
|
|||
|
After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 19 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 |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,83 @@ |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
using Blazorise; |
|||
using Blazorise.Utilities; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.Extensions.Localization; |
|||
using Volo.Abp.AspNetCore.Components.Web; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.ObjectExtending; |
|||
|
|||
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending; |
|||
|
|||
public abstract class ExtensionPropertyComponentBase<TEntity, TResourceType> : OwningComponentBase |
|||
where TEntity : IHasExtraProperties |
|||
{ |
|||
[Inject] |
|||
public IStringLocalizerFactory StringLocalizerFactory { get; set; } |
|||
|
|||
[Inject] |
|||
public IValidationMessageLocalizerAttributeFinder ValidationMessageLocalizerAttributeFinder { get; set; } |
|||
|
|||
[Parameter] |
|||
public TEntity Entity { get; set; } |
|||
|
|||
[Parameter] |
|||
public ObjectExtensionPropertyInfo PropertyInfo { get; set; } |
|||
|
|||
[Parameter] |
|||
public AbpBlazorMessageLocalizerHelper<TResourceType> LH { get; set; } |
|||
|
|||
protected virtual void Validate(ValidatorEventArgs e) |
|||
{ |
|||
e.Status = ValidationStatus.Success; |
|||
|
|||
var validationAttributes = PropertyInfo.GetValidationAttributes(); |
|||
var validationContext = new ValidationContext(Entity) |
|||
{ |
|||
DisplayName = PropertyInfo.Name, |
|||
MemberName = PropertyInfo.Name |
|||
}; |
|||
|
|||
foreach (var validationAttribute in validationAttributes) |
|||
{ |
|||
var result = validationAttribute.GetValidationResult(e.Value, validationContext); |
|||
if (result == ValidationResult.Success || result == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var errorMessage = result.ErrorMessage; |
|||
if (LH != null) |
|||
{ |
|||
var formattedErrorMessage = GetDefaultErrorMessage(validationAttribute); |
|||
var errorMessageString = ValidationAttributeHelper.RevertErrorMessagePlaceholders(formattedErrorMessage); |
|||
var errorMessageArguments = ValidationMessageLocalizerAttributeFinder.FindAll(errorMessage, errorMessageString) |
|||
?.OrderBy(x => x.Index) |
|||
?.Select(x => x.Argument); |
|||
|
|||
errorMessage = LH.Localize(errorMessageString, errorMessageArguments); |
|||
} |
|||
|
|||
e.MemberNames = result.MemberNames; |
|||
e.Status = ValidationStatus.Error; |
|||
e.ErrorText = errorMessage; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private static string GetDefaultErrorMessage(ValidationAttribute validationAttribute) |
|||
{ |
|||
if (validationAttribute is StringLengthAttribute stringLengthAttribute && stringLengthAttribute.MinimumLength != 0) |
|||
{ |
|||
var nullable = ValidationAttributeHelper.ValidationAttributeCustomErrorMessageSetProperty.GetValue((object) validationAttribute) as bool?; |
|||
var flag = true; |
|||
if (!(nullable.GetValueOrDefault() == flag & nullable.HasValue)) |
|||
{ |
|||
return ValidationAttributeHelper.SetErrorMessagePlaceholders("The field {0} must be a string with a minimum length of {2} and a maximum length of {1}."); |
|||
} |
|||
} |
|||
return ValidationAttributeHelper.SetErrorMessagePlaceholders(ValidationAttributeHelper.ValidationAttributeErrorMessageStringProperty.GetValue((object) validationAttribute) as string); |
|||
} |
|||
} |
|||
@ -1,13 +1,21 @@ |
|||
@typeparam TEntity |
|||
@typeparam TResourceType |
|||
@using Abp.Localization |
|||
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType> |
|||
|
|||
<Field> |
|||
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel> |
|||
<Select @bind-SelectedValue="@SelectedValue" > |
|||
@foreach (var item in SelectItems) |
|||
{ |
|||
<SelectItem Value="@item.Value">@item.Text</SelectItem> |
|||
} |
|||
</Select> |
|||
</Field> |
|||
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize"> |
|||
<Field> |
|||
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel> |
|||
<Select @bind-SelectedValue="@SelectedValue"> |
|||
<ChildContent> |
|||
@foreach (var item in SelectItems) |
|||
{ |
|||
<SelectItem Value="@item.Value">@item.Text</SelectItem> |
|||
} |
|||
</ChildContent> |
|||
<Feedback> |
|||
<ValidationError/> |
|||
</Feedback> |
|||
</Select> |
|||
</Field> |
|||
</Validation> |
|||
|
|||