Whiteapp ASP.NET Core 使用洋葱架构





5.00/5 (12投票s)
WhiteApp 或 QuickApp API 解决方案模板,基于洋葱架构构建
引言
Whiteapp 意为启动项目,它将集成所有必需的功能。当我们开始一个新项目时,应该思考业务。当业务需求准备就绪后,我们就需要开始关注业务,并准备好基础/必需的库。
我们将按顺序介绍一些应集成到我们启动项目中的重要库。
ASP.NET Core 必备库/准备步骤
- Entity Framework Core
- AutoMapper
- 依赖注入
- 日志 - Serilog/ NLog/ StackExchange.Exceptional
- CQRS with MediatR
- Fluent Validations
- 身份验证
- Swagger/ OpenAPI
- 错误处理
- 通过 NUnit 进行单元测试
- 通过 NUnit 进行集成测试
- 版本控制
目录
- 什么是洋葱架构
- 设计模式 vs 架构模式 vs 架构风格的区别
- 洋葱架构的优势
- 洋葱架构的劣势
- 洋葱架构的层级
- 洋葱架构入门
- 深入代码片段
- 这个解决方案解决了什么问题?
- 这如何帮助其他人?
- 代码是如何工作的?
- 参考文献
- 结论
- 历史
什么是洋葱架构
这是一种由 Jeffrey Palermo 在 2008 年提出的架构模式,它解决了应用程序维护的问题。在传统的架构中,我们通常采用以数据库为中心的架构。
洋葱架构基于控制反转原则。它由领域同心架构组成,其中各层向领域(实体/类)进行接口交互。
洋葱架构的主要优势在于更高的灵活性和解耦性。在这种方法中,我们可以看到所有层都只依赖于领域层(有时也称为核心层)。
设计模式 vs 架构模式 vs 架构风格的区别
- 架构风格是最高抽象级别的应用设计
- 架构模式是实现架构风格的一种方式
- 设计模式是解决局部问题的方案
例如
- 您想在项目或需求中实现的,例如具有高抽象级别的 CRUD 操作,这就是架构风格
- 您将如何实现它,这就是架构模式
- 您将遇到的并要解决的问题是设计模式
洋葱架构基于架构模式。
洋葱架构的优势
- 可测试性:由于解耦了所有层,因此很容易为每个组件编写测试用例
- 适应性/增强性:添加与应用程序交互的新方式非常容易
- 可持续性:我们可以将所有第三方库放在基础设施层,从而便于维护
- 数据库独立性:由于数据库与数据访问分离,因此切换数据库提供程序非常容易
- 干净的代码:由于业务逻辑与表示层分离,因此易于实现 UI(如 React、Angular 或 Blazor)
- 组织良好:项目组织良好,便于理解和新成员加入项目
洋葱架构的劣势
- 领域层可能很重:大量逻辑将在领域层(有时称为核心层)中实现
- 层数较多:即使是实现 CRUD 操作这种小型应用,也需要实现较多的层
洋葱架构的层级
在这种方法中,我们可以看到所有层都只依赖于核心层。
- 领域层:领域层(核心层)位于中心,并且不依赖于任何其他层。因此,我们通过创建持久化层的接口,并在外部层实现这些接口。这也被称为 DIP 或依赖倒置原则。
- 持久化层:在持久化层,我们实现存储库设计模式。在我们的项目中,我们实现了 EntityFramework,它已经实现了存储库设计模式。
DbContext
将是 UoW(工作单元),每个 DbSet 都是存储库。它使用数据提供程序与我们的数据库交互。 - 服务层:服务层(也称为应用层),我们可以在这里实现业务逻辑。对于 OLAP/OLTP 过程,我们可以实现 CQRS 设计模式。在我们的项目中,我们在 MediatR 库之上实现了基于 Mediator 设计模式的 CQRS 设计模式。
如果您想实现电子邮件功能逻辑,我们在服务层定义一个IMailService
。
使用 DIP,可以轻松切换实现。这有助于构建可扩展的应用程序。 - 基础设施层:在此层,我们添加第三方库,如 JWT Tokens 身份验证或 Serilog 用于日志记录等,以便所有第三方库都集中在一个地方。在我们的项目中,我们实现了几乎所有重要的库,您可以根据您的项目需求在 StartUp.cs 文件中即插即用(添加/删除)。
- 表示层:可以是 WebApi 或 UI。
当我们开始实现下面的项目时,您会了解更多。
洋葱架构入门
第 1 步:从项目模板下载并安装 Visual Studio 扩展
从 Microsoft marketplace 下载并安装 Visual Studio 扩展。
第 2 步:创建项目
选择项目类型为 API,并选择 **洋葱架构**。
第 3 步:选择洋葱架构项目模板
选择项目类型为 **API**,并选择 **洋葱架构**。
第 4 步:项目已准备就绪
第 5 步:在 appsettings.json 中配置连接字符串
根据您的 SQL Server 配置连接字符串。在演示项目中,我们使用 MS SQL Server。
"ConnectionStrings": {
"OnionArchConn": "Data Source=(local)\\SQLexpress;
Initial Catalog=OnionDb;Integrated Security=True"
},
第 6 步:创建数据库(示例为 Microsoft SQL Server)
由于我们使用 EF Core,采用 Code first 方法
对于 Code First 方法(要运行此应用程序,请使用 Code First 方法)
Update-Database
第 7 步:构建并运行应用程序
Swagger UI
深入代码片段
将深入了解所有层
领域层代码片段
首先看领域层,在这里我们将根据需求创建实体。我们假设需求如下面的图所示。客户应用程序,客户与订单之间为 1 对多关系,类似地,供应商与产品之间为 1 对多关系,并且 OrderDetail 表是多对多关系。
根据实体文件名(如下所示)创建以下实体。要在 Entityframework core 中创建 1 对多关系,请在 Customer 中添加 Orders 属性列表。对 Supplier、Category、Product 和 OrderDetail 重复相同的操作(请参阅源代码)。
public class BaseEntity
{
[Key]
public int Id { get; set; }
}
public class Customer : BaseEntity
{
public Customer()
{
Orders = new List<Order>();
}
public string CustomerName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
public List<Order> Orders { get; set; }
}
public class Order : BaseEntity
{
public Customer Customers { get; set; }
public int CustomerId { get; set; }
public int EmployeeId { get; set; }
public DateTime OrderDate { get; set; }
public DateTime RequiredDate { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
持久化层代码片段
在持久化层,我们将创建 ApplicationDbContext 和 IApplicationDbContext 以实现关注点分离(或者,如果使用 Dapper 或任何其他库,您可以使用存储库设计模式)。
根据文件名创建以下 IApplicationDbContext 和 ApplicationDbContext。
public interface IApplicationDbContext
{
DbSet<Category> Categories { get; set; }
DbSet<Customer> Customers { get; set; }
DbSet<Order> Orders { get; set; }
DbSet<Product> Products { get; set; }
DbSet<Supplier> Suppliers { get; set; }
Task<int> SaveChangesAsync();
}
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
// This constructor is used of runit testing
public ApplicationDbContext()
{
}
public ApplicationDbContext(DbContextOptions options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Supplier> Suppliers { get; set; }
// In case table not required with primary constraint, OrderDetail table has no primary key
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderDetail>().HasKey(o => new { o.OrderId, o.ProductId });
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder
.UseSqlServer("DataSource=app.db");
}
}
public async Task<int> SaveChangesAsync()
{
return await base.SaveChangesAsync();
}
}
此层包含
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
服务层代码片段
在服务层,我们在代码示例中将基于 Mediator 设计模式通过 MediatR 库实现 CQRS 设计模式。对于业务逻辑,我们可以使用同一层。
以下代码片段显示了 GetAllCustomerQuery 和 CreateCustomerCommand,它们与文件名相对应。我们可以利用 MediatR 库来代替手动编写 Mediator 设计模式。
public class GetAllCustomerQuery : IRequest<IEnumerable<Customer>>
{
public class GetAllCustomerQueryHandler : IRequestHandler<GetAllCustomerQuery, IEnumerable<Customer>>
{
private readonly IApplicationDbContext _context;
public GetAllCustomerQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Customer>> Handle(GetAllCustomerQuery request, CancellationToken cancellationToken)
{
var customerList = await _context.Customers.ToListAsync();
if (customerList == null)
{
return null;
}
return customerList.AsReadOnly();
}
}
}
public class CreateCustomerCommand : IRequest<int>
{
public string CustomerName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateCustomerCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
var customer = new Customer();
customer.CustomerName = request.CustomerName;
customer.ContactName = request.ContactName;
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return customer.Id;
}
}
}
与 CreateCustomerCommand 类类似,为 UpdateCustomerCommand 和 DeleteCustomerByIdCommand 创建(请参阅源代码)。
基础设施层代码片段
在基础设施层,我们可以看到所有第三方库都在此层。所有库也实现了 Configure 和 ServiceConfigure 的扩展方法。
以下代码片段是扩展方法,而不是在 StartUp.cs 文件中进行配置,这使得我们在表示层中的代码更加整洁。
以下配置包含 EntityFrameworkCore、Swagger、Versioning 等库。
public static class ConfigureServiceContainer
{
public static void AddDbContext(this IServiceCollection serviceCollection,
IConfiguration configuration, IConfigurationRoot configRoot)
{
serviceCollection.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("OnionArchConn") ?? configRoot["ConnectionStrings:OnionArchConn"])
);
}
public static void AddAddScopedServices(this IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
}
public static void AddTransientServices(this IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<IMailService, MailService>();
}
public static void AddSwaggerOpenAPI(this IServiceCollection serviceCollection)
{
serviceCollection.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc(
"OpenAPISpecification",
new Microsoft.OpenApi.Models.OpenApiInfo()
{
Title = "Customer API",
Version = "1",
Description = "Through this API you can access customer details",
Contact = new Microsoft.OpenApi.Models.OpenApiContact()
{
Email = "amit.naik8103@gmail.com",
Name = "Amit Naik",
Url = new Uri("https://amitpnk.github.io/")
},
License = new Microsoft.OpenApi.Models.OpenApiLicense()
{
Name = "MIT License",
Url = new Uri("https://open-source.org.cn/licenses/MIT")
}
});
var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile);
setupAction.IncludeXmlComments(xmlCommentsFullPath);
});
}
public static void AddMailSetting(this IServiceCollection serviceCollection,
IConfiguration configuration)
{
serviceCollection.Configure<MailSettings>(configuration.GetSection("MailSettings"));
}
public static void AddController(this IServiceCollection serviceCollection)
{
serviceCollection.AddControllers().AddNewtonsoftJson();
}
public static void AddVersion(this IServiceCollection serviceCollection)
{
serviceCollection.AddApiVersioning(config =>
{
config.DefaultApiVersion = new ApiVersion(1, 0);
config.AssumeDefaultVersionWhenUnspecified = true;
config.ReportApiVersions = true;
});
}
}
与 ConfigureContainer.cs 文件类似的配置。
public static class ConfigureContainer
{
public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<CustomExceptionMiddleware>();
}
public static void ConfigureSwagger(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(setupAction =>
{
setupAction.SwaggerEndpoint("/swagger/OpenAPISpecification/swagger.json", "Onion Architecture API");
setupAction.RoutePrefix = "OpenAPI";
});
}
public static void ConfigureSwagger(this ILoggerFactory loggerFactory)
{
loggerFactory.AddSerilog();
}
}
表示层代码片段
在 StartUp.cs 文件中,您可以看到 ConfigureServices 和 Configure 方法中的代码非常简洁。
public class Startup
{
private readonly IConfigurationRoot configRoot;
public Startup(IConfiguration configuration)
{
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
Configuration = configuration;
IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json");
configRoot = builder.Build();
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddController();
services.AddDbContext(Configuration, configRoot);
services.AddAutoMapper();
services.AddAddScopedServices();
services.AddTransientServices();
services.AddSwaggerOpenAPI();
services.AddMailSetting(Configuration);
services.AddMediatorCQRS();
services.AddVersion();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory log)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.ConfigureCustomExceptionMiddleware();
app.ConfigureSwagger();
log.AddSerilog();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
在 CustomerController.cs 文件中,通过 MediatR 库,我们可以使用 CQRS 模式来发送或获取数据。
[ApiController]
[Route("api/v{version:apiVersion}/Customer")]
[ApiVersion("1.0")]
public class CustomerController : ControllerBase
{
private IMediator _mediator;
protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
[HttpPost]
public async Task<IActionResult> Create(CreateCustomerCommand command)
{
return Ok(await Mediator.Send(command));
}
[HttpGet]
[Route("")]
public async Task<IActionResult> GetAll()
{
return Ok(await Mediator.Send(new GetAllCustomerQuery()));
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
return Ok(await Mediator.Send(new GetCustomerByIdQuery { Id = id }));
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
return Ok(await Mediator.Send(new DeleteCustomerByIdCommand { Id = id }));
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, UpdateCustomerCommand command)
{
if (id != command.Id)
{
return BadRequest();
}
return Ok(await Mediator.Send(command));
}
}
此解决方案解决了什么问题
White app 解决方案包含了所有必需的库和最佳实践,有助于快速启动项目。开发人员可以专注于业务需求并构建实体。这可以节省大量的开发时间。
以下是一些已包含在项目中的、基于洋葱架构的最佳实践的必备库:
- Entity Framework Core
- AutoMapper
- 依赖注入
- 日志 - Serilog/ NLog/ StackExchange.Exceptional
- CQRS with MediatR
- Fluent Validations
- 身份验证
- Swagger/ OpenAPI
- 错误处理
- 通过 NUnit 进行单元测试
- 通过 NUnit 进行集成测试
- 版本控制
这如何帮助他人
此解决方案有助于开发人员节省开发时间并专注于业务模块。对于经验较少的人员,它有助于在项目中维护最佳实践(如干净的代码)。
代码是如何工作的?
这是托管在 marketplace.visualstudio.com 上的项目模板。从 marketplace 下载此扩展并安装到您的 Visual Studio 中。创建项目时选择此模板。
结论
在本文中,我们了解了洋葱架构是什么,以及设计模式、架构模式和架构风格之间的区别。项目模板将帮助我们快速启动应用程序。
参考文献
- https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/
- https://dev.to/pereiren/clean-architecture-series-part-2-49db
历史
- 2020 年 7 月 10 日:初始版本