← 回所有專案

個人練習專案 ‧ 剛入行 · 2021

氣象開放資料排程系統

剛入行時的練習專案:串接中央氣象局開放資料,用 Hangfire 定期抓取、清洗並寫入資料庫,第一次完整走過分層架構、AutoMapper 與三層測試(Repository/Service/整合測試)。

ASP.NET CoreC#HangfireEF CoreAutoMapperMSTestMSSQL
氣象開放資料排程系統

專案概述

剛入行時寫的練習專案,目的不是做出產品,而是紮實地把「分層架構+背景排程」 這套組合走過一遍:串接台灣中央氣象局開放資料 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 傳進去,方便事後追查某次同步是哪個 排程觸發的。

C#
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」「正常回傳兩筆」等情境下的解析 邏輯,完全不需要真的打氣象局 API
  • Training.Hangfire.ServiceTests:針對 DTO 轉換與商業邏輯做單元測試
  • Training.Hangfire.IntegrationTest:用 TestEnvironment.Docker 在 測試時起一個真的 MSSQL 容器,每個測試用 BeginTransaction / RollbackTransaction 包起來,測完自動回滾,驗證「同觀測站舊資料存在時 會先刪除再更新」「不存在時直接新增」這類跟真實資料庫行為有關的邏輯

測試方法命名走 Given-When-Then 的中文敘述,搭配 [TestClass] / [TestMethod] / [TestInitialize] / [TestCleanup] 以及 [Owner]、[TestCategory]、[TestProperty] 這些 MSTest 的屬性標記測試 的負責人與被測方法:

C#
[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 資料庫,更能驗 證交易、關聯資料刪除等真實資料庫才會出現的行為,也是後來重視整合測試的 起點

這個專案後來也成了 訂單開立發票系統 的架構起 點——同一套分層模式,套用到不同的業務情境。