メソッドチェーンでアスペクト指向プログラミング

透過プロキシでアスペクト指向プログラミング (1) では、ログ出力やデータベース トランザクションなどの横断的関心事を、

public class NorthwindBusiness : MarshalByRefObject
{
    [TraceLog]
    [TransactionScope]
    public short SelectUnitsInStock()
    {
    }
}

のように、属性として記述することができました。
今回は属性を使わずに、LINQ と同様にメソッドチェーンを使って横断的関心事を記述してみます。

まず、戻り値を持つ (Func<TResult> に相当する) 処理を IProxyable<TResult> インターフェイスとして定義して、
後ろにメソッドチェーンを追加することで処理を上書きできるようにします。

using System;
namespace ProxyableConsole
{
public interface IProxyable<TResult>
{
TResult Execute();
}
class BodyProxyable<TResult> : IProxyable<TResult>
{
Func<TResult> execute;
public BodyProxyable(Func<TResult> execute)
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
}
public TResult Execute() => execute();
}
class AspectProxyable<TResult> : IProxyable<TResult>
{
IProxyable<TResult> source;
Func<Func<TResult>, TResult> execute;
public AspectProxyable(IProxyable<TResult> source, Func<Func<TResult>, TResult> execute)
{
this.source = source ?? throw new ArgumentNullException(nameof(source));
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
}
public TResult Execute() => execute(source.Execute);
}
public static class Proxyable
{
public static IProxyable<TResult> Body<TResult>(Func<TResult> execute) => new BodyProxyable<TResult>(execute);
public static IProxyable<TResult> Aspect<TResult>(this IProxyable<TResult> source, Func<Func<TResult>, TResult> execute) => new AspectProxyable<TResult>(source, execute);
}
}
using System;
namespace ProxyableConsole
{
class Program
{
static void Main(string[] args)
{
var result = Proxyable.Body(() =>
{
Console.WriteLine("Body");
return 123;
})
.Aspect(f =>
{
Console.WriteLine("Before");
var r = f();
Console.WriteLine("After");
return r;
})
.Execute();
}
}
}
view raw Program.cs hosted with ❤ by GitHub

これを実行すると出力は次のようになり、元の処理の前後に処理が追加されていることがわかります。

Before
Body
After

次に、戻り値を持たない (Action に相当する) 処理を表す IProxyable インターフェイスを、
IProxyable<TResult> インターフェイスの特別な場合とみなして継承させます。

using System;
namespace ProxyableConsole
{
public interface IProxyable : IProxyable<object>
{
new void Execute();
}
class BodyProxyable : BodyProxyable<object>, IProxyable
{
public BodyProxyable(Action execute) : base(() =>
{
execute?.Invoke();
return null;
})
{
}
void IProxyable.Execute() => Execute();
}
class AspectProxyable : AspectProxyable<object>, IProxyable
{
public AspectProxyable(IProxyable source, Action<Action> execute) : base(source, f =>
{
execute?.Invoke(() => f());
return null;
})
{
}
void IProxyable.Execute() => Execute();
}
public static class Proxyable
{
public static IProxyable<TResult> Body<TResult>(Func<TResult> execute) => new BodyProxyable<TResult>(execute);
public static IProxyable Body(Action execute) => new BodyProxyable(execute);
public static IProxyable<TResult> Aspect<TResult>(this IProxyable<TResult> source, Func<Func<TResult>, TResult> execute) => new AspectProxyable<TResult>(source, execute);
public static IProxyable Aspect(this IProxyable source, Action<Action> execute) => new AspectProxyable(source, execute);
}
}
view raw IProxyable.cs hosted with ❤ by GitHub
using System;
namespace ProxyableConsole
{
class Program
{
static void Main(string[] args)
{
Proxyable.Body(() => Console.WriteLine("Body"))
.Aspect(a =>
{
Console.WriteLine("Before");
a();
Console.WriteLine("After");
})
.Execute();
}
}
}
view raw Program.cs hosted with ❤ by GitHub

あとは、ログ出力やデータベース トランザクションなどの横断的関心事を拡張メソッドとして作成すれば完成です。

using System;
using System.Transactions;
namespace ProxyableConsole
{
class Program
{
static void Main(string[] args)
{
var units = NorthwindBusiness.SelectUnitsInStock();
NorthwindBusiness.InsertCategory("Books");
try
{
NorthwindBusiness.ErrorTest();
}
catch (Exception)
{
}
}
}
public static class NorthwindBusiness
{
public static short SelectUnitsInStock() =>
Proxyable.Body(() =>
{
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;
}
})
.TransactionScope()
.TraceLog()
.Execute();
public static void InsertCategory(string name) =>
Proxyable.Body(() =>
{
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();
}
})
.TransactionScope(IsolationLevel.Serializable)
.TraceLog()
.Execute();
public static void ErrorTest() =>
Proxyable.Body(() => throw new InvalidOperationException("This is an error test."))
.TraceLog()
.Execute();
}
}
view raw Program.cs hosted with ❤ by GitHub
using System;
using System.Runtime.CompilerServices;
using System.Transactions;
namespace ProxyableConsole
{
public static class ProxyableExtension
{
public static IProxyable<TResult> TraceLog<TResult>(this IProxyable<TResult> source, [CallerMemberName]string caller = "") =>
source.Aspect(f =>
{
try
{
Console.WriteLine($"Begin: {caller}");
var result = f();
Console.WriteLine($"Success: {caller}");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"Error: {caller}");
Console.WriteLine(ex);
throw;
}
});
public static IProxyable<TResult> TransactionScope<TResult>(this IProxyable<TResult> source,
IsolationLevel isolationLevel = IsolationLevel.ReadCommitted,
double timeoutInSeconds = 30,
TransactionScopeOption scopeOption = TransactionScopeOption.Required)
{
var transactionOptions = new TransactionOptions
{
IsolationLevel = isolationLevel,
Timeout = TimeSpan.FromSeconds(timeoutInSeconds),
};
return source.Aspect(f =>
{
using (var scope = new TransactionScope(scopeOption, transactionOptions))
{
var result = f();
scope.Complete();
return result;
}
});
}
}
}

実行結果です:

ProxyableConsole

 

使い道としては、

  • .NET で透過プロキシを使いたくないとき (処理速度を上げたい、など)
  • .NET の属性のような仕組みを持たない別のプラットフォーム

などの場合が考えられるでしょう。

前回:透過プロキシでアスペクト指向プログラミング (2)

作成したサンプル
ProxyableConsole (GitHub)

バージョン情報
C# 7.0
.NET Framework 4.5

参照
アスペクト指向プログラミング

カテゴリー: .NET Framework, データベース. タグ: . 1 Comment »

透過プロキシでアスペクト指向プログラミング (2)

前回の記事で、NorthwindBusiness クラスの透過プロキシを生成するときに、

var nw = CrossCuttingProxy.CreateProxy<NorthwindBusiness>();

というコードを記述していましたが、
MarshalByRefObject の代わりに ContextBoundObject クラスを継承させると、通常のコンストラクターを利用することができます。
ただし、クラスに属性を付ける必要があります。
次のように実装します。

using System;
using System.Runtime.Remoting.Proxies;
namespace CrossCuttingConsole
{
public sealed class CrossCuttingAttribute : ProxyAttribute
{
public override MarshalByRefObject CreateInstance(Type serverType)
{
return (MarshalByRefObject)new CrossCuttingProxy(serverType).GetTransparentProxy();
}
}
}
using System;
using System.Transactions;
namespace CrossCuttingConsole
{
[CrossCutting]
public class NorthwindBusiness : ContextBoundObject
{
[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 = new NorthwindBusiness();
var units = nw.SelectUnitsInStock();
nw.InsertCategory("Books");
nw.PropertyTest = 123;
try
{
nw.ErrorTest();
}
catch (Exception)
{
}
}
}
}
view raw Program.cs hosted with ❤ by GitHub

以上で、

var nw = new NorthwindBusiness();

と記述できるようになりました。
なお、上記のコードには現れていませんが、
コンストラクターが呼び出されたときに、CrossCuttingProxy クラスの Invoke メソッドが呼び出されます。

前回:透過プロキシでアスペクト指向プログラミング (1)
次回:メソッドチェーンでアスペクト指向プログラミング

作成したサンプル
CrossCuttingConsole (GitHub)

バージョン情報
C# 7.0
.NET Framework 4.5

参照
RealProxy クラス
アスペクト指向プログラミング (Wikipedia)
.NETでアスペクト指向プログラミング(AOP)

透過プロキシでアスペクト指向プログラミング (1)

前回のインターフェイスに対する透過プロキシでは、RealProxy クラスを利用してインターフェイスに対する透過プロキシを生成し、
その実装クラスが存在していなくてもメソッドに処理を割り当てることができました。

改めて、透過プロキシ (transparent proxy) の主な特徴を整理すると次のようになります。

  • 対象となる型は、MarshalByRefObject を継承したクラス、またはインターフェイス
  • 各メンバーが呼び出されたときの挙動を上書きできる

今回は 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)
{
}
}
}
}
view raw Program.cs hosted with ❤ by GitHub

実行結果です。ログが出力されています:

CrossCuttingConsole

前回:インターフェイスに対する透過プロキシ
次回:透過プロキシでアスペクト指向プログラミング (2)

作成したサンプル
CrossCuttingConsole (GitHub)

バージョン情報
C# 7.0
.NET Framework 4.5

参照
RealProxy クラス
アスペクト指向プログラミング (Wikipedia)
RealProxy クラスによるアスペクト指向プログラミング (MSDN Magazine)