Published on

ASP.NET Core 使用 MediatR 簡單的實現 Clean Architecture、CQRS 和分層架構

在原本正在運行的專案已經導入 Clean Architecture 的架構和實作,不過在看到了 Clean Architecture with ASP.NET Core 2.1 這個分享後,覺得這個架構的實作比自己寫的還要漂亮,所以慢慢的把這個分享的東西導入到自己的架構裡面

今天要分享的是使用 MediatR 這個套件,可以簡單的做到 CQRS 和架構上的分層

範例使用 ASP.NET Core 2.2 MVC 範本 Repository 在 Github

  • 先建立一個 ASP.NET Core 2.2 MVC 的專案 (TestMediatR),然後再同一個 solution 下面再建立二個 .NETCoreApp v2.2 的 Library (BusinessLogicDataAssert),相關的專案 reference 如下圖
    • TestMediatR -> BusinessLogic -> DataAssert

建置 DataAssert

為了測試方便,在這裡使用的是 InMemory 的 Database,實務上請換成自己使用的 Database 和相關的套件

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
            : base(options)
    {
    }

    public DbSet<Person> Person { get; set; }
}
  • 建立 Person
public class Person
{
    public int Id { get; set; }

    public string Name { get; set; }
}
  • TestMediatR 專案加入 AppDbContext
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options =>
    {
        options.UseInMemoryDatabase("TestMediatR");
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

建置新增 Person 的 BusinessLogic

public class AddPersonCommand : IRequest
{
    public int Id { get; set; }

    public string Name { get; set; }
}
  • 建立 AddPersonCommandHandler,來處理 AddPersonCommand,需要實作 MediatR 的 IRequestHandler,會需要實作一個 Handle 的方法,方法第一個參數就是實作 IRequest 的類別 (第一個泛型參數)
    • 第一個泛型參數是實作 IRequest 的 AddPersonCommand
    • 第二個泛型參數是回傳值,沒有的話就給 Unit
      • Unit 這是 MediatR 的內建型別,給無回傳值時使用
public class AddPersonCommandHandler : IRequestHandler<AddPersonCommand, Unit>
{
    public Task<Unit> Handle(AddPersonCommand request, CancellationToken cancellationToken)
    {
        throw new System.NotImplementedException();
    }
}
  • 實作 AddPersonCommandHandler 的內容,把傳入值轉成 Person,然後加到 DB 裡面,因為要回傳 Unit,可以使用Unit.Value當成回傳值
public class AddPersonCommandHandler : IRequestHandler<AddPersonCommand, Unit>
{
    private readonly AppDbContext _dbContext;

    public AddPersonCommandHandler(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Unit> Handle(AddPersonCommand request, CancellationToken cancellationToken)
    {
        var person = new Person { Id = request.Id, Name = request.Name };
        await _dbContext.Person.AddAsync(person, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);

        return Unit.Value;
    }
}
  • TestMediatR 專案註冊 MediatR
    • 如果是放在不同的專案,可以給那個專案的 Assembly name,同專案的話,基本上就不用加
public void ConfigureServices(IServiceCollection services)
{
    services.AddMediatR(typeof(AddPersonCommand).GetTypeInfo().Assembly);
}

建置 MVC

  • 增加 PersonController,可以在 constructor 的時候注入 IMediator 使用
[Route("api/[controller]/[action]")]
public class PersonController : Controller
{
    private readonly IMediator _mediator;

    public PersonController(IMediator mediator)
    {
        _mediator = mediator;
    }
}
  • 增加 Add Action,傳入參數就是在 BusinessLogic 裡面新增的 AddPersonCommand,然後使用 Mediator 的 Send 方法,把需要處理的物件丟進去 (也就是實作 IRequest 的物件),因為沒有回傳值,就直接 await 它就好
[Route("api/[controller]/[action]")]
public class PersonController : Controller
{
    private readonly IMediator _mediator;

    public PersonController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> Add(AddPersonCommand command)
    {
        await _mediator.Send(command);
        return Ok();
    }
}
  • 實際測試

建置查詢 Person 的 BusinessLogic

  • 建立查詢的回傳物件 PersonQueryResponse
public class PersonQueryResponse
{
    public PersonQueryResponse(Person person)
    {
        Person = person;
    }

    public Person Person { get; private set; }
}
  • 建立 PersonQuery,實作 MediatR 的 IRequest,泛型參數為回傳型別 PersonQueryResponse
public class PersonQuery : IRequest<PersonQueryResponse>
{
    public int Id { get; set; }
}
  • 建立 PersonQueryHandler 處理 PersonQuery
    • 第一個泛型參數是實作 IRequest 的 PersonQuery
    • 第二個泛型參數是回傳值 PersonQueryResponse

注意 : 如果 PersonQuery 的泛型參數和 PersonQueryHandler 第二個泛型參數不一致時會報錯

public class PersonQueryHandler : IRequestHandler<PersonQuery, PersonQueryResponse>
{
    public Task<PersonQueryResponse> Handle(PersonQuery request, CancellationToken cancellationToken)
    {
        throw new System.NotImplementedException();
    }
}
  • 實作 PersonQueryHandler 的內容,從 DB 查詢到資料然後回傳
public class PersonQueryHandler : IRequestHandler<PersonQuery, PersonQueryResponse>
{
    private readonly AppDbContext _dbContext;

    public PersonQueryHandler(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<PersonQueryResponse> Handle(PersonQuery request, CancellationToken cancellationToken)
    {
        var person = await _dbContext.Person.FirstOrDefaultAsync(a => a.Id == request.Id, cancellationToken);

        return new PersonQueryResponse(person);
    }
}

建置查詢 Person 的 Action

  • 建立 Query Action
[HttpPost]
public async Task<IActionResult> Query(PersonQuery query)
{
    return Ok(await _mediator.Send(query));
}
  • 實際測試

後記

原本我們都會寫在 Action 裡面的程式碼,利用 MediatR 這個套件,就可以簡單的作到把程式碼移到另外一個 Library 裡面,也可以從 Request 的 DTO 物件就實現了程式碼的 CQRS,不過如果還要作到影片中的 Clean Architecture 的話,以這個範例還有一段距離,有機會的話在來分享,有興趣的人也建議可以把影片看一次,應該會學到蠻多東西的。