0%

[DotnetCore]泛型運用系列-商業邏輯實作篇

前提情要

前提情要請參考[DotnetCore]泛型運用系列-Model設計篇,不過筆者還是在這邊再列一下會用到的技術觀念與套件:

  • AutoMapper:運用於將檔案內容對應的物件轉成EFCore的物件時
  • Generic Class/Method: 不想寫多個Service去處理多個檔案,設計成泛型形式以符合各種檔案類型
  • Attribute: 透過Description Attribute,註記其對應的Comlumn欄位順序
  • Coravel:透過該套件,將寫好的Service掛成排程任務
  • Extension Method:有一些通用的Method,不要落落等塞在一個Service中,因此轉換成可以共用的Extension Method
  • EFCore:資料存取用開發套件

內容

前兩篇提到基礎建設,這篇來實作商業邏輯吧,筆者在覆頌一下欲實作的細節

  • FTP下載檔案:實作細節可參考[DotnetCore]FTP-下載上傳,筆者就不另外說明了
  • 從下載檔案逐筆讀取資料內容:這段使用StreamReader即可
  • 透過Generic Method將資料內容轉換成物件清單:上兩篇中說明過
  • 最後透過AutoMapper Profile設定轉換成EFCore物件
  • 透過EFCore存進資料庫中

前三項已解釋過,此篇就以最後兩項為主說明其實作細節

Mapper Profile設定及注入

上一篇裡做完前置作業,已經取得Model List,到這邊會有疑問為什麼不直接在最終的EFCore Entity上套上Column順序設定值就好,這樣就不用透過AutoMapper多轉一手。

第一個原因是筆者工作環境中所使用的EFCore模式為DBFirst的形式,那些Entities是透過efcore scaffold指令產生出來的,因自動產生出來的Class就不應該再手動去編輯。

第二個原因是之所以有ViewModel或者比較多人所稱呼的DTO物件,有其必要性,可以自由地套上顯示層級或者Model Binding層級上需要處理的設定,例如若從API接到的Model來說,可能會透過自定義的JsonConverter轉成特定格式,就像此系列中的實作邏輯,需透過Description Attribute套上Column順序設定值。

以上述兩個理由,足夠理由建立額外的ViewModel物件,處理商業邏輯處理時的物件,跟著筆者設定對應關係吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// 以上省略
// Domain.Profiles為Profile檔案放置之專案名稱
services.AddAutoMapper(AppDomain.CurrentDomain.Load("Domain.Profiles"));
// 以下省略
}

// dotnet add package AutoMapper
// DomainJobProfile.cs
using AutoMapper;

public class DomainJobProfile : Profile
{
// 筆者這邊假設ViewModel物件與Entity物件Property Name及資料型別皆一致
CreateMap<ADomainModel, AEntity>();
}

EFCore實作

已經有Mapper可以轉換成最終EFCore使用的Entity物件,EFCore部份就只要AddRangeSaveChanges即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注入取得IMapper(_mapper)、DBContext(_db)
// private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
var aEntityList= _mapper.Map<List<AEntity>>(modelList);
var existsDataOrNot = _db.AEntities
.Any(x => x.DataDate == aEntityList.FirstOrDefault().DataDate
&& x.EventCode == aEntityList.FirstOrDefault().EventCode);
if (existsDataOrNot)
{
_logger.Error($"[DomainJobService:DoProcess] 對應資料已存在!");
return;
}
_db.AEntities.AddRange(aEntityList);
_db.SaveChanges();

Service完整程式碼

到這邊實作Service告一段落,筆者就把幾篇寫好的實作完整程式碼貼出來

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// IDomainJobService
public interface IDomainJobService<T>
where T : BaseModel
{
bool CanRunOrNot();
void DoProcess();
}

// DomainJobService
public class DomainJobService<T> : IDomainJobService<T>
where T : BaseModel
{
private readonly IFtpService _ftpService;
private readonly FtpSetting _ftpSetting;
private readonly IHostEnvironment _env;
private readonly DBContext _db;
private readonly IMapper _mapper;
private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
public DomainJobService(IFtpService ftpService
, IOptions<FtpSetting> options, IHostEnvironment env
, DBContext db, IMapper mapper)
{
_ftpService = ftpService;
_ftpSetting = options.Value;
_env = env;
_db = db;
_mapper = mapper;
}

public bool CanRunOrNot()
{
var result = true;

// TODO: Call Sql Function for check, always return true for test

return result;
}

public void DoProcess()
{
// Step0: Data Initial
var fileName = CustomTypeExtension.GetDescription(typeof(T));
var orderDict = CustomTypeExtension.GetPropertiesWithOrderDescription(typeof(T));
// Step1: Download Ftp Service
//_ftpService.DownloadData(_ftpSetting, new List<string>() { fileName });
var filePath = Path.Combine(_env.ContentRootPath, "upload", "Downloads", fileName);
var modelList = ObjectExtension.GetModelList<T>(filePath, Encoding.GetEncoding("Big5"), orderDict, '=');
// Step3: Add Range:AEntity
var aEntityList= _mapper.Map<List<AEntity>>(modelList);
var existsDataOrNot = _db.AEntities
.Any(x => x.DataDate == aEntityList.FirstOrDefault().DataDate
&& x.EventCode == aEntityList.FirstOrDefault().EventCode);
if (existsDataOrNot)
{
_logger.Error($"[DomainJobService:DoProcess] 對應資料已存在!");
return;
}
_db.AEntities.AddRange(aEntityList);
_db.SaveChanges();
}
}

上述中CanRunOrNot()部份要解釋一下,筆者工作環境金融業內部系統,基本上都會牽扯到營業日這件事,但營業日的定義有一定的運算邏輯,通常是撇除假日後的日期,若排程的設定若要照著營業日走,CronExpression是無法描述的,因此筆者這邊設計是每日跑,但每日排程觸發時,會呼叫一個Sql Function確認是否需要跑排程,然而於該Sql Function中撰寫營業日的判斷,回傳是否當日需要跑排程。

結論

到此篇該實作的皆實作完畢,預告一下下一篇將這些實作透過Startup中設定注入至DI Container中,也包含Coravel的排程設定,才能完整的讓其整個流程運作起來,那我們就下篇見了。