359. Implementing an interception framework – ContextBoundObject

בהמשך לפעם הקודמת, הפעם נראה כיצד ניתן לממש תשתית Interception בעצמנו.

הדרך הראשונה היא באמצעות משהו שנקרא ContextBoundObject. זה מאוד מזכיר את RealProxy (טיפ מספר 348).

שוב, תזכורת:

אנחנו מעוניינים באיזשהו ממשק של הFramework שאם נממש אותו נקבל יכולת Intercept לאובייקטים שלנו.

ContextBoundObject הוא אובייקט שיורש מMarshalByRefObject – נאמר שתי מילים על MarshalByRefObject:

ב.net קיימת אופציה לסרלז אובייקט: להפוך אותו לייצוג בינארי. כך אפשר להעביר אובייקטים בין השאר בין כמה AppDomainים. MarshalByRefObject הוא אובייקט שהCLR יודע לסרלז אותו באופן מיוחד: הוא מסרלז אותו בצורה כך שכאשר הוא עובר לAppDomain אחר, הוא מועבר לפי הReference שלו.

ContextBoudObject מאפשר יותר שליטה על האובייקט בCLR ובעצם מאפשר להתערב בקריאות של הפונקציות של האובייקט שלנו.

אז איך זה עובד?

זה קצת מסובך להשתמש בזה Out of the box, אבל ניתן לעשות זאת עם קצת (או קצת יותר) זיעה. להלן מימוש של LogInterceptor באמצעות ContextBoundObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
[AttributeUsage(AttributeTargets.Class)]
public class LogAttribute : ContextAttribute
{
#region Constructor
public LogAttribute()
: base("Log")
{
}
#endregion
#region ContextAttribute Overrides
public override void Freeze(Context newContext)
{
}
public override void GetPropertiesForNewContext(System.Runtime.Remoting.Activation.IConstructionCallMessage ctorMsg)
{
ctorMsg.ContextProperties.Add(new LogProperty());
}
public override bool IsContextOK(Context ctx, System.Runtime.Remoting.Activation.IConstructionCallMessage ctorMsg)
{
LogProperty property =
ctx.GetProperty("Log") as LogProperty;
if (property == null)
{
return false;
}
return true;
}
public override bool IsNewContextOK(Context newCtx)
{
LogProperty property =
newCtx.GetProperty("Log") as LogProperty;
if (property == null)
{
return false;
}
return true;
}
#endregion
}
public class LogProperty : IContextProperty, IContributeObjectSink
{
#region IContextProperty Members
public string Name
{
get
{
return "Log";
}
}
public bool IsNewContextOK(Context newCtx)
{
LogProperty property =
newCtx.GetProperty("Log") as LogProperty;
if (property == null)
{
return false;
}
return true;
}
public void Freeze(Context newContext)
{
}
#endregion
#region IContributeObjectSink Members
public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
{
return new LogSink(nextSink);
}
#endregion
}
public class LogSink : IMessageSink
{
#region Data Members
private IMessageSink mNextSink;
private ILog mLog = LogManager.GetLogger("MyLogger");
#endregion
#region Constructor
public LogSink(IMessageSink nextSink)
{
this.mNextSink = nextSink;
}
#endregion
#region IMessageSink Members
public IMessage SyncProcessMessage(IMessage msg)
{
IMethodCallMessage call = msg as IMethodCallMessage;
mLog.DebugFormat("Entering {0}, with parameters {1}",
call.MethodBase.Name,
GetParameters(call));
IMethodReturnMessage result =
mNextSink.SyncProcessMessage(msg) as IMethodReturnMessage;
if (result.Exception != null)
{
mLog.Error("An error occured on " + call.MethodBase.Name,
result.Exception);
}
else
{
mLog.DebugFormat("Exiting {0} with return value: {1}",
result.MethodBase,
result.ReturnValue);
}
return result;
}
public IMessageSink NextSink
{
get
{
return this.mNextSink;
}
}
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
{
IMessageCtrl rtnMsgCtrl = mNextSink.AsyncProcessMessage(msg, replySink);
return rtnMsgCtrl;
}
private string GetParameters(IMethodCallMessage call)
{
return string.Join
(",",
call.MethodBase.GetParameters().Select((x, i) =>
new { x.Name, Index = i }).
Select(x => x.Name + ":" + call.Args[x.Index]));
}
#endregion
}

תכלס זה קצת קשה להבין מה קורה כאן:

נתחיל בסוף: מה שבאמת חשוב כאן זאת הפונקציה SyncProcessMessage שמשנה את ההתנהגות לקריאה לפונקציה ומבצעת במקומה התנהגות משלנו.

(מזכירה את Route שתמיד ראינו כשדיברנו על Interception)

כל שאר הסיפור שיש כאן זה מה שצריך לעשות בFramework של ContextBoudObject כדי שתקרא הפונקציה SyncProcessMessage. (למען האמת, אני לא מכיר את זה לעומק, ויכול שיש פה טיפה יתירות)

זה רק הרקע שצריך לעשות כדי לכתוב את הInterceptor. כדי להשתמש בו, זה יותר קל:

נירש מContextBoundObject ונשים את הAttribute שלנו:

1
2
3
4
5
[Log]
public class Bank : ContextBoundObject, IBank
{
// ...
}

עכשיו כשנקרא לפונקציות של האובייקט שלנו, הן תעבורנה דרך הInterceptor 😀:

1
2
Bank myBank = new Bank();
myBank.GetAvailableBudget("My Identity"); // Logs!

כמה הערות:

מן הסתם אפשר לכתוב Framework שיקטין משמעותית את כמות הקוד שיש פה. בצוות שלי עשינו משהו, ואז כתיבת Interceptorים פשוט נראה כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// Logs exceptions thrown out of the method call
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class LogExceptionsAttribute : AspectAttribute
{
private ILogger mLog;
private LogLevel mLevel = LogLevel.Error;
#region AspectAttribute Members
public override void PostProcess(ProcessingContext context)
{
if (context.ReturnMessage.Exception != null)
{
// Initialize the logger first time the method is called.
if (mLog == null)
{
mLog = Infrastructure.GetLayer<ILoggingLayer>().
GetLogger(context.ReturnMessage.MethodBase.DeclaringType.ToString());
}
mLog.Log(mLevel, context.ReturnMessage.Exception);
}
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the log level of the exceptions thrown out of the method
/// </summary>
public LogLevel Level
{
get
{
return mLevel;
}
set
{
mLevel = value;
}
}
#endregion
}

אותו שמים פשוט מעל מתודה שאנחנו רוצים שתכתוב לLog את הExceptionים.


הערה נוספת:

זאת דרך אפשרית לממש Interception. היתרון המובהק שלה שהיא יחסית Built in בFramework.

יש לה די הרבה חסרונות:

  • ContextBoundObject לא נחמד בדיבוג – נראה בWatch ובשאר חלונות הדיבוג הערה ש"מדובר בProxy ולכן אי אפשר לראות אותו בWatch".
  • ContextBoundObject כמו RealProxy וחבריו איטי יותר. אני לא אערוך פה השוואה, אבל אני מניח שמדובר באותם סדרי גודל, אם לא איטי יותר.

כאמור זאת רק דרך אחת לממש Interception. נראה בהמשך עוד דרכים נוספות למימוש ונשווה בין השיטות.

המשך יום עם Context חסום לטובה.

שתף