ASP.NET Core与RESTful API 开发实战(三)
资源操作
大体操作流程:
创建仓储接口
创建基于接口的具体仓储实现
添加服务,Startup类的ConfigureServices方法中将它们添加到依赖注入容器中
创建Controller,构造函数注入得到仓储接口,这里使用构造函数注入。添加对应的CRUD
创建项目
创建一个ASP.NET Core WebApi项目,1.添加Models文件夹,创建AuthorDto和BookDto类。
public class AuthorDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
public class BookDto
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int Pages { get; set; }
public Guid AuthorId { get; set; }
}
使用内存数据
创建内存数据源
创建Data文件夹,添加LibraryMockData类,该类包括一个构造函数、两个集合属性及一个静态属性、其中两个集合属性分别代表AuthorDto和BookDto集合,而静态属性Current则返回一个LibraryMockData实例,方便访问该对象。
public class LibraryMockData
{
//获取LibraryMockData实例
public static LibraryMockData Current { get; } = new LibraryMockData();
public List<AuthorDto> Authors { get; set; }
public List<BookDto> Books { get; set; }
public LibraryMockData()
{
Authors = new List<AuthorDto>
{
new AuthorDto
{
Id = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12"),
Age = 46,
Email = "123456@qq.com",
Name = "黄泽涛"
},
new AuthorDto
{
Id = new Guid("e0a953c3ee6040eaa9fae2b667060e09"),
Name = "李策润",
Age = 16,
Email = "1234kl@163.com"
}
};
Books = new List<BookDto>
{
new BookDto
{
Id = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12"),
Title ="王蒙讲孔孟老庄",
Description="这是王蒙先生讲解论语道德经等国学知识的书籍",
Pages=12,
AuthorId=new("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12")
},
new BookDto
{
Id = new Guid("734fd453-a4f8-4c5d-9c98-3fe2d7079760"),
Title ="终身成长",
Description="终身学习成长的一本书籍",
Pages = 244,
AuthorId = new Guid("e0a953c3ee6040eaa9fae2b667060e09")
},
new BookDto
{
Id = new Guid("ade24d16-db0f-40af-8794-1e08e2040df3"),
Title ="弟子规",
Description ="一本国学古书",
Pages = 32,
AuthorId = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12")
},
new BookDto
{
Id =new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af11"),
Title ="山海经",
Description = "古人的奇物志",
Pages = 90,
AuthorId = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12")
},
};
}
}
仓储模式
仓储模式作为领域驱动设计(DDD)的一部分,在系统设计中的使用非常广泛。它主要用于解除业务逻辑与数据访问层之间的耦合,使业务逻辑在储存、访问数据库时无须关心数据的来源及储存方式。
实现仓储模式的方法有多种:
- 其中一种简单的方法是对每一个与数据库交互的业务对象创建一个仓储接口及其实现。这样做的好处是,对一种数据对象可以根据其实际情况来定义接口的成员,比如有些对象只需要读,那么在其仓储接口中就不需要定义Update、Insert等成员。
- 还有一种也是比较常见的,就是创建一个通用仓储接口,然后所有仓储接口都继承自这个接口。
内存数据测试先使用第一种仓储模式:分别定义对于AuthorDto和BookDto的相关操作方法,目前所有方法都是为了获取数据。后面对数据的其他操作(如添加、更新和删除等)都会添加进来。
public interface IAuthorRepository
{
IEnumerable<AuthorDto> GetAuthors();
AuthorDto GetAuthor(Guid authorId);
bool IsAuthorExists(Guid authorId);
}
public interface IBookRepository
{
IEnumerable<BookDto> GetBooksForAuthor(Guid authorId);
BookDto GetBookForAuthor(Guid authorId, Guid bookId);
}
创建上述两个接口的具体仓储实现:
public class AuthorMockRepository : IAuthorRepository
{
public AuthorDto GetAuthor(Guid authorId)
{
var author = LibraryMockData.Current.Authors.FirstOrDefault(au => au.Id == authorId);
return author;
}
public IEnumerable<AuthorDto> GetAuthors()
{
return LibraryMockData.Current.Authors;
}
public bool IsAuthorExists(Guid authorId)
{
return LibraryMockData.Current.Authors.Any(au => au.Id == authorId);
}
}
public class BookMockRepository : IBookRepository
{
public BookDto GetBookForAuthor(Guid authorId, Guid bookId)
{
return LibraryMockData.Current.Books.FirstOrDefault(b => b.AuthorId == authorId && b.Id == bookId);
}
public IEnumerable<BookDto> GetBooksForAuthor(Guid authorId)
{
return LibraryMockData.Current.Books.Where(b => b.AuthorId == authorId).ToList();
}
}
为了在程序中使用上述两个仓储接口,还需要再Startup类的ConfigureServices方法中将它们添加到依赖注入容器中:
public void ConfigureServices(IServiceCollection services)
{
···
services.AddScoped<IAuthorRepository, AuthorMockRepository>();
services.AddScoped<IBookRepository, BookMockRepository>();
}
创建Controller
AuthorController类继承自ControllerBase类,并且标有[Route]特性和[ApiController]特性,其中,
- [Route]特性设置类默认的路由值api/[Controller].由于WebApi作为向外公开的接口,其路由名称应固定,为了防止由于类名重构后引起API路由发生变化,可以将这里的默认路由值改为固定值。
- [ApiController]继承自ControllerAttribute类,并且包括了一些在开发WebApi应用时极为方便的功能,如自动模型验证以及Action参数来源推断。因此ASP.NET Core2.1之前的Controller中,对于模型状态判断的代码可以删除。
构造函数注入,获取之前定义的仓储接口,增加对应的CRUD。
//[Route("api/[Controller]")] 由于WebApi作为向外公开的接口,其路由名称应固定,为了防止由于类名重构后引起API路由发生变化,可以将这里的默认路由值改为固定值。
[Route("api/authors")]
[ApiController]
public class AuthorController : ControllerBase
{
//构造函数注入 获得仓储接口
public IAuthorRepository AuthorRepository { get; set; }
public AuthorController(IAuthorRepository authorMockRepository)
{
AuthorRepository = authorMockRepository;
}
//获取集合 所有作者的信息
[HttpGet]
public ActionResult<List<AuthorDto>> GetAuthors()
{
return AuthorRepository.GetAuthors().ToList();
}
//获取单个资源 REST约束中规定每个资源应由一个URL代表,所以对于单个URL应使用api/authors/{authorId}
//[HttpGet]设置了路有模板authorId,用于为当前Action提供参数。
[HttpGet("{authorId}", Name = nameof(GetAuthor))]
public ActionResult<AuthorDto> GetAuthor(Guid authorId)
{
var author = AuthorRepository.GetAuthor(authorId);
if (author == null)
{
return NotFound();
}
else
{
return author;
}
}
}
使用EF Core 重构
使用EF Core的CodeFist重构项目,添加测试数据
创建实体类,添加文件夹Entities,创建Author类和Book类,[ForeignKey]特性,用于指明外键的属性名。
创建数据库上下文的DbContext类,继承DbContext类,DbSet类代表数据表
配置数据库连接字符串:appsettings.json中配置数据库连接
添加服务,在Startup类中通过IServiceCollection接口的AddDbContext扩展方法将他作为服务添加到依赖注入容器中。而要调用这个方法,LibraryDbContext必须要有一个带有DbContextOptions
类型参数的构造函数。 添加迁移与创建数据库:Ctrl+` 程序包管理控制台命令行操作
首次迁移:Add-Migration InitialCreation
将迁移应用到数据库中:Update-Database
添加数据:EF Core2.1增加了用于添加测试数据的API,ModelBuilder类的方法Entity
()会返回一个EntityTypeBuilder 对象,该对象提供了HasData方法,使用它可以将一个或多个实体对象添加到数据库中。为了保证类的简介清晰,可以为ModelBuilder创建扩展方法,在扩展方法内添加数据。 添加ModelBuilder扩展方法
将数据添加到数据库,创建一个迁移:Add-Migration SeedData
将数据更新到数据库中:Update-Database
删除测试数据,删除或注释调用HasData方法代码,添加一个迁移:Add-Migration RemoveSeedeData
//1.注释掉HasData方法代码 //modelBuilder.SeedData(); //2.添加迁移 Add-Migration RemoveSeedeData Update-Database
使用EF Core重构仓储类
创建通用仓储接口
Services目录下创建一个接口IRepositoryBase
,并为它添加CRUD方法。
- 创建、更新、删除三个同步方法
- 获取、条件获取、保存三个异步方法
添加接口IRepositoryBase2<T, TId>,两个方法:
- 根据指定的实体Id获取实体
- 检查具有指定Id的实体是否存在
实现仓储接口:Services文件中添加RepositoryBase类,继承两个通用仓储接口
创建每一个实体类型的仓储接口:IAuthorRepository、IBookRepository、AuthorRrpository、BookRepository
并使其所创建的接口及实现分别继承自IRepositoryBase、IRepositoryBase2。
之所以为每个实体再创建自己的仓储接口与实现类,是因为它们除了可以使用父接口和父类提供的方法以外,还可以根据自身的需要再单独定义方法。
创建仓储包装器:IRepositoryWrapper接口及其实现。包装器提供了对所有仓储接口的统一访问方式,避免单独访问每个仓储接口,对仓储的操作都是通过调用包装器所提供的成员他们来完成的。IRepositoryWrapper接口中的两个属性分别代表IBookRepository、IAuthorRepository接口。
添加服务:Startup类ConfigureServices()方法中:services.AddScoped<IRepositoryWrapper, RepositoryWrapper>();
重构Controller和Action
设置对象映射
- 使用对象映射库AutoMapper:NuGet搜索:AutoMapper
- 添加AutoMapper服务,Startup类中的ConfigureServices().添加到依赖注入容器中。
- 创建映射规则:为了使AutoMapper能够正确地执行对象映射,我们还需要创建一个Profile类的派生类,用以说明要映射的对象以及映射规则。Profile类位于AutoMapper命名空间下,它是AutoMapper中用于配置对象映射关系的类。
- 创建Helpers文件夹,添加LibraryMappingProfile类,添加无参构造函数,构造函数中使用基类Profile的CreateMap方法来创建对象映射关系。
重构Controller
Controller构造函数中将IRepositoryWrapper接口和IMapper接口注入进来,前者用于操作仓储类,后来用于处理对象之间的映射关系。
添加过滤器,把MVC请求过程中一些特性阶段(如执行Action)前后重复执行的一些代码提取出来。
添加Filters文件夹,新增CheckAuthorExistFilterAttribute,使它继承ActionFilterAttribute类,并重写基类的OnActionExecutionAsync方法。
添加服务:在Startup类的ConfigureServices方法中将CheckAuthorExistFilterAttribute类添加到容器中。
services.AddScoped<CheckAuthorExistFilterAttribute>();
使用过滤器:在Controller中通过[ServiceFilter]特性使用CheckAuthorExistFilterAttribute了。
[ServiceFilter(typeof(CheckAuthorExistFilterAttribute))] public class BookController : ControllerBase { }
添加对应的CRUD
创建实体类
创建实体类、数据库上下文类
public class Author
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required]
[MaxLength(20)]
public string Name { get; set; }
[Required]
public DateTimeOffset BirthData { get; set; }
[Required]
[MaxLength(40)]
public string BirthPlace { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
public ICollection<Book> Books { get; set; } = new List<Book>();
}
public class Book
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(100)]
public string Title { get; set; }
[MaxLength(500)]
public string Description { get; set; }
public int Pages { get; set; }
[ForeignKey("AuthorId")]
public Author Author { get; set; }
public Guid AuthorId { get; set; }
}
public class LibraryDbContext : DbContext
{
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
public LibraryDbContext(DbContextOptions<LibraryDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.SeedData();
}
}
添加服务,在调用AddDbContext方法时通过DbContextOptionsBuilder(option变量的类型)对象配置数据库。使用UseSqlServer方法来指定使用SQL Server数据库,同时通过方法参数指定了数据库连接字符串。为了避免硬编码数据库连接字符串,应将它放到配置文件中,在appsettings.json文件中的一级节点增加如下配置内容。
添加服务
services.AddDbContext<LibraryDbContext>(option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
配置连接字符串
"ConnectionStrings": {
"DefaultConnection": "Data Source = (local);Initial Catalog = Library;Integrated Security=SSPI"
},
添加测试数据:EF Core2.1增加了用于添加测试数据的API,ModelBuilder类的方法Entity
EntityTypeBuilder
类还提供了Fluent API,这些API包括了几类不同功能的方法,它们能够设置字段的属性(如长度、默认值、列名、是否必需等)、主键、表与表之间的关系等。
public class LibraryDbContext : DbContext
{
//ModelBuilder类的方法Entity<T>()会返回一个EntityTypeBuilder<T>对象,该对象提供了HasData方法,使用它可以将一个或多个实体对象添加到数据库中
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.SeedData();
}
}
//ModelBuilder的扩展方法
public static class ModelBuilderExtension
{
public static void SeedData(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>().HasData();
modelBuilder.Entity<Book>().HasData();
}
}
删除测试数据,删除或注释调用HasData方法代码,添加一个迁移:Add-Migration RemoveSeedeData
//1.注释掉HasData方法代码
//modelBuilder.SeedData();
//2.添加迁移
Add-Migration RemoveSeedeData
Update-Database
重构仓储类
创建通用的仓储接口:
- Services目录下创建一个接口IRepositoryBase
,并为它添加CRUD方法。 - 创建、更新、删除三个同步方法
- 获取、条件获取、保存三个异步方法
- 添加接口IRepositoryBase2<T, TId>,两个方法:
- 根据指定的实体Id获取实体
- 检查具有指定Id的实体是否存在
public interface IRepositoryBase<T>
{
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> GetByConditionAsync(Expression<Func<T, bool>> expression);
void Create(T entity);
void Update(T entity);
void Delete(T entity);
Task<bool> SaveAsync();
}
public interface IRepositoryBase2<T, TId>
{
Task<T> GetByIdAsync(TId id);
Task<bool> IsExistAsync(TId id);
}
实现仓储接口:Services文件中添加RepositoryBase类,继承两个通用仓储接口:
在RepositoryBase类中包括一个带有DbContext类型参数的构造函数,并有一个DbContext属性用于接受传入的参数。而在所有对接口定义方法的视线中,除了SaveAsync方法,其他方法均调用了DbContext.Set
EF Core对于查询的执行采用延迟执行的方式。当在程序中使用LINQ对数据库进行查询时,此时查询并未实际执行,而是仅在相应的变量中储存了查询命令,只有遇到了实际需要结果的操作时,查询才会执行,并返回给程序中定义的变量,这些操作包括以下几种类型:
- 对结果使用for 或foreach循环
- 使用了ToList()、ToArray()、ToDictionary()等方法
- 使用了Single()、Count()、Average()、First()和Max()等方法
使用延迟执行的好处是,EF Core在得到最终结果之前,能够对集合进行筛选和排序,但并不会执行实际的操作,仅在遇到上面那些情况时才会执行。这要比获取到所有结果之后再进行筛选和排序更高效。
public class RepositoryBase<T, TId> : IRepositoryBase<T>, IRepositoryBase2<T, TId> where T : class
{
public DbContext DbContext { get; set; }
public RepositoryBase(DbContext dbContext)
{
DbContext = dbContext;
}
public void Create(T entity)
{
DbContext.Set<T>().Add(entity);
}
public void Delete(T entity)
{
DbContext.Set<T>().Remove(entity);
}
public Task<IEnumerable<T>> GetAllAsync()
{
return Task.FromResult(DbContext.Set<T>().AsEnumerable());
}
public Task<IEnumerable<T>> GetByConditionAsync(Expression<Func<T, bool>> expression)
{
return Task.FromResult(DbContext.Set<T>().Where(expression).AsEnumerable());
}
public async Task<T> GetByIdAsync(TId id)
{
return await DbContext.Set<T>().FindAsync(id);
}
public async Task<bool> IsExistAsync(TId id)
{
return await DbContext.Set<T>().FindAsync(id) != null;
}
public async Task<bool> SaveAsync()
{
return await DbContext.SaveChangesAsync() > 0;
}
public void Update(T entity)
{
DbContext.Set<T>().Update(entity);
}
}
创建每一个实体类型的仓储接口并实现,并使其所创建的接口及实现分别继承自IRepositoryBase、IRepositoryBase2。之所以为每个实体再创建自己的仓储接口与实现类,是因为它们除了可以使用父接口和父类提供的方法以外,还可以根据自身的需要再单独定义方法。
public interface IAuthorRepository : IRepositoryBase<Author>, IRepositoryBase2<Author, Guid>
{
}
public interface IBookRepository : IRepositoryBase<Book>, IRepositoryBase2<Book, Guid>
{
}
public class AuthorRepository : RepositoryBase<Author, Guid>, IAuthorRepository
{
public AuthorRepository(DbContext dbContext) : base(dbContext)
{
}
}
public class BookRepository : RepositoryBase<Book, Guid>, IBookRepository
{
public BookRepository(DbContext dbContext) : base(dbContext)
{
}
}
创建仓储包装器:IRepositoryWrapper接口及其实现。包装器提供了对所有仓储接口的统一访问方式,避免单独访问每个仓储接口,对仓储的操作都是通过调用包装器所提供的成员他们来完成的。
IRepositoryWrapper接口中的两个属性分别代表IBookRepository、IAuthorRepository接口。
public interface IRepositoryWrapper
{
IBookRepository Book { get; }
IAuthorRepository Author { get; }
}
public class RepositoryWrapper : IRepositoryWrapper
{
private readonly IAuthorRepository _authorRepository = null;
private readonly IBookRepository _bookRepository = null;
public RepositoryWrapper(LibraryDbContext libraryDbContext)
{
LibraryDbContext = libraryDbContext;
}
public IAuthorRepository Author => _authorRepository ?? new AuthorRepository(LibraryDbContext);
public IBookRepository Book => _bookRepository ?? new BookRepository(LibraryDbContext);
public LibraryDbContext LibraryDbContext { get; }
}
添加服务:Startup的ConfigureServices中添加服务
services.AddScoped<IRepositoryWrapper, RepositoryWrapper>();
重构Controller和Action
使用AutoMapper
挡在项目中使用实体类以及EF Core时,应用程序将会从数据库中读取数据,并由EF Core返回实体对象。然而在Controller中,无论对GET请求返回资源,还是从POST、PUT、PATCH等请求接受正文,所有操作的对象都是DTO。对于实体对象,为了能够创建一个相应的DTO,需要对象反转,反之亦然。当实体类与DTO之间的映射属性较多时,甚至存在更复杂的映射规则,如果不借助于类似映射库之类的工具,使用手工转换会很费力,并且极容易出错。这里我们选择使用对象映射库AutoMapper:
AutoMapper是一个对象映射的库。在项目中,实体与DTO之间的转换通常由对象映射库完成。AutoMapper功能强大,简单易用。
- 使用对象映射库AutoMapper:NuGet搜索:AutoMapper
- 添加AutoMapper服务,Startup类中的ConfigureServices().添加到依赖注入容器中。
services.AddAutoMapper(typeof(Startup));
为了使AutoMapper能够正确地执行对象映射,我们还需要创建一个Profile类的派生类,用以说明要映射的对象以及映射规则。Profile类位于AutoMapper命名空间下,它是AutoMapper中用于配置对象映射关系的类。
创建Helpers文件夹,添加LibraryMappingProfile类,添加无参构造函数,构造函数中使用基类Profile的CreateMap方法来创建对象映射关系。
public class LibraryMappingProfile : Profile
{
public LibraryMappingProfile()
{
CreateMap<Author, AuthorDto>()
.ForMember(dest => dest.Age, config =>
config.MapFrom(src => src.BirthData.Year));
CreateMap<Book, BookDto>();
CreateMap<AuthorForCreationDto, Author>();
CreateMap<BookForCreationDto, Book>();
CreateMap<BookForUpdateDto, Book>();
}
重构AuthorController
AuthorController构造函数中将IRepositoryWrapper接口和IMapper接口注入进来,前者用于操作仓储类,后来用于处理对象之间的映射关系。
[Route("api/authors")]
[ApiController]
public class AuthorController : ControllerBase
{
public IMapper Mapper { get; }
public IRepositoryWrapper RepositoryWrapper { get; }
public AuthorController(IMapper mapper, IRepositoryWrapper repositoryWrapper)
{
Mapper = mapper;
RepositoryWrapper = repositoryWrapper;
}
}
当上述服务注入后,在Controller中的各个方法就可以使用它们。以下是获取作者列表重构后的代码。
- 首先由IRepositoryWrapper接口使用相应的仓储接口从数据库中获取数据
- 使用IMapper接口的Map方法将实体对象集合转化为DTO对象集合,并返回。
在RepositoryBase类中使用的延迟执行会在程序运行到“使用AutoMapper进行对象映射”这句代码时才实际去执行查询。
var authorDtoList = Mapper.Map<IEnumerable<AuthorDto>>(authors);
[HttpGet]
public async Task<ActionResult<IEnumerable<AuthorDto>>> GetAuthorsAsync()
{
var authors = (await RepositoryWrapper.Author.GetAllAsync()).OrderBy(author => author.Name);
var authorDtoList = Mapper.Map<IEnumerable<AuthorDto>>(authors);
return authorDtoList.ToList();
}
重构BookController
在BookController中,所有Action操作都是基于一个存在的Author资源,这可见于BookController类路由特性的定义中包含authorId,以及每个Action中都会检测指定的authorId是否存在。因此在每个Action中,首先都应包含如下逻辑:
if (!await RepositoryWrapper.Author.IsExistAsync(authorId))
{
return NotFound();
}
然而,若在每个Action中都添加同样的代码,则会造成代码多出重复,增加代码维护成本,因此可以考虑使用过滤器,在MVC请求过程中一些特定的阶段(如执行Action)前后执行一些代码。
添加Filters文件夹,新增CheckAuthorExistFilterAttribute,使它继承ActionFilterAttribute类,并重写基类的OnActionExecutionAsync方法。
在OnActionExecutionAsync方法中,通过ActionExecutingContext对象的ActionAtguments属性能够得到所有将要传入Action的参数。当得到authorId参数后,使用IAuthorRepository接口的.IsExistAsync方法来验证是否存在具有指定authorId参数值的实体。而IAuthorRepository接口则是通过构造函数注入进来的IRepositoryWrapper接口的Author属性得到的。如果检查结果不存在,则通过设置ActionExecutingContext对象的Result属性结束本次请求,并返回404 Not Found状态码;反之,则继续完成MVC请求。
public class CheckAuthorExistFilterAttribute : ActionFilterAttribute
{
public IRepositoryWrapper RepositoryWrapper { get; set; }
public CheckAuthorExistFilterAttribute(IRepositoryWrapper repositoryWrapper)
{
RepositoryWrapper = repositoryWrapper;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var authorIdParameter = context.ActionArguments.Single(m => m.Key == "authorId");
Guid authorId = (Guid)authorIdParameter.Value;
var isExist = await RepositoryWrapper.Author.IsExistAsync(authorId);
if (isExist) context.Result = new NotFoundResult();
await base.OnActionExecutionAsync(context, next);
}
}
添加服务:在Startup类的ConfigureServices方法中将CheckAuthorExistFilterAttribute类添加到容器中。
services.AddScoped<CheckAuthorExistFilterAttribute>();
使用过滤器:在Controller中通过[ServiceFilter]特性使用CheckAuthorExistFilterAttribute了。
[ServiceFilter(typeof(CheckAuthorExistFilterAttribute))]
public class BookController : ControllerBase
{
}