透過プロキシでアスペクト指向プログラミング (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 を使用する

DLR で名前付き引数を使う

C# で、メソッドに引数として「キーと値のペア」を渡す方法を考えてみます。
例えば HTTP で GET でアクセスするときに URL でクエリ文字列を指定する場合が挙げられます。

よく使われているのは、メソッドの引数に Dictionary や匿名型オブジェクトを渡す方法かと思います。
以下の HttpHelper クラスのように実装します。
なお、WebClient クラスではクエリ文字列 (QueryString プロパティ) は NameValueCollection 型であるため、
受け取った情報を NameValueCollection 型に変換しています。

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Text;
namespace DynamicHttpConsole
{
public static class HttpHelper
{
public static string Get(string uri, object query) =>
query == null ? Get(uri) :
IsPropertiesObject(query) ? Get(uri, ToDictionary(query)) :
Get(uri, new { query });
static bool IsPropertiesObject(object o)
{
if (o == null) return false;
var oType = o.GetType();
return oType.IsClass && oType != typeof(string);
}
static IDictionary<string, object> ToDictionary(object obj) =>
TypeDescriptor.GetProperties(obj)
.Cast<PropertyDescriptor>()
.ToDictionary(p => p.Name, p => p.GetValue(obj));
public static string Get(string uri, IDictionary<string, object> query = null)
{
using (var web = new WebClient { Encoding = Encoding.UTF8 })
{
if (query != null)
web.QueryString = query.ToNameValueCollection();
return web.DownloadString(uri);
}
}
public static NameValueCollection ToNameValueCollection(this IDictionary<string, object> dictionary)
{
var collection = new NameValueCollection();
foreach (var item in dictionary)
collection[item.Key] = item.Value?.ToString();
return collection;
}
}
}
view raw HttpHelper.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
namespace DynamicHttpConsole
{
class Program
{
// http://zip.cgis.biz/
const string Uri_Cgis_Xml = "http://zip.cgis.biz/xml/zip.php";
static void Main(string[] args)
{
// No query
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml));
// Dictionary
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new Dictionary<string, object> { { "zn", "6048301" } }));
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new Dictionary<string, object> { { "zn", "501" }, { "ver", 1 } }));
// Anonymous type
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new { zn = "6050073" }));
Console.WriteLine(HttpHelper.Get(Uri_Cgis_Xml, new { zn = "502", ver = 1 }));
}
}
}
view raw Program.cs hosted with ❤ by GitHub

なお、ここでは題材として CGI’s 郵便番号検索 API を利用しています。

さて、動的言語ランタイム (DLR)名前付き引数を利用して、引数の情報を実行時に解決できないかと考えると、
次のような方法を思いつきます。

dynamic http = new DynamicHttpProxy();
var result = http.Get(Uri_Cgis_Xml, zn: "402", ver: 1);

実際、DynamicObject クラスを継承した DynamicHttpProxy クラスを次のように作れば可能です。

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
namespace DynamicHttpConsole
{
public class DynamicHttpProxy : DynamicObject
{
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
if (binder.Name == "Get")
{
result = Get(binder.CallInfo.ArgumentNames, args);
return true;
}
return base.TryInvokeMember(binder, args, out result);
}
static string Get(ICollection<string> argNames, object[] args)
{
var query = argNames
.Zip(args.Skip(1), (n, v) => new { n, v })
.ToDictionary(_ => _.n, _ => _.v);
return HttpHelper.Get((string)args[0], query);
}
public string Get(string uri, object query = null) => HttpHelper.Get(uri, query);
}
}
view raw DynamicHttpProxy.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
namespace DynamicHttpConsole
{
class Program
{
// http://zip.cgis.biz/
const string Uri_Cgis_Xml = "http://zip.cgis.biz/xml/zip.php";
static void Main(string[] args)
{
dynamic http = new DynamicHttpProxy();
// No query
Console.WriteLine(http.Get(Uri_Cgis_Xml));
// Named arguments
Console.WriteLine(http.Get(Uri_Cgis_Xml, zn: "1510052"));
Console.WriteLine(http.Get(Uri_Cgis_Xml, zn: "402", ver: 1));
}
}
}
view raw Program.cs hosted with ❤ by GitHub

TryInvokeMember メソッドの中で、引数の名前は binder.CallInfo.ArgumentNames で取得できます。
ただし、引数の名前を指定せずに渡された分はここに含まれない (コレクションの長さが変わる) ため注意が必要です。

 

また、C# 7.0 で追加された ValueTuple を利用して、

var result = HttpHelper.Get(Uri_Cgis_Xml, (zn: "6050073"));

とする案もありましたが、

  • 要素が 1 つ以下の場合、タプル リテラルを記述できない
  • コンパイル後はフィールド名が残らないため、実行時に動的に取得できない

という制約により実現できませんでした。

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

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

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

参照
動的言語ランタイムの概要
名前付き引数と省略可能な引数
タプル – C# によるプログラミング入門

エンティティを匿名型で手軽に実装する (2)

前回のエンティティを匿名型で手軽に実装する (1) で作成した EntityType<TEntity> では、
インスタンスを初期化するために毎回 ConstructorInfo を経由していましたが、
少し重い処理のため、回数が多いと処理時間に影響します。

そこで、あらかじめコンストラクターをラムダ式の式ツリーからコンパイルしておくという方法があります。
先にコードを示します。

using System;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace ExpressionsConsole
{
[DebuggerDisplay(@"{typeof(TEntity)}")]
public class EntityType<TEntity>
{
public ConstructorInfo ConstructorInfo { get; private set; }
Func<object[], TEntity> _constructor;
internal EntityType(ConstructorInfo constructorInfo)
{
ConstructorInfo = constructorInfo;
_constructor = CompileConstructor(constructorInfo);
}
public TEntity CreateEntity(params object[] parameters)
{
return _constructor(parameters);
}
static Func<object[], TEntity> CompileConstructor(ConstructorInfo constructorInfo)
{
var parameterInfoes = constructorInfo.GetParameters();
var p = Expression.Parameter(typeof(object[]), "p");
var ctorExp = Expression.New(constructorInfo, parameterInfoes.Select(i => GetParameterValue(p, i)));
// p => new TEntity((int)p[0], (string)p[1])
var ctorLambda = Expression.Lambda<Func<object[], TEntity>>(ctorExp, p);
return ctorLambda.Compile();
}
static Expression GetParameterValue(ParameterExpression p, ParameterInfo info)
{
var p_i = Expression.ArrayIndex(p, Expression.Constant(info.Position));
return Expression.Convert(p_i, info.ParameterType);
}
}
}
view raw EntityType.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ExpressionsConsole
{
class Program
{
static void Main(string[] args)
{
EntityTypeTimeTest();
}
static void EntityTypeTimeTest()
{
var sw = Stopwatch.StartNew();
var PersonType = EntityType.Create(new { Id = 0, Name = "", Birthday = DateTime.MinValue });
Console.WriteLine(sw.Elapsed);
for (var i = 0; i < 1000000; i++)
PersonType.CreateEntity(i, "Person", DateTime.MaxValue);
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
}
}
view raw Program.cs hosted with ❤ by GitHub

取得した ConstructorInfo をもとに、

p => new TEntity((int)p[0], (string)p[1])

のような処理に相当する式ツリーを構築してコンパイルすることで、object[] から TEntity を初期化する関数が得られます。

CreateEntity メソッドの呼び出しを 100 万回実行して計測してみると、手元の環境だと前回のコードでは

  • EntityType の初期化: 0.0006 秒
  • CreateEntity メソッド 100 万回: 0.6 秒

だったのが、今回のコードでは

  • EntityType の初期化: 0.006 秒
  • CreateEntity メソッド 100 万回: 0.06 秒

になりました。

 

前回: エンティティを匿名型で手軽に実装する (1)

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

参照
Expression<TDelegate> クラス