個人練習專案 ‧ 剛入行 · 2021
氣象開放資料排程系統
剛入行時的練習專案:串接中央氣象局開放資料,用 Hangfire 定期抓取、清洗並寫入資料庫,第一次完整走過分層架構、AutoMapper 與三層測試(Repository/Service/整合測試)。
專案概述
剛入行時寫的練習專案,目的不是做出產品,而是紮實地把「分層架構+背景排程」 這套組合走過一遍:串接台灣中央氣象局開放資料 API,抓取各地觀測站的氣象資 料,用 Hangfire 排程定期同步進資料庫。因為是練習專案,程式庫是私有的,這裡 把設計上的重點整理出來。
分層架構
Controller → Service → Repository → UnitOfWork 四層,Repository 用泛型
IRepository<TEntity> 統一封裝 CRUD,UnitOfWork 再統一管理 SaveChanges
與交易邊界,Service 層只依賴介面,不直接碰 DbContext。
排程流程
伺服器啟動時,HangfireJobTrigger.OnStart() 會先清掉既有的
Recurring Job 再重新註冊一次,避免重新部署造成同一個排程被註冊兩次;接著用
兩段式的 Job 串接:先由排程觸發的 CreateJob 建立一筆工作紀錄,再用
BackgroundJob.Enqueue 呼叫實際抓資料的 UpdateMeteorologicalDataAsync,
並把前一個 Job 的 Id 當作 sourceJobId 傳進去,方便事後追查某次同步是哪個
排程觸發的。
public void OnStart()
{
// 清除已有的排程工作,避免重新部署造成排程重複註冊
using (var connection = JobStorage.Current.GetConnection())
{
foreach (var recurringJob in connection.GetRecurringJobs())
RecurringJob.RemoveIfExists(recurringJob.Id);
}
// 每 10 分鐘觸發一次
RecurringJob.AddOrUpdate<IHangfireJobs>(
x => x.CreateJob(null),
cronExpression: "0/10 * * * *",
timeZone: TimeZoneInfo.Local);
}
public async Task UpdateMeteorologicalDataAsync(PerformContext context, string sourceJobId)
{
// 呼叫氣象局 API 取得原始資料
var sourceData = await _meteorologicalOpenDataService.GetSourceDataAsync();
// 清洗、轉換後寫入資料庫(同觀測站的舊資料先刪除再整批寫入)
await _meteorologicalService.UpdateAsync(sourceData);
}
資料轉換(AutoMapper)
氣象局回傳的原始 JSON(Records → Location → WeatherElement /
Parameter)跟系統內部要用的模型長得不一樣,中間用四個 AutoMapper
Profile(ObservatoryInfoDtoProfile、WeatherInfoDtoProfile、
RegionInfoDtoProfile、MeteorologicalDtoProfile)把觀測站資訊、天氣資
訊、地區資訊分開轉換,再由 Service 層組成一個完整的 MeteorologicalDto,
最後才落地成 ObservatoryInfoModel / WeatherInfoModel / RegionInfoModel
三張表。同一個觀測站再次同步時,是先刪除舊的關聯資料再整批寫入新的,而不是
逐欄位比對更新。
測試策略(MSTest)
專案拆成三個測試專案,全部使用 MSTest(MSTest.TestFramework +
MSTest.TestAdapter),搭配 FluentAssertions 斷言、NSubstitute 做替身、
AutoFixture 產生測試資料:
Training.Hangfire.RepositoryTests:用RichardSzalay.MockHttp假造HttpClient的回應,搭配固定的假 JSON(SuccessFalse.json、locationNull.json、TwoLocation.json)驗證 Repository 在「空白回 應」「success為 false」「地點筆數為 0」「正常回傳兩筆」等情境下的解析 邏輯,完全不需要真的打氣象局 APITraining.Hangfire.ServiceTests:針對 DTO 轉換與商業邏輯做單元測試Training.Hangfire.IntegrationTest:用TestEnvironment.Docker在 測試時起一個真的 MSSQL 容器,每個測試用BeginTransaction/RollbackTransaction包起來,測完自動回滾,驗證「同觀測站舊資料存在時 會先刪除再更新」「不存在時直接新增」這類跟真實資料庫行為有關的邏輯
測試方法命名走 Given-When-Then 的中文敘述,搭配 [TestClass] /
[TestMethod] / [TestInitialize] / [TestCleanup] 以及
[Owner]、[TestCategory]、[TestProperty] 這些 MSTest 的屬性標記測試
的負責人與被測方法:
[TestMethod()]
[Owner("G8672")]
[TestCategory(nameof(ObservatoryInfoService))]
[TestProperty(nameof(ObservatoryInfoService), nameof(ObservatoryInfoService.UpdateAsync))]
public async Task UpdateAsync_輸入StationId為10點371770並且資料庫存在此筆資料_觸發刪除並完成更新()
{
// arrange / act / assert
}
學到的東西
- 第一次完整體會「為什麼要把資料存取邏輯抽到 Repository/UnitOfWork」,而不 是把 EF Core 直接寫在 Service 裡
- Hangfire 排程工作的基本用法與常見陷阱(部署時如何避免排程重複註冊、用
sourceJobId串起因果關係) - 用假的 HTTP 回應(
MockHttp+ fixture JSON)驗證會呼叫外部 API 的 Repository,讓測試穩定、不依賴氣象局 API 是否可用 - 用 Docker 容器起一個真的 MSSQL 做整合測試,比起完全 Mock 資料庫,更能驗 證交易、關聯資料刪除等真實資料庫才會出現的行為,也是後來重視整合測試的 起點
這個專案後來也成了 訂單開立發票系統 的架構起 點——同一套分層模式,套用到不同的業務情境。