前情提要 以上篇[DotnetCore]CleanArchitecture手做系列-建立專案 實作完成後,得到一個有整潔架構的方案,筆者在這篇以webapi template
所產生的WeatherForecast API
查詢作業轉成MediatR
風格的架構,筆者主要參考https://github.com/jasontaylordev/CleanArchitecture 專案預設範本所產生的撰寫風格,擷取筆者要的部份,ORM
部份則改用SqlSugarCore
實作資料庫操作行為,此篇以webapi template
所產生的WeatherForecastController
改寫成Clean Architecture
撰寫風格。
內容 每一個專案都有自己所屬的DependencyInjection
檔案,裡面宣告該專案所需要的初始化及注入,於最終端API專案中的Startup中引入各個專案宣告的AddService Extension Method
,滿常見這種宣告方式的,好處是套件引用乾淨,不需要在最終端API專案中引用一堆不是該在API專案中引用的套件,非常的亂,因為各個專案中實作時,還是會引用一次,若要變更套件的版本號,想必,每一個專案都要去同步一次,AddService Extension Method
完美解決這種相依姓,依照專案參考鏈,自動引入套件即可。
Infrastructure專案 這邊要宣告一個DependencyInjection Extension Method
,將ORM套件注入宣告以及,該專案所涉及到的DI
相關宣告
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static class DependencyInjection { public static IServiceCollection AddInfrastructure (this IServiceCollection services, IConfiguration configuration ) { services.AddTransient(provider => { var identityService = provider.GetService<IIdentityService>(); var db = new SqlSugarScope(new ConnectionConfig() { DbType = DbType.SqlServer, ConnectionString = configuration.GetConnectionString("ConnectionId" ), InitKeyType = InitKeyType.Attribute, IsAutoCloseConnection = true , MoreSettings = new ConnMoreSettings() { IsWithNoLockQuery = true } }); db.Aop.DataExecuting = (oldValue, entityInfo) => { if ((entityInfo.OperationType == DataFilterType.InsertByObject || entityInfo.OperationType == DataFilterType.UpdateByObject)) { switch (entityInfo.PropertyName) { case "AuditTime" : entityInfo.SetValue(DateTime.Now); break ; case "AuditUser" : entityInfo.SetValue(identityService.GetUserName()); break ; } } }; return db; }); return services; } }
上述程式碼中,有一段取得provider.GetService<IIdentityService>();
,筆者這邊再貼一下IdentityService
的程式碼,參考即可,因為關係到你的Login
成功後怎麼塞Claims
有關係,筆者這邊這段就不多加詳述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public interface IIdentityService { int GetUserId () ; string GetUserName () ; } public class IdentityService : IIdentityService { private readonly IHttpContextAccessor _context; public IdentityService (IHttpContextAccessor context ) { _context = context; } public int GetUserId () { var nameId = _context?.HttpContext?.User.FindFirst("id" ); return nameId != null ? Convert.ToInt32(nameId.Value) : 0 ; } public string GetUserName () { return _context?.HttpContext?.User.FindFirst("name" )?.Value; } }
Application專案 筆者這邊要來改造webapi template
產生出來的WeatherForecasts
的get API
,轉成mediatR
風格的RequestHandler
,這部份就直接參考https://github.com/jasontaylordev/CleanArchitecture 產生出來的專案即可,但筆者這邊就跟著這個專案一步步做。
建立Service目錄 主要邏輯是依照業務模組切分其Service
擺放地方,再則依照Queries
或Commands
切分其不同資料夾,Service
別當作第三層目錄,裡頭才放對應的RequestHandler
。以WeatherForecasts的get API來說,
業務模組:WeatherForecasts
CQRS: 屬Queries
Service別: GetWeatherForecasts
建立Model 首先將原本放在API
層的WeatherForecast
拉到上步驟建立之目錄中
1 2 3 4 5 6 7 8 9 10 public class WeatherForecast { public DateTime Date { get ; set ; } public int TemperatureC { get ; set ; } public int TemperatureF => 32 + (int )(TemperatureC / 0.5556 ); public string ? Summary { get ; set ; } }
實作Service 接著要來建立相對應的RequestHandler
了,主要邏輯為
實作IRequest
: 主要可以想成是Request Conditions
,IRequest
中宣告的TResponse
則為Request
回應的Response Model
實作IRequestHandler
:主要可以想成是一般Service
層的實作(包含資料庫操作行為),IRequestHandler
中的TRequest
為上方建立的IRequest
實作,TResponse
為上方建立的IRequest
實作中回傳的TResponse
上述敘述有點繞口令,直接看code會清楚許多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class GetWeatherForecastsQuery : IRequest <IEnumerable <WeatherForecast >>{ } public class GetWeatherForecastsQueryHandler : IRequestHandler <GetWeatherForecastsQuery , IEnumerable <WeatherForecast >>{ private static readonly string [] Summaries = new [] { "Freezing" , "Bracing" , "Chilly" , "Cool" , "Mild" , "Warm" , "Balmy" , "Hot" , "Sweltering" , "Scorching" }; public Task<IEnumerable<WeatherForecast>> Handle(GetWeatherForecastsQuery request, CancellationToken cancellationToken) { var rng = new Random(); var vm = Enumerable.Range(1 , 5 ).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20 , 55 ), Summary = Summaries[rng.Next(Summaries.Length)] }); return Task.FromResult(vm); } }
DependencyInjection宣告 最後將所套用的服務初始化宣告,已API專案中引入做備用
1 2 3 4 5 6 7 8 9 public static class DependencyInjection { public static IServiceCollection AddApplication (this IServiceCollection services ) { services.AddMediatR(Assembly.GetExecutingAssembly()); return services; } }
API專案 筆者就照template
生成的專案中的宣告方式,先宣告BaseAPIController,並使用該Controller已宣告好的mediatR
的Sender
來傳送其Request
1 2 3 4 5 6 7 8 9 [AllowAnonymous ] [Route("api/[controller]" ) ] [ApiController ] public class BaseAPIController : ControllerBase { private ISender _mediator = null !; protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService<ISender>(); }
1 2 3 4 5 6 7 8 9 10 [ApiController ] [Route("[controller]" ) ] public class WeatherForecastController : BaseAPIController { [HttpGet ] public async Task<IEnumerable<WeatherForecast>> Get() { return await Mediator.Send(new GetWeatherForecastsQuery()); } }
Domain專案 此次還沒有撰寫到資料庫相關行為以及比較深入的商業邏輯行為,因此目前這個專案尚未使用到,筆者在下幾篇會以範例資料庫來模擬其系統行為,屆時才會有東西會在此專案中宣告
Program.cs 接著已宣告好每一層的注入後,最後於dotnet6 webapi template
所產生的Program.cs
中宣告其引入
1 2 3 4 5 6 7 8 9 builder.Services.AddHttpContextAccessor(); builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddApplication();
實際效果 因筆者這邊照著template
範本產生的專案結構將webapi template
預設產生的WeatherForecastController
重刻一遍,行為上是一樣的
結論 最後依照Clean Architecture
的範本結構,mediartR
套件的RequestHandler
方式,將API
層與Service
層做解偶,mediatR本身有提供PipelineBehavior,將整合FluentValidator
及Authorization
的行為封裝為pipelinebehavior
,若無符合條件則直接於尚未進到Application
層就做回應,可以想成是dotnet core API
層的Middleware
,不過是針對Application
層的RequestHandler
做一個Pipeline
的實現,跟Middleware
依樣也是依照註冊順序執行其對應的behavior
行為,下幾篇會介紹其Pipeline Behavior
行為及真正撰寫商業邏輯行為下的實作方式,敬請期待。
參考