65.9K
CodeProject 正在变化。 阅读更多。
Home

Whiteapp ASP.NET Core 使用洋葱架构

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2020年7月19日

MIT

7分钟阅读

viewsIcon

50801

WhiteApp 或 QuickApp API 解决方案模板,基于洋葱架构构建

引言

Whiteapp 意为启动项目,它将集成所有必需的功能。当我们开始一个新项目时,应该思考业务。当业务需求准备就绪后,我们就需要开始关注业务,并准备好基础/必需的库。

我们将按顺序介绍一些应集成到我们启动项目中的重要库。

ASP.NET Core 必备库/准备步骤

  1. Entity Framework Core
  2. AutoMapper
  3. 依赖注入
  4. 日志 - Serilog/ NLog/ StackExchange.Exceptional
  5. CQRS with MediatR
  6. Fluent Validations
  7. 身份验证
  8. Swagger/ OpenAPI
  9. 错误处理
  10. 通过 NUnit 进行单元测试
  11. 通过 NUnit 进行集成测试
  12. 版本控制

目录

  1. 什么是洋葱架构
  2. 设计模式 vs 架构模式 vs 架构风格的区别
  3. 洋葱架构的优势
  4. 洋葱架构的劣势
  5. 洋葱架构的层级
  6. 洋葱架构入门
    1. 第 1 步:从项目模板下载并安装 Visual Studio 扩展
    2. 第 2 步:创建项目
    3. 第 3 步:选择洋葱架构项目模板
    4. 第 4 步:项目已准备就绪
    5. 第 5 步:在 appsettings.json 中配置连接字符串
    6. 第 6 步:创建数据库(示例为 Microsoft SQL Server)
    7. 第 7 步:构建并运行应用程序
  7. 深入代码片段
  8. 这个解决方案解决了什么问题?
  9. 这如何帮助其他人?
  10. 代码是如何工作的?
  11. 参考文献
  12. 结论
  13. 历史

什么是洋葱架构

这是一种由 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 解决方案包含了所有必需的库和最佳实践,有助于快速启动项目。开发人员可以专注于业务需求并构建实体。这可以节省大量的开发时间。

以下是一些已包含在项目中的、基于洋葱架构的最佳实践的必备库:

  1. Entity Framework Core
  2. AutoMapper
  3. 依赖注入
  4. 日志 - Serilog/ NLog/ StackExchange.Exceptional
  5. CQRS with MediatR
  6. Fluent Validations
  7. 身份验证
  8. Swagger/ OpenAPI
  9. 错误处理
  10. 通过 NUnit 进行单元测试
  11. 通过 NUnit 进行集成测试
  12. 版本控制

这如何帮助他人

此解决方案有助于开发人员节省开发时间并专注于业务模块。对于经验较少的人员,它有助于在项目中维护最佳实践(如干净的代码)。

代码是如何工作的?

这是托管在 marketplace.visualstudio.com 上的项目模板。从 marketplace 下载此扩展并安装到您的 Visual Studio 中。创建项目时选择此模板。 

结论

在本文中,我们了解了洋葱架构是什么,以及设计模式、架构模式和架构风格之间的区别。项目模板将帮助我们快速启动应用程序。

参考文献

历史

  • 2020 年 7 月 10 日:初始版本
© . All rights reserved.