Abp vNext 学习第七弹 - 作者 参考上一篇 :Abp vNext 学习(6) .
简介 在前面的章节中, 我们使用 ABP 框架轻松地构建了一些服务;
使用 CrudAppService
基类, 而不是为标准的增删改查操作手工开发应用服务.
使用 generic repositories
自动完成数据层功能.
对于 “作者” 部分;
我们将要展示在需要的情况下, 如何 手工做一些事情 .
我们将要实现一些 领域驱动设计 (DDD) 最佳实践 .
开发将会逐层完成, 一次聚焦一层. 在真实项目中, 你会逐个功能(垂直)开发, 如同前面的教程. 通过这种方式, 你可以体验这两种方式
作者实体 在 .Domain
项目中创建 Authors
文件夹 (命名空间), 在其中加入 Author
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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 ( ) { } 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>
继承使得实体支持软删除 (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了 审计 属性.
Name
属性的 private set
限制从类的外部设置这个属性. 有两种方法设置名字 (两种都进行了验证):
当新建一个作者时, 通过构造函数.
使用 ChangeName
方法更新名字.
构造函数 和 ChangeName
方法的访问级别是 internal
, 强制这些方法只能在领域层由 AuthorManager
使用. 稍后将对此进行解释.
Check
类是一个ABP框架工具类, 用于检查方法参数 (如果参数非法会抛出 ArgumentException
).
AuthorConsts
是一个简单的类, 它位于 .Domain.Shared
项目的 Authors
命名空间 (文件夹)中:
1 2 3 4 5 6 7 namespace Acme.BookStore.Authors { public static class AuthorConsts { public const int MaxNameLength = 64 ; } }
在 .Domain.Shared
项目中创建这个类, 因为数据传输类 (DTOs) 稍后会再一次用到它.
AuthorManager: 领域服务 Author
构造函数和 ChangeName
方法的访问级别是 internal
, 所以它们只能在领域层使用. 在 .BookStore.Domain
项目中的 Authors
文件夹 (命名空间)创建 AuthorManager
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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
, 这个异常定义在 .Domain
项目 (Authors
文件夹中):
1 2 3 4 5 6 7 8 9 10 11 12 13 using Volo.Abp;namespace Acme.BookStore.Authors { public class AuthorAlreadyExistsException : BusinessException { public AuthorAlreadyExistsException (string name ) : base (BookStoreDomainErrorCodes.AuthorAlreadyExists ) { WithData("name" , name); } } }
BusinessException
是一个特殊的异常类型. 在需要时抛出领域相关异常是一个好的实践. ABP框架会自动处理它, 并且它也容易本地化. WithData(...)
方法提供额外的数据给异常对象, 这些数据将会在本地化中或出于其它一些目的被使用.
打开 .Domain.Shared
项目中的 BookStoreDomainErrorCodes
并修改为:
1 2 3 4 5 6 7 namespace Acme.BookStore { public static class BookStoreDomainErrorCodes { public const string AuthorAlreadyExists = "BookStore:00001" ; } }
这里定义了一个字符串, 表示应用程序抛出的错误码, 这个错误码可以被客户端应用程序处理. 为了用户, 你可能希望本地化它. 打开 .Domain.Shared
项目中的 Localization/BookStore/en.json
, 加入以下项:
1 "BookStore:00001" : "There is already an author with the same name: {name}"
当 AuthorAlreadyExistsException 被抛出, 终端用户将会在UI上看到组织好的错误消息.
IAuthorRepository AuthorManager
注入了 IAuthorRepository
, 所以我们需要定义它. 在 .Domain
项目的 Authors
文件夹 (命名空间) 中创建这个新接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
方法对于 IAuthorRepository
都是可用的.
FindByNameAsync
在 AuthorManager
中用来根据姓名查询用户.
GetListAsync
用于应用层以获得一个排序的, 经过过滤的作者列表, 显示在UI上.
DBContext 打开 Acme.BookStore.EntityFrameworkCore 项目中的 BookStoreDbContext 加入 DbSet 属性:
1 public DbSet<Author> Authors { get ; set ; }
定位到相同项目中的 BookStoreDbContext
类中的 OnModelCreating
方法, 加入以下代码到方法的结尾:
1 2 3 4 5 6 7 8 9 10 11 12 13 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
. 因为我们还没有修改数据库映射配置,所以需要创建一个新的迁移并对数据库应用变更.
打开命令行终端, 切换当前目录为 .EntityFrameworkCore
项目目录, 输入以下命令:
1 dotnet ef migrations add Added_Authors
你可以在同一个命令行终端中使用以下命令对数据库应用更改:
1 dotnet ef database update
实现 IAuthorRepository 在 .EntityFrameworkCore
项目 (Authors
文件夹)中创建一个新类 EfCoreAuthorRepository
, 粘贴以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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
NuGet 包来得到支持.
Razor 页面 虽然安全的 HTTP API和应用服务阻止未授权用户使用服务, 但他们依然可以导航到图书管理页面. 虽然当页面发起第一个访问服务器的AJAX请求时会收到授权异常, 但为了更好的用户体验和安全性, 我们应该对页面进行授权.
打开 BookStoreWebModule
在 ConfigureServices
方法中加入以下代码:
1 2 3 4 5 6 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); });
现在未授权用户会被重定向至登录页面 .
隐藏新建图书按钮 图书管理页面有一个 新建图书 按钮, 当用户没有 图书新建 权限时就不可见的.
打开 Pages/Books/Index.cshtml
文件, 替换内容为以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @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
隐藏操作项.
打开 .Web
项目中的 Pages/Books/Index.js
, 为 Edit
操作加入 visible
属性:
1 2 3 4 5 6 7 { text : l('Edit' ), visible : abp.auth.isGranted('BookStore.Books.Edit' ), action : function (data ) { editModal.open({ id : data.record.id }); } }
对 Delete
操作进行同样的操作:
1 visible: abp.auth.isGranted('BookStore.Books.Delete' ),
abp.auth.isGranted(...)
检查前面定义的权限.
visible
也可以是一个返回 bool
值的函数. 这个函数可以稍后根据某些条件计算.
菜单项 即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项.
打开 BookStoreMenuContributor
类, 找到下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 context.Menu.AddItem( new ApplicationMenuItem( "BooksStore" , l["Menu:BookStore" ], icon: "fa fa-book" ).AddItem( new ApplicationMenuItem( "BooksStore.Books" , l["Menu:Books" ], url: "/Books" ) ) );
替换为以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var bookStoreMenu = new ApplicationMenuItem( "BooksStore" , l["Menu:BookStore" ], icon: "fa fa-book" ); context.Menu.AddItem(bookStoreMenu); if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)){ bookStoreMenu.AddItem(new ApplicationMenuItem( "BooksStore.Books" , l["Menu:Books" ], url: "/Books" )); }
Demo Codes
github commit
下一章 参阅的Abp vNext 学习(7) 。