---
# System prepended metadata

title: 專案目錄

---

# 目錄

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**：適用於整個應用程式生命周期的單一實例。