問題:前端修改實體多個屬性後呼叫 Update,若無變更追蹤,需將所有欄位包含在 SQL UPDATE 中,造成不必要開銷。解決方案:AOP 攔截 set_ 方法,自動記錄實際變更的欄位,僅更新異動欄位。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class EntityListUpdatableAttribute : Attribute, IContextAttribute, IContextProperty
{
internal static string PropertyName = "EntityListUpdatable";
public EntityListUpdatableAttribute() { }
string IContextProperty.Name => PropertyName;
void IContextProperty.Freeze(Context newContext) { }
bool IContextProperty.IsNewContextOK(Context newCtx) => true;
void IContextAttribute.GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
{
ctorMsg.ContextProperties.Add(new EntityListUpdatableProperty());
}
bool IContextAttribute.IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
{
return ctx.GetProperty(PropertyName) != null;
}
}
internal class EntityListUpdatableProperty : IContextProperty, IContributeObjectSink
{
void IContextProperty.Freeze(Context newContext) { }
string IContextProperty.Name => EntityListUpdatableAttribute.PropertyName;
bool IContextProperty.IsNewContextOK(Context newCtx)
{
return newCtx.GetProperty(EntityListUpdatableAttribute.PropertyName) is EntityListUpdatableProperty;
}
IMessageSink IContributeObjectSink.GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
{
return new EntityListUpdatableSink(obj, nextSink);
}
}
internal class EntityListUpdatableSink : IMessageSink
{
private readonly IMessageSink _nextSink;
private readonly MarshalByRefObject _target;
private static readonly object SyncRoot = new object();
public EntityListUpdatableSink(MarshalByRefObject target, IMessageSink nextSink)
{
lock (SyncRoot)
{
_target = target;
_nextSink = nextSink;
}
}
public IMessage SyncProcessMessage(IMessage msg)
{
IMethodReturnMessage ret;
HandleMessage(msg, false, out ret);
return ret;
}
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
{
IMethodReturnMessage ret;
HandleMessage(msg, true, out ret);
return _nextSink.AsyncProcessMessage(msg, replySink);
}
public IMessageSink NextSink => _nextSink;
private void HandleMessage(IMessage msg, bool isAsync, out IMethodReturnMessage returnedMessage)
{
returnedMessage = null;
if (!isAsync && msg is IMethodCallMessage mcm)
{
// 判斷是否為屬性 set 方法(排除內部標記屬性)
bool isFill = (bool)_target.GetType()
.GetProperty("InnerIsFill", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(_target, null);
bool isProperty = !isFill &&
((mcm.MethodName.Length > 4 && mcm.MethodName.StartsWith("set_")
&& mcm.MethodName != "set_InnerIsFill"
&& mcm.MethodName != "set_InnerDataState"
&& mcm.MethodName != "set_InnerData")
|| mcm.MethodName == "FieldSetter");
object oldValue = null, newValue = null;
MemberInfo member = null;
string propName = string.Empty;
// 攔截前:取得舊值
if (isProperty)
{
propName = mcm.MethodName == "FieldSetter"
? mcm.InArgs[1].ToString()
: mcm.MethodName.Substring(4); // 移除 "set_"
member = Utility.GetMember(_target.GetType(), propName);
if (member == null) isProperty = false;
else if (member.IsDefined(typeof(DataColumnAttribute), true))
oldValue = Utility.GetMemberValue(_target, member, propName);
}
// 呼叫下一個 Sink(最終執行真實 set 方法)
returnedMessage = (IMethodReturnMessage)_nextSink.SyncProcessMessage(msg);
// 攔截後:比較新值,若異動則記錄
if (isProperty)
{
newValue = Utility.GetMemberValue(_target, member, propName);
if (!Equals(oldValue, newValue))
{
_target.GetType()
.InvokeMember("AddUpdateColumn",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod,
null, _target, new object[] { propName });
}
}
}
else
{
returnedMessage = (IMethodReturnMessage)_nextSink.SyncProcessMessage(msg);
}
}
}
| 機制 | 說明 |
|---|---|
| 同步/非同步分離 | SyncProcessMessage 完整攔截;AsyncProcessMessage 僅記錄不阻塞 |
| 內部標記過濾 | InnerIsFill、InnerDataState、InnerData 不納入追蹤 |
| FieldSetter 支援 | 允許透過通用方法設值,參數含屬性名 |
| DataColumnAttribute 判斷 | 僅追蹤對應資料庫欄位的屬性 |
| 反射呼叫 AddUpdateColumn | 非公開實例方法,記錄異動欄位名 |
| 執行緒安全 | 建構函數鎖定靜態 SyncRoot |
EntityListUpdatableAttribute 標記實體,EntityListUpdatableSink 攔截 set_ 呼叫,透過前後值比較自動記錄異動欄位,最終僅更新必要資料庫欄位。此模式展示 AOP 在橫切關注點(變更追蹤)的強大表達力,但需權衡 ContextBoundObject 效能代價。