大体操作流程:
创建仓储接口
创建基于接口的具体仓储实现
添加服务,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)的一部分,在系统设计中的使用非常广泛。它主要用于解除业务逻辑与数据访问层之间的耦合,使业务逻辑在储存、访问数据库时无须关心数据的来源及储存方式。
实现仓储模式的方法有多种:
内存数据测试先使用第一种仓储模式:分别定义对于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>(); }
AuthorController类继承自ControllerBase类,并且标有[Route]特性和[ApiController]特性,其中,
构造函数注入,获取之前定义的仓储接口,增加对应的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的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
创建通用的仓储接口:
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>();
挡在项目中使用实体类以及EF Core时,应用程序将会从数据库中读取数据,并由EF Core返回实体对象。然而在Controller中,无论对GET请求返回资源,还是从POST、PUT、PATCH等请求接受正文,所有操作的对象都是DTO。对于实体对象,为了能够创建一个相应的DTO,需要对象反转,反之亦然。当实体类与DTO之间的映射属性较多时,甚至存在更复杂的映射规则,如果不借助于类似映射库之类的工具,使用手工转换会很费力,并且极容易出错。这里我们选择使用对象映射库AutoMapper:
AutoMapper是一个对象映射的库。在项目中,实体与DTO之间的转换通常由对象映射库完成。AutoMapper功能强大,简单易用。
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构造函数中将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中的各个方法就可以使用它们。以下是获取作者列表重构后的代码。
在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中,所有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 { }