# 目錄 1. [目錄](#目錄) 2. [系統架構設計](#系統架構設計) - [Model 資料模型層](#Model-資料模型層) - [Contract 服務契約層](#Contract-服務契約層) - [Service 服務實作層](#Service-服務實作層) - [API 應用程式介面層](#API-應用程式介面) 3. [注意事項與最佳實踐](#注意事項與最佳實踐) # 系統架構設計 專案的結構主要分為以下幾層 1. Model:資料模型層 3. Contract:服務契約層 4. Service:服務實作層 5. API:應用程式介面 ## Model 資料模型層 ### Data Model 資料模型 實體的類型在這個系統有三種型別 1. Entity:包含唯一識別值、建立資訊、更新資訊和註銷資訊的物件。通常為擁有完整CRUD操作的物件。 2. Identifiable:包含唯一識別值、建立資訊的物件。通常為在新增後不能夠進行異動操作的物件。 3. Snapshot:包含唯一識別值、建立資訊的物件。與Identifiable相似,但Snapshot特指為在某個時刻的資料。 Entity、Identifiable、Snapshot會各自提供一個抽象物件,在定義Data Model的時候可以使用繼承該抽象物件,有特別的物件也可以基於Interface來定義 ``` /// <summary> /// 實體物件的基底類別 /// </summary> public abstract class Entity : IIdentifiable, ICreatable, IUpdatable, IEnableable, IDeletable { public Guid Id { get; private set; } = GuidV7.NewGuid(); public required string CreateAtNo { get; set; } public DateTime CreateDateTime { get; private set; } = DateTime.UtcNow; public string? UpdateAtNo { get; set; } public DateTime? UpdateDateTime { get; set; } public bool IsEnabled { get; set; } = true; public Guid? DeleteKey { get; set; } public void GenerateDeleteKey() { DeleteKey = Guid.NewGuid(); } } ``` 以Site設定表為例,若需求需要SiteName、UTC兩個欄位 則在DataModel中,可以這樣實作 ``` public class SiteSetting : Entity { public required string SiteName { get; set; } public sbyte UTC { get; set; } = 0; } ``` 特別注意每個屬性的可null設定 ![image](https://hackmd.io/_uploads/rJPECJjrT.png) 在VS2022中會有如上的提示,請思考要如何處理Null情況 ### Data Access 資料處理 本專案使用EF Core,並且使用[Fluent API](https://learn.microsoft.com/zh-tw/ef/ef6/modeling/code-first/fluent/types-and-properties)來設定資料表的定義。 以前面的範例來說,已經建立了一個SiteSetting物件 接下來要定義幾個項目 1. [IEntityTypeConfiguration](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.entityframeworkcore.ientitytypeconfiguration-1?view=efcore-9.0) ``` internal class SiteSettingConfigure : EntityConfigure<SiteSetting> { protected override void ConfigureHelper(EntityTypeBuilder<SiteSetting> builder) { builder.HasIndex(x => new { x.SiteName }).IsUnique(); builder.Property(x => x.SiteName) .IsRequired() .HasMaxLength(20) .HasComment("Site名稱"); builder.Property(x => x.UTC) .HasComment("時區,預設為+0"); builder.HasOne(x => x.SiteGroup) .WithMany(x => x.SiteSettings) .HasForeignKey(x => x.SiteGroupId) .IsRequired(false) .OnDelete(DeleteBehavior.NoAction); } } ``` 2. DbContext加入DbSet (如果需要主動存取) ``` public DbSet<SiteSetting> SiteSettings { get; set; } ``` 3. [Seeds](https://learn.microsoft.com/zh-tw/ef/core/modeling/data-seeding) (如果需要資料植入) ### Migration指令 此專案使用[Microsoft.EntityFrameworkCore.Tools](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Tools/8.0.11?_src=template)套件來進行EF Core Migration。 使用步驟如下(VS2022) 1. 工具→Nuget套件工具管理員→套件管理員主控台 2. Add-Migration [name]:新增Migration 3. Update-Database:更新資料庫 Add-Migration時,會依照Snapshot來進行結構的差異比對並產生Migration檔案。 Update-Database時會依照Api專案中Program所設定的DbConnectionString的連線來更新。 其他常用指令可以參考 [EF Migration指令整理 ](https://hackmd.io/@spyua/H12GXw9Ud) ## Contract 服務契約層 此專案的Controller、Server、Unit Testing都會依賴於Contract層 在此會定義DTO、Interface、QueryParam、Validate ### Interface 以Site設定表為例 如果Site設定的需求有:Create、Update、Get、Query、Toggle、Delete 那ISiteSettingService會如以下 ``` /// <summary> /// 定義 Site 設定相關操作的服務介面。 /// 提供新增、更新、查詢、切換狀態和刪除 Site 設定的功能。 /// </summary> public interface ISiteSettingService { /// <summary> /// 新增 Site 設定。 /// </summary> /// <param name="param">包含 Site 設定詳細信息的 DTO。</param> /// <returns>新增 Site 的唯一標識符 (ID)。</returns> Task<IEnumerable<Guid>> CreateSiteAsync(SiteSettingCreationDTO param); /// <summary> /// 更新指定 Site 的設定。 /// </summary> /// <param name="param">包含需要更新的 Site 設定的 DTO。</param> Task UpdateSiteAsync(SiteSettingUpdateDTO param); /// <summary> /// 獲取指定 Site 的詳細設定。 /// </summary> /// <param name="param">包含 Site 唯一標識符的 DTO。</param> /// <returns>包含 Site 設定詳細信息的 DTO。</returns> Task<SiteSettingDetailDTO> GetSiteAsync(EntityKeyDTO param); /// <summary> /// 查詢 Site 設定列表,支持過濾、排序和分頁操作。 /// </summary> /// <param name="param">包含查詢參數的 DTO。</param> /// <returns>包含 Site 設定列表及分頁信息的結果物件。</returns> Task<PaginationResult<SiteSettingEntryDTO>> QuerySitesAsync(SiteSettingQueryParam param); /// <summary> /// 切換指定 Site 的啟用狀態。 /// </summary> /// <param name="param">包含 Site 唯一標識符及新狀態的 DTO。</param> Task ToggleSiteAsync(ToggleEntityDTO param); /// <summary> /// 刪除指定 Site。 /// </summary> /// <param name="param">包含 Site 唯一標識符的 DTO。</param> Task DeleteSiteAsync(EntityKeyDTO param); } ``` ### DTO 在實務的需求上,在大多數的情況下不允許傳出底層的Entity給外面 因此會需要透過[資料傳輸物件(DTO)](https://learn.microsoft.com/zh-tw/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5)包裝完之後在傳入&傳出。 比如說在Get的時候,只傳入ID(以下會將這個情況統一包裝成EntityKeyDTO) Create的時候只需要傳入SiteName和UTC 那就會建立兩個DTO如以下: ``` /// <summary> /// 表示一個通用的實體鍵值傳輸物件 (DTO)。 /// 用於識別特定實體的唯一標識符。 /// </summary> public class EntityKeyDTO : IDto { /// <summary> /// 實體的唯一識別碼 (UUID v7)。 /// 用於標識實體,應遵循 UUID v7 標準。 /// </summary> public Guid Id { get; set; } } ``` ``` /// <summary> /// 表示用於創建 Site 設定的數據傳輸物件。 /// </summary> public class SiteSettingCreationDTO : IDto { /// <summary> /// Site 名稱集合。 /// 參考 DataZone 的 Site 名稱,這些名稱不一定存在於 GlobalMeeting 資料庫中。 /// </summary> public IEnumerable<string> SiteNames { get; set; } = []; /// <summary> /// Site 群組的唯一標識符 (ID)。 /// 如果為空,則該 Site 不隸屬於任何群組。 /// </summary> public Guid? SiteGroupId { get; set; } /// <summary> /// 語系類型。 /// 指定該 Site 的顯示語言或預設語言。 /// </summary> public LangTypeEnum? LangType { get; set; } /// <summary> /// 時區的 UTC 偏移量。 /// 用於指定該 Site 所在的時區。 /// </summary> public sbyte? UTC { get; set; } /// <summary> /// 會議室可預約的開始時間。 /// 指定會議室每天最早的預約時間。 /// </summary> public TimeOnly? RoomStartTime { get; set; } /// <summary> /// 會議室可預約的結束時間。 /// 指定會議室每天最晚的預約時間。 /// </summary> public TimeOnly? RoomEndTime { get; set; } } ``` 在原本的介面中,傳入和傳出的物件就可以使用DTO ``` Task<SiteSettingDetailDTO> GetSiteAsync(EntityKeyDTO param); Task<IEnumerable<Guid>> CreateSiteAsync(SiteSettingCreationDTO param); ``` ## Service 服務實作層 會在此處時做Interface。 在實作時,也可能會依需求注入其他的功能, 以SystemParameterService為例: ``` /// <summary> /// 提供 SiteSetting 相關操作的服務實現類。 /// </summary> public class SiteSettingService : ISiteSettingService { private readonly GlobalWiMeetingDbContext _dbContext; /// <summary> /// 初始化 <see cref="SiteSettingService"/> 類的新實例。 /// </summary> /// <param name="dbContext">資料庫上下文。</param> public SiteSettingService(GlobalWiMeetingDbContext dbContext) { _dbContext = dbContext; } /// <summary> /// 創建新的 Site 設定。 /// </summary> /// <param name="param">包含 Site 設定的 DTO。</param> /// <returns>返回創建成功的 Site ID 列表。</returns> public async Task<IEnumerable<Guid>> CreateSiteAsync(SiteSettingCreationDTO param) { throw new NotImplementedException(); } /// <summary> /// 刪除指定的 Site。 /// </summary> /// <param name="param">包含 Site 唯一標識符的 DTO。</param> public async Task DeleteSiteAsync(EntityKeyDTO param) { throw new NotImplementedException(); } /// <summary> /// 根據 ID 獲取 Site 的詳細信息。 /// </summary> /// <param name="param">包含 Site 唯一標識符的 DTO。</param> /// <returns>返回 Site 的詳細 DTO。</returns> public async Task<SiteSettingDetailDTO> GetSiteAsync(EntityKeyDTO param) { throw new NotImplementedException(); } /// <summary> /// 查詢符合條件的 Site 集合。 /// </summary> /// <param name="param">包含查詢條件的參數。</param> /// <returns>返回分頁結果的 DTO。</returns> public async Task<PaginationResult<SiteSettingEntryDTO>> QuerySitesAsync(SiteSettingQueryParam param) { throw new NotImplementedException(); } /// <summary> /// 啟用或停用指定的 Site。 /// </summary> /// <param name="param">包含 Site 唯一標識符及啟用狀態的 DTO。</param> public async Task ToggleSiteAsync(ToggleEntityDTO param) { throw new NotImplementedException(); } /// <summary> /// 更新指定的 Site 設定。 /// </summary> /// <param name="param">包含更新內容的 DTO。</param> public async Task UpdateSiteAsync(SiteSettingUpdateDTO param) { throw new NotImplementedException(); } /// <summary> /// 獲取 SiteGroup 的對應關係。 /// </summary> /// <param name="siteSettings">需要查詢的 Site 集合。</param> /// <returns>返回 SiteGroup 的字典集合。</returns> private async Task<Dictionary<Guid, string>> GetSiteGroupsAsync(List<SiteSetting> siteSettings) { throw new NotImplementedException(); } /// <summary> /// 將 Site 實體映射為 SiteSettingCollectOptionDTO。 /// </summary> /// <param name="site">Site 實體。</param> /// <param name="siteGroups">SiteGroup 的對應關係。</param> /// <returns>返回對應的 DTO。</returns> private SiteSettingEntryDTO MapToCollectOptionDto(SiteSetting site, Dictionary<Guid, string> siteGroups) { throw new NotImplementedException(); } } ``` 如果想打開Transaction,可參考以下的語法 ``` public async Task ToggleSiteAsync(ToggleEntityDTO param) { using var trans = await _dbContext.Database.BeginTransactionAsync(); await _dbContext.SiteSettings .Where(x => x.Id == param.Id) .ExecuteUpdateAsync(setters => setters.SetProperty(param => param.IsEnabled, param.IsEnable)); await trans.CommitAsync(); } ``` ## API 應用程式介面 這一層主要定義Controller和.Net Pipeline過程中會做的一些行為(如註冊Swagger、設定CORS和設定DI容器等等)。 ### Controller 由於在Contract中已經定義過DTO和Interface了,因此在Controller僅需將功能注入即可。 ``` /// <summary> /// Site設定的控制器 /// </summary> [Route("api/[controller]/[action]")] [ApiController] public class SiteSettingController : ControllerBase { private readonly ISiteSettingService _siteSettingService; /// <summary> /// Site設定的建構式 /// </summary> /// <param name="siteSettingService"></param> /// <param name="createSiteValidator"></param> public SiteSettingController(ISiteSettingService siteSettingService) { _siteSettingService = siteSettingService; } /// <summary> /// 取得單一Site設定 /// </summary> /// <param name="param"></param> /// <returns></returns> [HttpGet] public async Task<SiteSettingDetailDTO> Get([FromQuery] EntityKeyDTO param) { return await _siteSettingService.GetSiteAsync(param); } /// <summary> /// 取得Site設定清單 /// </summary> /// <param name="param"></param> /// <returns></returns> [HttpGet] public async Task<PaginationResult<SiteSettingEntryDTO>> GetAll([FromQuery] SiteSettingQueryParam param) { return await _siteSettingService.QuerySitesAsync(param); } /// <summary> /// 新增Site設定 /// </summary> /// <param name="param"></param> /// <returns></returns> [HttpPost] public async Task<IEnumerable<Guid>> Create(SiteSettingCreationDTO param) { return await _siteSettingService.CreateSiteAsync(param); } /// <summary> /// 更新Site設定 /// </summary> /// <param name="param"></param> [HttpPut] public async Task Update(SiteSettingUpdateDTO param) { await _siteSettingService.UpdateSiteAsync(param); } /// <summary> /// 開關Site設定 /// </summary> /// <param name="param"></param> [HttpPatch] public async Task Toggle(ToggleEntityDTO param) { await _siteSettingService.ToggleSiteAsync(param); } /// <summary> /// 刪除Site設定 /// </summary> /// <param name="param"></param> [HttpDelete] public async Task Delete(EntityKeyDTO param) { await _siteSettingService.DeleteSiteAsync(param); } } ``` ### DI容器 在定義好Interface和實作完Service之後 需要在DI容器註冊Service,讓系統知道Interface對應哪個實例 ``` public static IServiceCollection AddGlobalWiMeetingService(this IServiceCollection services) { // 註冊 SiteSettingService 為 ISiteSettingService 的具體實現 services.AddTransient<ISiteSettingService, SiteSettingService>(); services.AddTransient<ISiteGroupService, SiteGroupService>(); return services; } ``` ## 注意事項與最佳實踐 1. **DTO 的使用** - 所有的外部資料交換應使用 DTO,不直接傳遞實體類型。 2. **Fluent API 的使用** - 優先使用 Fluent API 定義資料表結構,避免混用 Data Annotation。 - 所有資料表的主鍵、索引應明確指定。 3. **Transaction 使用** - 當操作多個資料表時,建議明確使用 Transaction 確保一致性。 - 在需要跨服務的操作中,優先考慮事件驅動或補償模式。 4. **依賴注入** - 合理選擇服務的生命周期: - **Transient**:適用於短暫使用且無需保持狀態的場景。 - **Scoped**:適用於同一個請求範圍內共享的場景(如 DB Context)。 - **Singleton**:適用於整個應用程式生命周期的單一實例。