前回のインターフェイスに対する透過プロキシでは、RealProxy クラスを利用してインターフェイスに対する透過プロキシを生成し、
その実装クラスが存在していなくてもメソッドに処理を割り当てることができました。
改めて、透過プロキシ (transparent proxy) の主な特徴を整理すると次のようになります。
今回は MarshalByRefObject を継承したクラスの既存の処理を透過プロキシで拡張して、
アスペクト指向プログラミング (AOP) を実践してみます。
一般的にアプリケーション設計においては、
ログ出力やデータベース トランザクションなど、いろいろなビジネス ロジックに共通する横断的関心事があります。
例えばデータベース トランザクションであれば、以前にファントム リードとその解決方法などで書いている通り、
ビジネス ロジックごとに TransactionScope に関する同じようなコードを繰り返し記述することになります。
この部分をアスペクト (側面) として分離し、属性として記述できるようにすることで再利用性を高めることを目指します。
まず次のコードで示す通り、RealProxy を継承した CrossCuttingProxy クラスと、アスペクトを表す属性を定義します。
|
using System; |
|
using System.Runtime.Remoting.Messaging; |
|
using System.Transactions; |
|
|
|
namespace CrossCuttingConsole |
|
{ |
|
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)] |
|
public abstract class AspectAttribute : Attribute |
|
{ |
|
public abstract IMethodReturnMessage Invoke(Func<IMethodReturnMessage> baseInvoke, MarshalByRefObject target, IMethodCallMessage methodCall); |
|
} |
|
|
|
public class TraceLogAttribute : AspectAttribute |
|
{ |
|
public override IMethodReturnMessage Invoke(Func<IMethodReturnMessage> baseInvoke, MarshalByRefObject target, IMethodCallMessage methodCall) |
|
{ |
|
var methodInfo = $"{methodCall.MethodBase.DeclaringType.Name}.{methodCall.MethodName}({string.Join(", ", methodCall.InArgs)})"; |
|
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: Begin: {methodInfo}"); |
|
|
|
var result = baseInvoke(); |
|
|
|
if (result.Exception == null) |
|
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: Success: {methodInfo}"); |
|
else |
|
{ |
|
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: Error: {methodInfo}"); |
|
Console.WriteLine(result.Exception); |
|
} |
|
|
|
return result; |
|
} |
|
} |
|
|
|
public class TransactionScopeAttribute : AspectAttribute |
|
{ |
|
public TransactionOptions TransactionOptions { get; } |
|
public TransactionScopeOption TransactionScopeOption { get; } |
|
|
|
public TransactionScopeAttribute( |
|
IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, |
|
double timeoutInSeconds = 30, |
|
TransactionScopeOption scopeOption = TransactionScopeOption.Required) |
|
{ |
|
TransactionOptions = new TransactionOptions |
|
{ |
|
IsolationLevel = isolationLevel, |
|
Timeout = TimeSpan.FromSeconds(timeoutInSeconds), |
|
}; |
|
TransactionScopeOption = scopeOption; |
|
} |
|
|
|
public override IMethodReturnMessage Invoke(Func<IMethodReturnMessage> baseInvoke, MarshalByRefObject target, IMethodCallMessage methodCall) |
|
{ |
|
using (var scope = new TransactionScope(TransactionScopeOption, TransactionOptions)) |
|
{ |
|
var result = baseInvoke(); |
|
|
|
scope.Complete(); |
|
|
|
return result; |
|
} |
|
} |
|
} |
|
} |
|
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Reflection; |
|
using System.Runtime.Remoting; |
|
using System.Runtime.Remoting.Activation; |
|
using System.Runtime.Remoting.Messaging; |
|
using System.Runtime.Remoting.Proxies; |
|
|
|
namespace CrossCuttingConsole |
|
{ |
|
public class CrossCuttingProxy : RealProxy |
|
{ |
|
public static T CreateProxy<T>() where T : MarshalByRefObject, new() |
|
{ |
|
return (T)new CrossCuttingProxy(new T()).GetTransparentProxy(); |
|
} |
|
|
|
public MarshalByRefObject Target { get; private set; } |
|
|
|
// For non-ContextBoundObject. |
|
internal CrossCuttingProxy(MarshalByRefObject target) : base(target.GetType()) |
|
{ |
|
Target = target; |
|
} |
|
|
|
// For ContextBoundObject. |
|
internal CrossCuttingProxy(Type classToProxy) : base(classToProxy) |
|
{ |
|
} |
|
|
|
public override IMessage Invoke(IMessage msg) |
|
{ |
|
if (msg is IConstructionCallMessage constructionCall) |
|
return InvokeConstructor(constructionCall); |
|
|
|
if (msg is IMethodCallMessage methodCall) |
|
return InvokeMethod(methodCall); |
|
|
|
throw new InvalidOperationException(); |
|
} |
|
|
|
IConstructionReturnMessage InvokeConstructor(IConstructionCallMessage constructionCall) |
|
{ |
|
var constructionReturn = InitializeServerObject(constructionCall); |
|
Target = GetUnwrappedServer(); |
|
SetStubData(this, Target); |
|
return constructionReturn; |
|
} |
|
|
|
IMethodReturnMessage InvokeMethod(IMethodCallMessage methodCall) |
|
{ |
|
Func<IMethodReturnMessage> baseInvoke = () => RemotingServices.ExecuteMessage(Target, methodCall); |
|
|
|
var newInvoke = methodCall.MethodBase.GetCustomAttributes<AspectAttribute>(true) |
|
.Reverse() |
|
.Aggregate(baseInvoke, (f, a) => () => a.Invoke(f, Target, methodCall)); |
|
|
|
return newInvoke(); |
|
} |
|
} |
|
} |
ログ出力を表す TraceLogAttribute クラスとデータベース トランザクションを表す TransactionScopeAttribute クラスを用意し、
既存の処理の前後に割り込むようにしてそれぞれの処理を追加しています。
以上のようにすれば、ビジネス ロジックを次のように記述するだけで済むようになります。
|
using System; |
|
using System.Transactions; |
|
|
|
namespace CrossCuttingConsole |
|
{ |
|
public class NorthwindBusiness : MarshalByRefObject |
|
{ |
|
[TraceLog] |
|
[TransactionScope] |
|
public short SelectUnitsInStock() |
|
{ |
|
Console.WriteLine("SelectUnitsInStock"); |
|
|
|
// cf. https://sakapon.wordpress.com/2011/10/02/dirtyread2/ |
|
using (var context = new NorthwindEntities()) |
|
{ |
|
var product = context.Products.Single(p => p.ProductID == 1); |
|
return product.UnitsInStock; |
|
} |
|
} |
|
|
|
[TraceLog] |
|
[TransactionScope(IsolationLevel.Serializable)] |
|
public void InsertCategory(string name) |
|
{ |
|
Console.WriteLine("InsertCategory"); |
|
|
|
// cf. https://sakapon.wordpress.com/2011/12/14/phantomread2/ |
|
using (var context = new NorthwindEntities()) |
|
{ |
|
context.AddToCategories(Category.CreateCategory(0, name)); |
|
context.SaveChanges(); |
|
} |
|
} |
|
|
|
public int PropertyTest { get; [TraceLog]set; } |
|
|
|
[TraceLog] |
|
public void ErrorTest() |
|
{ |
|
throw new InvalidOperationException("This is an error test."); |
|
} |
|
} |
|
} |
|
using System; |
|
|
|
namespace CrossCuttingConsole |
|
{ |
|
class Program |
|
{ |
|
static void Main(string[] args) |
|
{ |
|
var nw = CrossCuttingProxy.CreateProxy<NorthwindBusiness>(); |
|
var units = nw.SelectUnitsInStock(); |
|
nw.InsertCategory("Books"); |
|
nw.PropertyTest = 123; |
|
try |
|
{ |
|
nw.ErrorTest(); |
|
} |
|
catch (Exception) |
|
{ |
|
} |
|
} |
|
} |
|
} |
実行結果です。ログが出力されています:

前回:インターフェイスに対する透過プロキシ
次回:透過プロキシでアスペクト指向プログラミング (2)
作成したサンプル
CrossCuttingConsole (GitHub)
バージョン情報
C# 7.0
.NET Framework 4.5
参照
RealProxy クラス
アスペクト指向プログラミング (Wikipedia)
RealProxy クラスによるアスペクト指向プログラミング (MSDN Magazine)