# 目錄
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設定

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