Dotnet洋蔥架構實踐

老王Plus發表於2021-03-17

一個很清晰的架構實踐,同時刨刨MySQL的坑。

一、洋蔥架構簡介

洋蔥架構出來的其實有一點年頭了。大約在2017年下半年,就有相關的說法了。不過,大量的文章在於理論性的討論,而我們今天會用一個專案來完成這個架構。

洋蔥架構,有時候也被叫做整潔架構,它本身是為高質量的軟體而存在的。

相對其它架構而言,洋蔥架構具有更好的可測試性、實用性和穩定性,並且足夠靈活,完全適應專案未來可能的成長和進化。可以這麼說,洋蔥架構完美解決了三層或N層架構所面臨的困難和問題。

牛吹完了,下面來看張圖:

這張圖,充分解釋了它為什麼叫洋蔥架構。

不過,這不是重點。這個架構最重要的是裡面的程式碼依賴原則:從外向內,並且只有這一個方向。處於內環的程式碼,不應該知道外環的任何東西

從上面圖也可以看到,洋蔥架構,也使用層的概念。不過,它不同於我們習慣的三層或N層。我們來看看每個層的情況:

  • 資料層(Domain Layer)

存在於架構的中心部分,由所有業務資料的實體組成。大多數情況下,就是我們的資料模型。後面的實踐程式碼中,我是用EF(Entity Framework)來操作的資料庫。

  • 儲存層(Repository Layer)

儲存層在架構中充當服務層和資料模型之間的紐帶,並且在這一層將保持所有資料庫操作和應用資料的上下文。通常的做法是介面,用介面來描述資料訪問所涉及的讀寫操作。

  • 服務層(Services Layer)

服務層用於實現儲存層和專案之間的通訊,同時,還可以儲存實體的業務邏輯。在這一層,服務介面與實現分離,以實現解耦和焦點分離。

  • 使用者介面層(UI Layer)

這個不解釋了。專案最終對外的一層。注意,這兒可能是網站,也可能是API。不需要糾結有沒有實際的介面。我們們的實踐程式碼中,我用的是API。

    為了防止不提供原網址的轉載,特在這裡加上原文連結:https://www.cnblogs.com/tiger-wang/p/14547702.html

二、實踐

好,現在直接進入程式碼。

1. 建立工程

這個不解釋了,都是套路:

% dotnet new webapi -o demo -f netcoreapp3.1

我這個工程用的是Dotnet Core 3.1。框架不重要,基本上哪個版本都可以用。

下面設定Swagger

這個是我的習慣,而且這個專案是個WebApi,裝個Swagger方便。

% dotnet add package swashbuckle.aspnetcore

Swagger的設定不是本文的重點,略過。需要的同學可以去看原始碼。

下面,我們在工程中建三個目錄:

  • DomainLayer
  • RepositoryLayer
  • ServicesLayer

這三個目錄對應上面的三個層。UI在這個專案裡其實就是控制器Controller,已經存在了。

建這三個目錄的目的,是為了放置三個層的程式碼。後面編碼的時候,你會看到這三個層之間的關係。另外,這三個層在實際應用時,可以獨立為三個類庫,這樣會更清晰。

前邊說了,我會用EF運算元據庫。所以,這兒還需要引入三個庫:

% dotnet add package Microsoft.EntityFrameworkCore
% dotnet add package Microsoft.EntityFrameworkCore.Relational
% dotnet add package Pomelo.EntityFrameworkCore.MySql

注意,微軟的EF框架沒有提供MySQL的接入,所以引用了一個三方的庫。

至此,專案的準備工作完成。

2. 實現資料層

在DomainLayer目錄裡,建一個Models目錄。在Models目錄下,建兩個類:

BaseEntity.cs

public class BaseEntity
{
    public int Id { get; set; }
    public DateTime CreatedDate { get; set; }
    public DateTime ModifiedDate { get; set; }
    public bool IsActive { get; set; }
}

Customer.cs

public class Customer : BaseEntity
{
    public string CustomerName { get; set; }
    public string PurchasesProduct { get; set; }
    public string PaymentType { get; set; }
}

兩個類,Customer派生自BaseEntity。沒什麼特殊的含義,也是一個習慣。而且,後面到儲存層寫著方便。

後面,我們會用到Customer和BaseEntity實體類建立的資料表。為了讓大家看的明白,我在這兒建一個目錄EntityMapper,在目錄裡寫個表結構對映。

CustomerMap.cs

public class CustomerMap : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.HasKey(x => x.Id)
            .HasName("pk_customerid");

        builder.Property(x => x.Id).ValueGeneratedOnAdd()
            .HasColumnName("id")
                .HasColumnType("INT");
        builder.Property(x => x.CustomerName)
            .HasColumnName("customer_name")
                .HasColumnType("NVARCHAR(100)");
        builder.Property(x => x.PurchasesProduct)
            .HasColumnName("purchased_product")
                .HasColumnType("NVARCHAR(100)")
                .IsRequired();
        builder.Property(x => x.PaymentType)
            .HasColumnName("payment_type")
                .HasColumnType("NVARCHAR(50)")
                .IsRequired();
        builder.Property(x => x.CreatedDate)
            .HasColumnName("created_date")
                .HasColumnType("datetime");
        builder.Property(x => x.ModifiedDate)
            .HasColumnName("modified_date")
                .HasColumnType("datetime");
        builder.Property(x => x.IsActive)
            .HasColumnName("is_active")
                .HasColumnType("bit");
    }
}

或者也可以自己建立一個表ef.Customer:

CREATE TABLE `Customer` (
  `id` int NOT NULL AUTO_INCREMENT,
  `created_date` datetime DEFAULT NULL,
  `customer_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `is_active` bit(1) DEFAULT NULL,
  `modified_date` datetime DEFAULT NULL,
  `payment_type` varchar(50) DEFAULT NULL,
  `purchased_product` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
)

3. 實現儲存層

這個層,主要用來運算元據庫。

先在Startup.cs中配置資料庫引用:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContextPool<ApplicationDbContext>(
            options => options.UseMySql(
                "server=192.168.0.241;user=root;password=xxxxxx;database=ef",
                new MySqlServerVersion(new Version(8, 0, 21)),
                mysqlOptions =>
                {
                    mysqlOptions.CharSetBehavior(CharSetBehavior.NeverAppend);
                }
        ));
    }
}

這兒偷個懶,連線串就直接寫程式碼裡了。正式做專案時,最好寫在配置檔案中。

在RepositoryLayer目錄中建一個DataContext,裡面用來放置相關資料庫會話,和操作的例項:

ApplicationDbContext.cs

public partial class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new CustomerMap());

        base.OnModelCreating(modelBuilder);
    }
}

再建個目錄RespositoryPattern,用來存放資料庫操作的類。按照注入的原則,會是兩個檔案,一個介面定義,一個實現類:

IRepository.cs

public interface IRepository<T> where T : BaseEntity
{
    IEnumerable<T> GetAll();
    T Get(int Id);
    void Insert(T entity);
    void Update(T entity);
    void Delete(T entity);
    void Remove(T entity);
    void SaveChanges();
}

Repository.cs

public class Repository<T> : IRepository<T> where T : BaseEntity
{
    private readonly ApplicationDbContext _applicationDbContext;
    private DbSet<T> entities;

    public Repository(ApplicationDbContext applicationDbContext)
    {
        _applicationDbContext = applicationDbContext;
        entities = _applicationDbContext.Set<T>();
    }

    public void Delete(T entity)
    {
        if (entity == null)
        {
            throw new ArgumentNullException("entity");
        }
        entities.Remove(entity);
        _applicationDbContext.SaveChanges();
    }
    public T Get(int Id)
    {
        return entities.SingleOrDefault(c => c.Id == Id);
    }
    public IEnumerable<T> GetAll()
    {
        return entities.AsEnumerable();
    }
    public void Insert(T entity)
    {
        if (entity == null)
        {
            throw new ArgumentNullException("entity");
        }
        entities.Add(entity);
        _applicationDbContext.SaveChanges();
    }
    public void Remove(T entity)
    {
        if (entity == null)
        {
            throw new ArgumentNullException("entity");
        }
        entities.Remove(entity);
    }
    public void SaveChanges()
    {
        _applicationDbContext.SaveChanges();
    }
    public void Update(T entity)
    {
        if (entity == null)
        {
            throw new ArgumentNullException("entity");
        }
        entities.Update(entity);
        _applicationDbContext.SaveChanges();
    }
}

4. 實現服務層

服務層用來實現核心的業務邏輯。同樣先建一個目錄CustomerService,方便注入,也是一個介面一個類:

ICustomerService.cs

public interface ICustomerService
{
    IEnumerable<Customer> GetAllCustomers();
    Customer GetCustomer(int id);
    void InsertCustomer(Customer customer);
    void UpdateCustomer(Customer customer);
    void DeleteCustomer(int id);
}

CustomerService.cs

public class CustomerService : ICustomerService
{
    private IRepository<Customer> _repository;

    public CustomerService(IRepository<Customer> repository)
    {
        _repository = repository;
    }

    public IEnumerable<Customer> GetAllCustomers()
    {
        return _repository.GetAll();
    }
    public Customer GetCustomer(int id)
    {
        return _repository.Get(id);
    }
    public void InsertCustomer(Customer customer)
    {
        _repository.Insert(customer);
    }
    public void UpdateCustomer(Customer customer)
    {
        _repository.Update(customer);
    }
    public void DeleteCustomer(int id)
    {
        Customer customer = GetCustomer(id);
        _repository.Remove(customer);
        _repository.SaveChanges();
    }
}

4. 注入

這兒就是套路了,不解釋。

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
    services.AddTransient<ICustomerService, CustomerService>();
}

5. 實現控制器

重要的三層都已經實現。下面做個演示用的控制器:

CustomerController.cs

[ApiController]
[Route("[controller]")]
public class CustomerController : ControllerBase
{
    private readonly ICustomerService _customerService;

    public CustomerController(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    [HttpGet(nameof(GetCustomer))]
    public IActionResult GetCustomer(int id)
    {
        var result = _customerService.GetCustomer(id);
        if (result != null)
        {
            return Ok(result);
        }
        return BadRequest("No records found");
    }
    [HttpGet(nameof(GetAllCustomer))]
    public IActionResult GetAllCustomer()
    {
        var result = _customerService.GetAllCustomers();
        if (result != null)
        {
            return Ok(result);
        }
        return BadRequest("No records found");
    }
    [HttpPost(nameof(InsertCustomer))]
    public IActionResult InsertCustomer(Customer customer)
    {
        _customerService.InsertCustomer(customer);
        return Ok("Data inserted");
    }
    [HttpPut(nameof(UpdateCustomer))]
    public IActionResult UpdateCustomer(Customer customer)
    {
        _customerService.UpdateCustomer(customer);
        return Ok("Updation done");
    }
    [HttpDelete(nameof(DeleteCustomer))]
    public IActionResult DeleteCustomer(int Id)
    {
        _customerService.DeleteCustomer(Id);
        return Ok("Data Deleted");
    }
}

程式碼部分全部完成。編譯執行~~~

三、總結

通過上面的程式碼可以看到:

  • 洋蔥架構各層間通過介面互相關聯,資料引入是在執行時進行的
  • 應用以區域模型為基礎
  • 所有的外部依賴,如資料集准入和管理調,都是在外部處理
  • 適應性強,設計也方便

總之,從應用來說,洋蔥架構算是個很優秀的架構。以我的經驗,在多個共同開發的專案中,具有比較大的優勢。

本文的相關程式碼,在https://github.com/humornif/Demo-Code/tree/master/0045/demo

微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此宣告和原文連結

相關文章