前情提要
前提情要請參考[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物件,處理商業邏輯處理時的物件,跟著筆者設定對應關係吧。
| 12
 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即可。
| 12
 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告一段落,筆者就把幾篇寫好的實作完整程式碼貼出來
| 12
 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的排程設定,才能完整的讓其整個流程運作起來,那我們就下篇見了。