在資料存取框架開發中,實體集更新追蹤與延遲載入是兩大核心需求。本文展示如何利用 .NET 訊息鏈與 Context 屬性機制,透過 AOP(面向切面程式設計)優雅地解決這兩個問題,實現領域模型與持久化邏輯的徹底解耦。
所有實體類別繼承 ContextBoundObject,進入 .NET 運行時的上下文邊界。自訂 IContextAttribute 與 IContextProperty 可在物件建構時注入 IContributeObjectSink,構建攔截器鏈(Sink Chain)處理方法呼叫訊息(IMessage)。
兩大 AOP 共用相同架構模式:
Attribute (IContextAttribute, IContextProperty)
→ ContextProperty (IContextProperty, IContributeObjectSink)
→ MessageSink (IMessageSink) 核心邏輯
更新實體集時,若不知哪些屬性已修改,全欄位更新效能差;手動在每個 Setter 呼叫 AddUpdateColumn 侵入性強、易遺漏。
internal class EntityListUpdatableSink : IMessageSink
{
private void HandleMessage(IMessage msg, bool isAsync, out IMethodReturnMessage retMsg)
{
retMsg = null;
if (isAsync) { retMsg = (IMethodReturnMessage)_nextSink.SyncProcessMessage(msg); return; }
if (msg is IMethodCallMessage mcm)
{
bool isFill = (bool)_target.GetType().GetProperty("InnerIsFill", ...).GetValue(_target, null);
// 僅攔截 Setter(排除內部狀態屬性)與 FieldSetter
bool isSetter = !isFill && (
(mcm.MethodName.StartsWith("set_") && !ExcludedSetters.Contains(mcm.MethodName)) ||
mcm.MethodName == "FieldSetter"
);
if (isSetter)
{
string propName = mcm.MethodName == "FieldSetter"
? mcm.InArgs[1].ToString()
: mcm.MethodName.Substring(4);
MemberInfo mInfo = Utility.GetMember(_target.GetType(), propName);
// 僅處理標記 [DataColumn] 的屬性
if (mInfo != null && mInfo.IsDefined(typeof(DataColumnAttribute), true))
{
object oldVal = Utility.GetMemberValue(_target, mInfo, propName);
// 先執行原始 Setter
retMsg = (IMethodReturnMessage)_nextSink.SyncProcessMessage(msg);
object newVal = Utility.GetMemberValue(_target, mInfo, propName);
if (!Equals(oldVal, newVal)) // 值真正改變
{
// 反射呼叫內部方法記錄修改欄位
_target.GetType().InvokeMember("AddUpdateColumn",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod,
null, _target, new object[] { propName });
}
return;
}
}
}
retMsg = (IMethodReturnMessage)_nextSink.SyncProcessMessage(msg);
}
}
[EntityListUpdatable]FieldSetter、SetValue 多種賦值方式LazyLoadableAttribute:標記類別,Context 屬性名 "LazyLoadable"LazyLoadableProperty:注入 LazyLoadableSinkLazyLoadableSink:攔截 Getter,依屬性標記動態載入關聯資料| 屬性 | 適用場景 | 關鍵參數 |
|---|---|---|
[SubEntityList] | 一對多集合 | OperationTypes, PrimaryKey, ForeignKey, Condition |
[ReferenceEntity] | 多對一實體 | PrimaryKey, ForeignKey, ReferenceType |
[ReferenceProperty] | 多對一單屬性 | PrimaryKey, ForeignKey, ReferenceType, ReferencePropertyName |
實體集載入時將 DataHelper 類型與連接字串快取為 InnerCache<object[]>,Key 由 DataCacheKeyManager 管理。延遲載入時從快取重建操作實例,避免重複解析配置。
[Serializable]
[DataTable("TCompanyType")]
[LazyLoadable]
[EntityListUpdatable]
public class TCompanyType : DataEntity
{
[DataColumn][PrimaryKey(true)] public int Id;
[DataColumn] public string Name;
[DataColumn] public BaseType BaseType;
[DataColumn] public int CompanyId;
[DataColumn] public bool IsDelete;
[DataColumn][ForeignKey(OperationTypes.Delete, typeof(TCompanyType), "Id")]
public int ParentId;
[DataColumn] public int ChildCount;
[DataColumn] public int Sort;
[SubEntityList(OperationTypes.All, "ID", "CompanyTypeId")]
public TCompanies Companies { get; set; }
[SubEntityList(OperationTypes.All, "ID", "ParentId")]
public TCompanyTypes SubCompanyTypes { get; set; }
}
var da = new ATCompanyType();
da.AccessOptions = AccessOptions.Defined;
var roots = da.Select("ParentId=0"); // 僅載入根節點
foreach (var type in roots)
{
Console.WriteLine(type.Name);
// 首次存取 Companies 觸發延遲載入
foreach (var comp in type.Companies)
Console.WriteLine(" 公司: " + comp.Name);
// 首次存取 SubCompanyTypes 觸發遞迴載入
PrintSubTypes(type.SubCompanyTypes);
}
此雙 AOP 設計實現了:
已知限制:ContextBoundObject 代理開銷、Field 不支援、深度遞迴風險。後續可探索 Roslyn Source Generator 或 IL Weaving 實現編譯期織入,消除運行時效能損耗。