0%

[DotnetCore]CleanArchitecture手做系列-實作API

前情提要

以上篇[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 =>
{
// 取得登入者資訊Service
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) =>
{
// Insert/Update Process
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產生出來的WeatherForecastsget API,轉成mediatR風格的RequestHandler,這部份就直接參考https://github.com/jasontaylordev/CleanArchitecture產生出來的專案即可,但筆者這邊就跟著這個專案一步步做。

建立Service目錄

主要邏輯是依照業務模組切分其Service擺放地方,再則依照QueriesCommands切分其不同資料夾,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 ConditionsIRequest中宣告的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>>
{
// get API中沒有查詢條件,因此這邊會是空的
}

public class GetWeatherForecastsQueryHandler : IRequestHandler<GetWeatherForecastsQuery, IEnumerable<WeatherForecast>>
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
// 將webapi template產生的API中的查詢語法貼到此Handle方法中
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)
{
// 目前僅使用到mediatR套件,因此這邊將mediartR初始化
services.AddMediatR(Assembly.GetExecutingAssembly());
return services;
}
}

API專案

筆者就照template生成的專案中的宣告方式,先宣告BaseAPIController,並使用該Controller已宣告好的mediatRSender來傳送其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
// 將每一層專案所宣告之DependencyInjecttion.cs靜態方法於此呼叫
// Add services to the container.
builder.Services.AddHttpContextAccessor();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication();

// 將預設套用先註解
//app.UseHttpsRedirection();
//app.UseAuthorization();

實際效果

因筆者這邊照著template範本產生的專案結構將webapi template預設產生的WeatherForecastController重刻一遍,行為上是一樣的

結論

最後依照Clean Architecture的範本結構,mediartR套件的RequestHandler方式,將API層與Service層做解偶,mediatR本身有提供PipelineBehavior,將整合FluentValidatorAuthorization的行為封裝為pipelinebehavior,若無符合條件則直接於尚未進到Application層就做回應,可以想成是dotnet core API層的Middleware,不過是針對Application層的RequestHandler做一個Pipeline的實現,跟Middleware依樣也是依照註冊順序執行其對應的behavior行為,下幾篇會介紹其Pipeline Behavior行為及真正撰寫商業邏輯行為下的實作方式,敬請期待。

參考