P/PromptTools
Instruction asset / bll-instructions

BLL.instructions.md

從 Google Drive .github/instructions/BLL.instructions.md 匯入

共用指令內容

使用中

---

applyTo: 'App_Code/BLL/**/*.cs'

---

BLL (Business Logic Layer) 開發規範

> 文件版本: 1.0.0

> 最後更新: 2026-01-26

> 適用範圍: App_Code/BLL/**/*.cs

---

1. 概述

1.1 層級定位

BLL (業務邏輯層) 為三層式架構中的中間層,負責所有業務規則的實作與驗證。BLL 是 Presentation Layer (ASPX/ASMX) 與 DAL 之間的唯一橋樑。

1.2 核心職責

| 職責 | 說明 |

|:-----|:-----|

| 業務規則驗證 | 實作所有業務邏輯,如資料驗證、權限檢查、計算規則 |

| 流程協調 | 協調多個 DAL 物件完成複雜業務流程 |

| 資料轉換 | 將 DAL 回傳的 IEnumerable<Dto> 透過 .ToList() 具現化為 List<Dto>;或將 DataSet 映射為強型別 DTO |

| 交易管理 | 管理跨表或跨資料庫的交易一致性 |

| 例外處理 | 統一捕獲並記錄下層例外,回傳安全結果給上層 |

1.3 設計原則

| 原則 | 說明 |

|:-----|:-----|

| 單一職責 | 每個 BLL 類別對應一個業務領域 |

| 高內聚 | 相關業務邏輯集中於同一類別 |

| 低耦合 | 透過 DTO 與其他層溝通,不直接依賴 DataSet 結構 |

| 防禦性程式設計 | 所有輸入參數必須驗證,所有回傳必須安全 |

---

2. 類別結構規範

2.1 命名規則

| 項目 | 規則 | 範例 |

|:-----|:-----|:-----|

| 類別名稱 | Biz{業務領域} | BizEmployee, BizOrder, BizPermission |

| 命名空間 | {專案名稱}.BLL | Template.BLL |

| 方法名稱 | 動詞 + 名詞,描述業務意圖 | Authenticate, GetUserProfile, ValidateOrder |

2.2 標準類別結構


using System;

using System.Collections.Generic;

using System.Data;

using System.IO;

using CHMC;



namespace {ProjectName}.BLL

{

    /// <summary>

    /// [業務領域描述] 業務邏輯類別

    /// </summary>

    public class Biz{EntityName}

    {

        #region ========== 建構子 ==========

        

        /// <summary>

        /// 預設建構子

        /// </summary>

        public Biz{EntityName}()

        { }

        

        #endregion

        

        #region ========== 靜態屬性 ==========

        

        /// <summary>

        /// 關聯的資料表名稱 (供外部參考)

        /// </summary>

        public static String TableName

        {

            get { return Dbo{EntityName}.TableName; }

        }

        

        #endregion

        

        #region ========== 公開方法 ==========

        

        // 業務方法實作於此區塊

        

        #endregion

        

        #region ========== 私有方法 ==========

        

        // 輔助方法實作於此區塊

        

        #endregion

    }

}

---

3. DAL 互動規範

3.1 DAL 回傳型別與 BLL 處理策略

DAL 有兩種回傳模式,BLL 需依據不同模式採用對應的處理方式:

| DAL 回傳模式 | BLL 處理方式 | 適用場景 |

|:------------|:------------|:---------|

| IEnumerable<DtoXxx> (主流) | 呼叫 .ToList() 取得 List<DtoXxx> | 一般業務 CRUD 查詢 |

| DataSet | 自行遍歷 DataRow 做映射或直接操作 | 系統基礎設施查詢 (權限、員工);報表匯出 |

3.2 模式 A — IEnumerable 模式 (主流)

> 當 DAL 回傳 IEnumerable<Dto> 時,BLL 只需呼叫 .ToList() 即可取得完整清單。DTO 的映射已由 DAL 內的 yield return + DTO.GetSingleData(DataRow) 完成。


/// <summary>

/// 查詢連絡記錄

/// </summary>

/// <param name="dto">查詢條件</param>

/// <param name="_ReturnCode">回傳碼</param>

/// <param name="_ReturnMsg">回傳訊息</param>

/// <returns>連絡記錄清單</returns>

public List<DtoContactRecord> RetrieveContactRecord(DtoContactRecord dto,

                                                     ref Int32 _ReturnCode,

                                                     ref String _ReturnMsg)

{

    DboContactRecord _dboContactRecord = null;

    List<DtoContactRecord> lstReturn = null;



    try

    {

        // 1. 實例化 DAL,傳入連線字串

        _dboContactRecord = new DboContactRecord(Utility.SybaseConnectionString);

        

        // 2. 呼叫 DAL 取得 IEnumerable<T>,立即 .ToList() 具現化

        lstReturn = _dboContactRecord.RetrieveContactRecord(dto).ToList();

        

        // 3. (選擇性) 業務後處理

        // 例如:JSON 轉換、計算欄位填充等

        

        _ReturnCode = Enumeration.ReturnCode.SUCCESS;

        _ReturnMsg = Enums.ReturnMsg.Select;

    }

    catch (Exception ex)

    {

        Log.WriteLog("「BizContactRecord.RetrieveContactRecord」發生錯誤,原因:" + ex.Message);

    }

    finally

    {

        if (_dboContactRecord != null)

        {

            _dboContactRecord.Dispose();

            _dboContactRecord = null;

        }

    }



    return lstReturn;

}

3.3 模式 B — DataSet 模式 (基礎設施)

> 當 DAL 回傳 DataSet 時 (例如權限查詢、員工查詢),BLL 需自行遍歷 DataRow 進行映射。


public List<DtoEntity> GetAllEntities()

{

    // 1. 宣告於方法開頭,初始化為 null

    Dbo{EntityName} dboEntity = null;

    List<DtoEntity> result = new List<DtoEntity>();

    DataSet dsData = null;

    

    try

    {

        // 2. 在 try 區塊內實例化,傳入連線字串

        dboEntity = new Dbo{EntityName}(Utility.SybaseConnectionString);

        

        // 3. 執行資料存取操作

        dsData = dboEntity.SelectAll();

        

        // 4. 資料轉換與業務邏輯

        if (dsData != null && dsData.Tables.Count > 0 && dsData.Tables[0].Rows.Count > 0)

        {

            foreach (DataRow row in dsData.Tables[0].Rows)

            {

                result.Add(MapRowToDto(row));

            }

        }

    }

    catch (Exception ex)

    {

        // 5. 例外處理

        Log.WriteLog(ex);

    }

    finally

    {

        // 6. 資源釋放 - 必須在 finally 中執行

        if (dsData != null)

        {

            dsData.Dispose();

            dsData = null;

        }

        

        if (dboEntity != null)

        {

            dboEntity.Dispose();

            dboEntity = null;

        }

    }

    

    return result;

}

3.4 DAL 物件生命週期管理

強制規則:所有 DAL 物件必須遵循以下模式:

1. 在方法開頭宣告並初始化為 null

2. 在 try 區塊內實例化,傳入連線字串

3. 在 finally 區塊呼叫 Dispose() 並設為 null

> ⛔ 嚴重警告:禁止使用 using 語句包覆 DAL 物件

> 技術原因:所有 DAL 子類別的 Dispose() 宣告為 internal new void Dispose()(使用 new 隱藏父類別,而非 override 覆寫)。

> C# 的 using 語句透過 IDisposable 介面呼叫 Dispose(),多型解析時會找到父類別 DbHelperSQL.Dispose()完全跳過子類別自訂的 DataSet / param 清理邏輯,造成記憶體洩漏。

>

> ```csharp

> // ❌ 禁止:using 只呼叫父類別 DbHelperSQL.Dispose(),

> // 子類別的 DataSet/param 清理永遠不執行

> using (DboBooking dal = new DboBooking(_connStr))

> {

> return dal.SelectAll(...);

> }

>

> // ✅ 正確:明確呼叫子類別自訂的 Dispose()

> DboBooking dal = new DboBooking(_connStr);

> try

> {

> return dal.SelectAll(...);

> }

> finally

> {

> if (dal != null)

> {

> dal.Dispose(); // 呼叫 DboBooking.Dispose(),確實釋放 DataSet 與 param

> dal = null;

> }

> }

> ```

3.2 連線字串使用

強制:透過 Utility 類別取得連線字串:


// ✅ 正確:使用 Utility 提供的連線字串

dboEmployee = new DboEmployeeAseClient(Utility.SybaseConnectionString);

dboMember = new DboEmployeeMSSQL(Utility.CHMC1ConnectionString);



// ❌ 禁止:硬編碼連線字串

dboEmployee = new DboEmployee("Data Source=server;...");

---

4. 資料映射規範

4.1 映射策略選擇

| DAL 回傳型別 | 映射方式 | 說明 |

|:------------|:---------|:-----|

| IEnumerable<DtoXxx> | 不需映射 — 直接 .ToList() | DTO 映射已在 DAL 的 yield return + DTO.GetSingleData() 完成 |

| DataSet | BLL 自行 MapRowToDto() 或逐欄讀取 | 系統基礎設施類別 (權限、員工等) 使用此模式 |

> 重要:大多數業務查詢使用 IEnumerable 模式,BLL 不需手動映射。僅在 DAL 回傳 DataSet 時才需要以下映射邏輯。

4.2 DataSet 轉 DTO 標準模式 (僅適用於 DataSet 直通模式)


/// <summary>

/// 將 DataRow 映射為 DTO 物件

/// </summary>

/// <param name="row">資料列</param>

/// <returns>DTO 物件</returns>

private DtoEmployee MapRowToDto(DataRow row)

{

    DtoEmployee dto = new DtoEmployee();

    

    // 字串欄位 - 使用 ToString() 並 Trim()

    dto.EmployeeId = row["employee_id"].ToString().Trim();

    dto.EmployeeName = row["employee_name"].ToString().Trim();

    

    // 可空值欄位 - 必須檢查 DBNull

    if (row["birth_date"] != DBNull.Value)

    {

        dto.BirthDate = Convert.ToDateTime(row["birth_date"]);

    }

    else

    {

        dto.BirthDate = null;

    }

    

    // 數值欄位

    dto.DeptId = Convert.ToInt32(row["dept_id"]);

    

    // 布林欄位

    dto.IsEnabled = Convert.ToBoolean(row["is_enabled"]);

    

    return dto;

}

4.2 DBNull 處理規則

| 欄位類型 | 處理方式 |

|:---------|:---------|

| String | 直接 ToString(),空值自動轉為 "" |

| Int32?, DateTime? | 必須先檢查 DBNull.Value |

| Boolean | 使用 Convert.ToBoolean(),確保資料庫值為 0/1 |


// ✅ 正確的 DBNull 處理

dto.LastLoginDate = row["last_login_date"] == DBNull.Value 

    ? (DateTime?)null 

    : Convert.ToDateTime(row["last_login_date"]);



// ✅ 欄位存在性檢查 (當欄位可能不存在時)

if (row.Table.Columns.Contains("work_title") && row["work_title"] != DBNull.Value)

{

    dto.JobTitle = row["work_title"].ToString().Trim();

}

---

5. 業務邏輯實作規範

5.1 輸入驗證

所有公開方法必須在開頭進行輸入驗證:


/// <summary>

/// 建立新使用者

/// </summary>

/// <param name="newUser">使用者資料</param>

/// <returns>成功回傳 true,失敗回傳 false</returns>

public Boolean CreateUser(DtoUser newUser)

{

    // 驗證 1:Null 檢查

    if (newUser == null)

    {

        return false;

    }

    

    // 驗證 2:必要欄位檢查

    if (String.IsNullOrEmpty(newUser.UserName))

    {

        return false;

    }

    

    if (String.IsNullOrEmpty(newUser.Email))

    {

        return false;

    }

    

    // 驗證 3:格式驗證

    if (!IsValidEmail(newUser.Email))

    {

        return false;

    }

    

    // 驗證 4:業務規則驗證

    if (IsUserNameExists(newUser.UserName))

    {

        return false;

    }

    

    // 通過所有驗證後,執行資料存取

    DboUser dboUser = null;

    try

    {

        dboUser = new DboUser(Utility.SybaseConnectionString);

        return dboUser.Insert(newUser);

    }

    catch (Exception ex)

    {

        Log.WriteLog(ex);

        return false;

    }

    finally

    {

        if (dboUser != null)

        {

            dboUser.Dispose();

            dboUser = null;

        }

    }

}

5.2 複合業務流程

當業務流程涉及多個 DAL 操作時:


/// <summary>

/// 員工身分驗證並取得完整資訊

/// </summary>

public Boolean Authenticate(String userId, String password, out EmpInfo empInfo)

{

    empInfo = null;

    Boolean isAuthenticated = false;

    

    DboEmployeeAseClient dboEmployee = null;

    DataSet dsEmployee = null;

    

    try

    {

        // 步驟 1:身分驗證 (呼叫外部服務)

        isAuthenticated = ValidateCredentials(userId, password);

        

        if (!isAuthenticated)

        {

            return false;

        }

        

        // 步驟 2:取得員工資料

        dboEmployee = new DboEmployeeAseClient(Utility.SybaseConnectionString);

        dsEmployee = dboEmployee.RetrieveByEmp(userId, "1");

        

        // 步驟 3:驗證資料完整性

        if (dsEmployee == null || 

            dsEmployee.Tables.Count == 0 || 

            dsEmployee.Tables[0].Rows.Count != 1)

        {

            return false;

        }

        

        // 步驟 4:組裝結果物件

        DataRow row = dsEmployee.Tables[0].Rows[0];

        empInfo = new EmpInfo();

        empInfo.EmpID = row["employee_id"].ToString().Trim();

        empInfo.EmpName = row["employee_name"].ToString().Trim();

        empInfo.DeptID = row["dept_id"].ToString().Trim();

        empInfo.DeptName = row["subject_desc"].ToString().Trim();

        

        return true;

    }

    catch (Exception ex)

    {

        Log.WriteLog(ex);

        return false;

    }

    finally

    {

        if (dsEmployee != null)

        {

            dsEmployee.Dispose();

            dsEmployee = null;

        }

        

        if (dboEmployee != null)

        {

            dboEmployee.Dispose();

            dboEmployee = null;

        }

    }

}

---

6. 錯誤處理規範

6.1 例外處理原則

| 規則 | 說明 |

|:-----|:-----|

| 必須捕獲 | 所有 DAL 呼叫及外部服務呼叫必須包覆於 try-catch |

| 必須記錄 | 使用 Log.WriteLog(ex) 記錄所有例外 |

| 安全回傳 | 查詢類回傳空集合 new List<Dto>(),異動類回傳 false |

| 禁止吞掉 | 禁止空的 catch 區塊,至少要記錄 |

6.2 錯誤回傳策略


// 查詢方法 - 失敗時回傳空集合

public List<DtoEmployee> GetAllEmployees()

{

    List<DtoEmployee> result = new List<DtoEmployee>();

    

    try

    {

        // ... 業務邏輯

    }

    catch (Exception ex)

    {

        Log.WriteLog(ex);

        // 回傳空集合,而非 null

    }

    

    return result;

}



// 異動方法 - 失敗時回傳 false

public Boolean UpdateEmployee(DtoEmployee employee)

{

    try

    {

        // ... 業務邏輯

        return true;

    }

    catch (Exception ex)

    {

        Log.WriteLog(ex);

        return false;

    }

}



// 查詢單筆 - 失敗時回傳 null

public DtoEmployee GetEmployeeById(String id)

{

    try

    {

        // ... 業務邏輯

    }

    catch (Exception ex)

    {

        Log.WriteLog(ex);

    }

    

    return null;

}

6.3 例外重新包裝 (僅在必要時)


// 當需要向上層提供更明確的錯誤訊息時

catch (Exception ex)

{

    Log.WriteLog(ex);

    throw new Exception(

        String.Format("「{0}.{1}」發生錯誤,原因:{2}", 

            this.GetType().Name, 

            "MethodName", 

            ex.Message), 

        ex);

}

---

7. 禁止事項 (Anti-Patterns)

7.1 🚫 禁止在 BLL 直接存取資料庫


// ❌ 絕對禁止

using (AseConnection conn = new AseConnection(connectionString))

{

    conn.Open();

    // ...

}



// ✅ 必須透過 DAL

DboEmployee dbo = new DboEmployee(Utility.SybaseConnectionString);

DataSet ds = dbo.SelectById(id);

7.2 🚫 禁止直接回傳 DataSet 給 Presentation Layer


// ❌ 禁止:破壞層級隔離

public DataSet GetEmployeeData(String id)

{

    DboEmployee dbo = new DboEmployee(Utility.SybaseConnectionString);

    return dbo.SelectById(id);  // 直接回傳 DataSet

}



// ✅ 正確 (主流):DAL 回傳 IEnumerable<Dto>,BLL 用 .ToList() 轉為 List

public List<DtoEmployee> GetAllEmployees()

{

    DboEmployee dbo = new DboEmployee(Utility.SybaseConnectionString);

    return dbo.RetrieveAll().ToList();

}



// ✅ 正確 (基礎設施):取得 DataSet 後轉換為 DTO

public DtoEmployee GetEmployee(String id)

{

    // ... 取得 DataSet 後手動轉換為 DTO

    return dto;

}

7.3 🚫 禁止使用 using 語句或在 finally 外釋放 DAL 物件

技術背景:DAL 子類別的 Dispose() 宣告為 internal new void Dispose()隱藏父類別,不是 override)。using 透過 IDisposable 介面進行多型解析,只會執行父類別 DbHelperSQL.Dispose(),子類別的 DataSet、param 等資源不會被釋放,造成記憶體洩漏。


// ❌ 禁止:看似安全,實則 DataSet/param 未釋放

using (DboEmployee dbo = new DboEmployee(Utility.SybaseConnectionString))

{

    return dbo.SelectById(id);

}



// ❌ 危險:若中途發生例外,資源不會被釋放

DboEmployee dbo = new DboEmployee(Utility.SybaseConnectionString);

DataSet ds = dbo.SelectById(id);

dbo.Dispose();  // 可能永遠執行不到



// ✅ 正確:在 finally 中明確呼叫子類別的 Dispose()

DboEmployee dbo = null;

try

{

    dbo = new DboEmployee(Utility.SybaseConnectionString);

    return dbo.SelectById(id);

}

finally

{

    if (dbo != null)

    {

        dbo.Dispose();  // 呼叫 DboEmployee.Dispose()(子類別),確實清理 DataSet / param

        dbo = null;

    }

}

---

8. Sybase 編碼處理

8.1 CP850 轉換時機

當從 Sybase 讀取中文資料時,轉換應在 DAL 層 完成。BLL 接收的 DataSet 應已是 Unicode 編碼。

若 DAL 未處理,BLL 需進行轉換:


// 取得資料

dsData = dboEmployee.SelectById(id);



// 如果 DAL 未做編碼轉換,在 BLL 處理

if (dsData != null && dsData.Tables[0].Rows.Count > 0)

{

    String strXml = StringHelper.CP850TransUnicode(dsData.GetXml());

    StringReader sr = new StringReader(strXml);

    dsData = new DataSet();

    dsData.ReadXml(sr);

    sr.Close();

    sr.Dispose();

}

---

9. 程式碼審查檢核表

在提交程式碼前,請確認以下項目:

類別結構

  • [ ] 類別名稱符合 Biz{EntityName} 格式
  • [ ] 使用正確的命名空間 {Project}.BLL
  • [ ] 所有公開方法皆有 XML 註解

DAL 互動

  • [ ] 所有 DAL 物件使用 Utility.SybaseConnectionStringUtility.CHMC1ConnectionString
  • [ ] DAL 物件在 finally 區塊中呼叫 Dispose() 並設為 null
  • [ ] DataSet 在使用完畢後呼叫 Dispose() 並設為 null
  • [ ] IEnumerable 模式使用 .ToList() 具現化,DataSet 模式使用 MapRowToDto() 映射

業務邏輯

  • [ ] 所有輸入參數在使用前進行驗證
  • [ ] 實作必要的業務規則驗證
  • [ ] DataSet 轉 DTO 時正確處理 DBNull

錯誤處理

  • [ ] 所有外部呼叫包覆於 try-catch-finally
  • [ ] catch 區塊使用 Log.WriteLog(ex) 記錄錯誤
  • [ ] 查詢方法失敗時回傳空集合或 null
  • [ ] 異動方法失敗時回傳 false

禁止事項

  • [ ] 無直接資料庫連線程式碼
  • [ ] 無直接回傳 DataSet 給上層
  • [ ] 無在 finally 區塊外釋放資源的程式碼
  • [ ] 無使用 using 語句包覆 DAL 物件(DAL 的 Dispose()new 隱藏而非 overrideusing 只呼叫父類別,子類別 DataSet/param 不會被釋放)

版本紀錄

1 個不可變版本
v1
從 Google Drive 初次匯入
2026/07/28 02:59:44