本文内容来自书籍: Marinko Spasojevic - Ultimate ASP.NET Core Web API - From Zero To Six-Figure Backend Developer (2nd edition)
这章是说的洋葱架构实现,学习如何创建不同的层,以此分割应用的不同部分。
对数据服务,我们需要创建Model
,使用code first
的方法将Model
转换成数据库表
我们将会创建一个Repository
层作为数据访问层,当创建这样一个抽象层后,我们会把业务逻辑和数据访问分割开。
这样我们的业务逻辑代码会变得更加干净,然后将所有的业务逻辑划分在Service
层,将外部的访问划分在presentation
层
关于洋葱的切分有不同的方法,这里我们将它分割为四个层次
然后Presentation
和Infrastructure
在层次结构上是在同一个level
的
下面看看更多的细节,看看为什么要这样做
所有层次间的交互时严格按照接口的定义
依赖关系的流向时指向洋葱的中心的
在整个项目中使用依赖倒置
,依赖抽象而不是实现,可以允许我们在运行时切换实现
在编译时依赖抽象,在运行时提供实现
当所有东西都是依赖抽象的,这样会使得整个架构变得容易测试
因为我们可以通过Moq
来替换抽象的实现,这样我们在编写业务逻辑的时候,就不需要依赖任何实现的细节
如果需要任何来自外部的服务,只需要创建一个接口,然后consume
,不需要担心它的实现
在洋葱里面,层次越深,所需要的依赖越少
Domain layer
没有任何直接的依赖外部的层次,它是个孤岛一样的
洋葱的低层次定义接口,高层次的洋葱实现这些接口
使用这种方法,我们就可以将所有的业务逻辑封装在Service
和Domain
中,而不需要知道任何实现的细节
在Service
中,我们只知道定义在Domain
中的接口
根据之前的理论,创建一个库Entities
,然后在里面创建一个文件夹Models
,它是包含了所有的模型
Entities
代表了EF Core
这个框架用来映射到数据库的所有模型
然后再创建一个新的库Repository
,它是数据库的上下文和repository
的实现
Repository
是关于数据库访问的实现,它需要引用EntityFrameworkCore
和Entities
然后需要创建一个RepositoryContext
继承EntityFrameworkCore
的DbContext
,用来代表访问数据库的上下文
在主项目中,需要配置数据库连接的信息,在appsettings.json
中,而且主项目需要引用Repository
"ConnectionStrings": { "sqlConnection": "server=.; database=CompanyEmployee; Integrated Security=true" }
然后在Repository
项目中,需要创建关于数据库上下文构建的工厂类
public class RepositoryContextFactory : IDesignTimeDbContextFactory<RepositoryContext> { public RepositoryContext CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); var builder = new DbContextOptionsBuilder<RepositoryContext>() .UseSqlServer(configuration.GetConnectionString("sqlConnection")); return new RepositoryContext(builder.Options); } }
迁移是标准的创建或更新数据库的过程,当我们创建Model
之后,我们就可以将Model
映射到真实的数据库中
首先修改之前的创建数据库上下文的方法
public class RepositoryContextFactory : IDesignTimeDbContextFactory<RepositoryContext> { public RepositoryContext CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); var builder = new DbContextOptionsBuilder<RepositoryContext>() .UseSqlServer(configuration.GetConnectionString("sqlConnection"), b => b.MigrationsAssembly("CompanyEmployees")); return new RepositoryContext(builder.Options); } }
修改这个是因为,我们的迁移程序集不在主程序,而是在Repository
中
在迁移之前,需要引用这个库Microsoft.EntityFrameworkCore.Tools
,然后执行迁移并更新数据库
在创建数据库之后,我们一些初始数据,在Repository
中创建一个文件夹Configuration
创建一个类CompanyConfiguration
用以初始化
public class CompanyConfiguration : IEntityTypeConfiguration<Company> { public void Configure(EntityTypeBuilder<Company> builder) { builder.HasData ( new Company { Id = new Guid("c9d4c053-49b6-410c-bc78-2d54a9991870"), Name = "IT_Solutions Ltd", Address = "583 Wall Dr. Gwynn Oak, MD 21207", Country = "USA" }, new Company { Id = new Guid("3d490a70-94ce-4d15-9494-5248280c2ce3"), Name = "Admin_Solutions Ltd", Address = "312 Forest Avenue, BF 923", Country = "USA" } ); } }
然后修改RepositoryContext
public class RepositoryContext: DbContext { public RepositoryContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new CompanyConfiguration()); modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); } public DbSet<Company> Companies { get; set; } public DbSet<Employee> Employees { get; set; } }
然后再迁移和更新数据库
我们需要创建通用的repository
,用以提供CRUD方法
然后对repository
做一个包装,用作服务注册到IoC中
最后,我们将实例化这个repository
,并且在任何controllers
中将需要的repository
注入
Contracts
项目中,为repository
创建接口public interface IRepositoryBase<T> { IQueryable<T> FindAll(bool trackChanges); IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression, bool trackChanges); void Create(T entity); void Update(T entity); void Delete(T entity); }
Repository
引用Contracts
,然后创建一个抽象类RepositoryBase
用作对IRepositoryBase
的实现public abstract class RepositoryBase<T> : IRepositoryBase<T> where T : class { protected RepositoryContext RepositoryContext; public RepositoryBase(RepositoryContext repositoryContext) => RepositoryContext = repositoryContext; public IQueryable<T> FindAll(bool trackChanges) => !trackChanges ? RepositoryContext.Set<T>().AsNoTracking() : RepositoryContext.Set<T>(); public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression, bool trackChanges) => !trackChanges ? RepositoryContext.Set<T>() .Where(expression) .AsNoTracking() : RepositoryContext.Set<T>() .Where(expression); public void Create(T entity) => RepositoryContext.Set<T>().Add(entity); public void Update(T entity) => RepositoryContext.Set<T>().Update(entity); public void Delete(T entity) => RepositoryContext.Set<T>().Remove(entity); }
现在创建实际使用的类,它是继承自RepositoryBase,而且,它需要有特定于模型的接口,这个接口可能有其他特殊的方法
这样,我们就可以将公共的逻辑和特定于模型的逻辑切割分开
Contracts
中建立接口namespace Contracts { public interface ICompanyRepository { } } namespace Contracts { public interface IEmployeeRepository { } }
Repository
中,创建User类public class CompanyRepository : RepositoryBase<Company>, ICompanyRepository { public CompanyRepository(RepositoryContext repositoryContext) : base(repositoryContext) { } }
从API中获取由多个repository
组成的结果,是非常正常的逻辑,所以有时候需要实例化多个repository
来从数据库中拉取数据
如果只有两个或者很少的repository
,当然没有问题,但是如果有非常多的不同repository
的时候,就会变得非常复杂
所以我们需要构建一个Repository Manager
,用它来帮我们实例化repository
,并且注册到IoC中
Contract
中创建接口public interface IRepositoryManager { ICompanyRepository Company { get; } IEmployeeRepository Employee { get; } void Save(); }
Repository
中,实现这个接口public sealed class RepositoryManager : IRepositoryManager { private readonly RepositoryContext _repositoryContext; private readonly Lazy<ICompanyRepository> _companyRepository; private readonly Lazy<IEmployeeRepository> _employeeRepository; public RepositoryManager(RepositoryContext repositoryContext) { _repositoryContext = repositoryContext; _companyRepository = new Lazy<ICompanyRepository>(() => new CompanyRepository(repositoryContext)); _employeeRepository = new Lazy<IEmployeeRepository>(() => new EmployeeRepository(repositoryContext)); } public ICompanyRepository Company => _companyRepository.Value; public IEmployeeRepository Employee => _employeeRepository.Value; public void Save() => _repositoryContext.SaveChanges(); }
RepositoryManager
注册到主项目中public static void ConfigureRepositoryManager(this IServiceCollection services) => services.AddScoped<IRepositoryManager, RepositoryManager>(); // Program.cs builder.Services.ConfigureRepositoryManager();
Service
是在Domain
(Contracts
是Domain
的一部分)的上一层,
所以Service
会引用Domain
Service Layer
会切分为两个项目Service.Contracts
和Service
Service.Contracts
是用于定义服务接口,以此封装主要的业务逻辑
然后会有三个接口
其实这几个接口和Repository
中使用的模式是一样的,接下来就是在Service
中实现这几个接口
internal sealed class CompanyService : ICompanyService { private readonly IRepositoryManager _repository; private readonly ILoggerManager _logger; public CompanyService(IRepositoryManager repository, ILoggerManager logger) { _repository = repository; _logger = logger; } }
public sealed class ServiceManager : IServiceManager { private readonly Lazy<ICompanyService> _companyService; private readonly Lazy<IEmployeeService> _employeeService; public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger) { _companyService = new Lazy<ICompanyService>(() => new CompanyService(repositoryManager, logger)); _employeeService = new Lazy<IEmployeeService>(() => new EmployeeService(repositoryManager, logger)); } public ICompanyService CompanyService => _companyService.Value; public IEmployeeService EmployeeService => _employeeService.Value; }
然后在主项目中引用Service
,并注册服务
public static void ConfigureServiceManager(this IServiceCollection services) => services.AddScoped<IServiceManager, ServiceManager>(); // Program.cs builder.Services.ConfigureServiceManager();
在实现了IDesignTimeDbContextFactory
接口的RepositoryContextFactory
中,我们可以在设计时就注册了RepositoryContext
,这可以让我们在迁移的时候,在别的项目找到RepositoryContext
并执行
然后我们需要修改数据库的注册方法
public static void ConfigureSqlContext(this IServiceCollection services, IConfiguration configuration) => services.AddDbContext<RepositoryContext>(opts => opts.UseSqlServer(configuration.GetConnectionString("sqlConnection")));
不需要MigrationAssembly
这个方法了
项目到了后面,在迁移数据库的时候,还是出现了错误提示,还是需要添加
MigrationAssembly
这个方法来指定