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.SybaseConnectionString或Utility.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隱藏而非override,using只呼叫父類別,子類別 DataSet/param 不會被釋放)