前情提要
前提情要請參考[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
|
public void ConfigureServices(IServiceCollection services) { services.AddAutoMapper(AppDomain.CurrentDomain.Load("Domain.Profiles")); }
using AutoMapper;
public class DomainJobProfile : Profile { CreateMap<ADomainModel, AEntity>(); }
|
EFCore實作
已經有Mapper可以轉換成最終EFCore使用的Entity物件,EFCore部份就只要AddRange跟SaveChanges即可。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
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
| public interface IDomainJobService<T> where T : BaseModel { bool CanRunOrNot(); void DoProcess(); }
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;
return result; }
public void DoProcess() { var fileName = CustomTypeExtension.GetDescription(typeof(T)); var orderDict = CustomTypeExtension.GetPropertiesWithOrderDescription(typeof(T)); var filePath = Path.Combine(_env.ContentRootPath, "upload", "Downloads", fileName); var modelList = ObjectExtension.GetModelList<T>(filePath, Encoding.GetEncoding("Big5"), orderDict, '='); 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的排程設定,才能完整的讓其整個流程運作起來,那我們就下篇見了。