透過プロキシでアスペクト指向プログラミング (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.");
}
}
}
view raw NorthwindBusiness.cs hosted with ❤ by GitHub
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;
}
}
}
}
view raw AspectAttribute.cs hosted with ❤ by GitHub
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();
}
}
}
view raw CrossCuttingProxy.cs hosted with ❤ by GitHub

ログ出力を表す 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.");
}
}
}
view raw NorthwindBusiness.cs hosted with ❤ by GitHub
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)

インターフェイスに対する透過プロキシ

前回の DLR で名前付き引数を使うでは、メソッドに引数として「キーと値のペア」を渡す方法を考え、
動的言語ランタイム (DLR) を利用する方法を紹介しました。
しかし、同じ処理を複数の場所で呼び出す必要があるなど、再利用性を重視する場合は、
なるべく静的にシグネチャを解決する方法がよいでしょう。

そこで今回は、呼び出すサービスをインターフェイスとして定義し、透過プロキシ (transparent proxy) を利用してみます。
先にコードを示します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
namespace TransparentHttpConsole
{
public static class HttpProxy
{
public static IService CreateProxy<IService>()
{
return (IService)new HttpProxy<IService>().GetTransparentProxy();
}
}
public class HttpProxy<IService> : RealProxy
{
public string BaseUri { get; }
public HttpProxy() : base(typeof(IService))
{
var serviceType = typeof(IService);
if (!serviceType.IsInterface) throw new InvalidOperationException("IService must be an interface.");
var baseUriAttribute = serviceType.GetCustomAttribute<BaseUriAttribute>(true);
if (baseUriAttribute == null) throw new InvalidOperationException("IService must have a BaseUriAttribute.");
BaseUri = baseUriAttribute.BaseUri;
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = (IMethodCallMessage)msg;
if (methodCall.MethodBase.DeclaringType == typeof(object))
{
var result =
methodCall.MethodName == "GetType" ? typeof(IService) :
methodCall.MethodBase.Invoke(this, methodCall.InArgs);
return CreateReturnMessage(result, methodCall);
}
var query = Enumerable.Range(0, methodCall.InArgCount)
.ToDictionary(methodCall.GetInArgName, methodCall.GetInArg);
switch (methodCall.MethodName)
{
case "Get":
return CreateReturnMessage(HttpHelper.Get(BaseUri, query), methodCall);
default:
throw new InvalidOperationException($"The method \"{methodCall.MethodName}\" is not available.");
}
}
static IMethodReturnMessage CreateReturnMessage(object returnValue, IMethodCallMessage methodCall)
{
return new ReturnMessage(returnValue, new object[0], 0, methodCall.LogicalCallContext, methodCall);
}
#region Methods of Object class
// GetType method can not be overridden.
public override int GetHashCode() => base.GetHashCode();
public override bool Equals(object obj) => ReferenceEquals(GetTransparentProxy(), obj);
public override string ToString() => typeof(IService).ToString();
#endregion
}
[AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
public sealed class BaseUriAttribute : Attribute
{
public string BaseUri { get; }
public BaseUriAttribute(string baseUri)
{
BaseUri = baseUri;
}
}
}
view raw HttpProxy.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
namespace TransparentHttpConsole
{
class Program
{
static void Main(string[] args)
{
var cgis = HttpProxy.CreateProxy<ICgisService>();
Console.WriteLine(cgis.Get("6048301"));
Console.WriteLine(cgis.Get("501", 1));
}
}
// http://zip.cgis.biz/
[BaseUri("http://zip.cgis.biz/xml/zip.php")]
public interface ICgisService
{
string Get(string zn);
string Get(string zn, int ver);
}
}
view raw Program.cs hosted with ❤ by GitHub

このコードでは、RealProxy クラスを継承した HttpProxy<IService> クラスを作成しています。
RealProxy はプロキシの実体となるものであり、
その外層である透過プロキシを RealProxy.GetTransparentProxy メソッドで取得できます。

一方、利用する側の Program.cs では、ICgisService インターフェイスを定義しておきます。
その透過プロキシを生成すれば、ICgisService インターフェイスを実装するクラスが存在しなくてもメソッドを呼び出すことができ、
実体は HttpProxy<IService> クラスの Invoke メソッドとなります。

また、BaseUriAttribute クラスを定義しており、
呼び出される Web API のベースとなる URI を属性で指定できるようにしています。

WCF における契約プログラミングでは、クライアント側とサーバー側で同一のインターフェイスを利用し、
クライアント側からのアクセスを上記のように透過プロキシで実装する方法があります。
方法 : ChannelFactory を使用する にある通り、ChannelFactory.CreateChannel メソッドで透過プロキシを生成します。

前回:DLR で名前付き引数を使う
次回:透過プロキシでアスペクト指向プログラミング (1)

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

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

参照
RealProxy クラス
方法 : ChannelFactory を使用する