Interceptor(Aspect)
DI によって注入されるオブジェクトの動作に「横断的関心」を組み込む。
主要なものにログ取得、トランザクション処理がある。
Aspect使わない場合
public int Foo(){
logger.LogTrace($"{nameof(Foo)} start");
DoSomething();
using(var transaction = context.Database.BeginTransaction()){
var data = context.Foos.Find(1);
logger.LogDebug($"data id = {data.Id} name = {data.Name}")
data.Name ="updated";
context.SaveChanges();
transaction.Commit();
logger.LogDebug($"{nameof(Foo)} commit")
return 1;
}
}
となって、特にLog出力は処理の本質には関係ない(=Log出力を削除しても処理結果は同じ)のに処理の可読性を下げてしまう。
Aspect使うと
[TransactionMethod]
public int Foo(){
DoSomething();
var data = context.Foos.Find(1);
logger.LogDebug($"data id = {data.Id} name = {data.Name}")
data.Name ="updated";
context.SaveChanges();
transaction.Commit();
return 1;
}
}
で済み、デバッグ用出力以外の「本質に関係ない」コードを除去できる。
他にも、メソッドの前後に必ず実行する処理を挟み込んだり継承によらず処理を書き換えたりできるようになったりする。
DryIoc によるInterceptor
Castle.DynamicProxy.IInterceptor の実装を作る。
// Interceptor 自体のコード
public class TraceInterceptor : IInterceptor
{
ILogger logger = LogManager.GetCurrentClassLogger(); // 例えばNLogに出力
public void Intercept(IInvocation invocation)
{
logger.Trace($"{invocation.Method.Name} start");
invocation.Proceed();
logger.Trace($"{invocation.Method.Name} end");
}
}
Interceptor を適用できるようにする
public static class DryIocInterception
{
public static void Intercept<TService, TInterceptor>(this IRegistrator registrator, object serviceKey = null)
where TInterceptor : class, IInterceptor =>
registrator.Intercept<TInterceptor>(typeof(TService), serviceKey);
public static void Intercept<TInterceptor>(this IRegistrator registrator, Type serviceType, object serviceKey = null)
where TInterceptor : class, IInterceptor =>
registrator.Intercept(serviceType, Parameters.Of.Type(typeof(IInterceptor[]), typeof(TInterceptor[])), serviceKey);
public static void Intercept(this IRegistrator registrator, Type serviceType, Type[] interceptors, object serviceKey = null)
{
registrator.Intercept(
serviceType,
Parameters.Of.Type(
typeof(IInterceptor[]),
request =>
{
var list = new List<IInterceptor>();
foreach(var t in interceptors)
{
list.Add((IInterceptor)request.Container.Resolve(t));
}
return list.ToArray();
}),
serviceKey);
}
public static void Intercept(this IRegistrator registrator,
Type serviceType, ParameterSelector interceptorsParameterSelector, object serviceKey = null)
{
Type proxyType;
if (serviceType.IsInterface())
proxyType = ProxyBuilder.CreateInterfaceProxyTypeWithTargetInterface(
serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
else if (serviceType.IsClass())
proxyType = ProxyBuilder.CreateClassProxyTypeWithTarget(
serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
else
throw new ArgumentException(
$"Intercepted service type {serviceType} is not a supported, cause it is nor a class nor an interface");
registrator.Register(serviceType, proxyType,
made: Made.Of(
pt => pt.PublicConstructors().FindFirst(ctor => ctor.GetParameters().Length != 0),
interceptorsParameterSelector),
setup: Setup.DecoratorOf(useDecorateeReuse: true, decorateeServiceKey: serviceKey));
}
private static DefaultProxyBuilder ProxyBuilder => _proxyBuilder ?? (_proxyBuilder = new DefaultProxyBuilder());
private static DefaultProxyBuilder _proxyBuilder;
public static void Intercept<TInterceptor>(this IRegistrator registrator, Func<Type, bool> predicate)
where TInterceptor : class, IInterceptor
{
foreach (var registration in registrator.GetServiceRegistrations().Where(r => predicate(r.ServiceType)))
{
registrator.Intercept<TInterceptor>(registration.ServiceType);
}
}
}
DIコンテナを構成する
public static IServiceProvider CreateServiceProvider()
{
var services = new ServiceCollection();
services.AddLogging(loggingBuilder => {
loggingBuilder.ClearProviders();
loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
loggingBuilder.AddNLog();
});
var container = new DryIoc.Container(rules => rules.With()).WithDependencyInjectionAdapter(services);
container.Register<TraceInterceptor>(reuse: Reuse.Transient);
container.Register<TraceInterceptorAsync>(reuse: Reuse.Transient);
container.Register<IService, Service>(reuse: Reuse.Scoped);
container.Intercept(typeof(IService), new Type[] { typeof(TraceInterceptor), typeof(TransactionInterceptor) });
return container.BuildServiceProvider();
}
使う
var provider = CreateServiceProvider();
using(var scope = provider.CreateScope())
{
var service = scope.ServiceProvider.GetRequiredService<Services.IService>();
Console.WriteLine(service.DoSomething());
}
その他
AutofacにもUnity Container にもInterceptor注入の仕組みがあるのだけど DryIoc はいろいろと自前でやる必要があったりする。
ここのサンプルでは1サービスに複数のInterceptor を登録できるようにしている。
オリジナルは https://github.com/dadhi/DryIoc/blob/master/test/DryIoc.IssuesTests/Interception/DryIocInterception.cs
追加したのは
public static void Intercept(this IRegistrator registrator, Type serviceType, Type[] interceptors, object serviceKey = null)
{
registrator.Intercept(
serviceType,
Parameters.Of.Type(
typeof(IInterceptor[]),
request =>
{
var list = new List<IInterceptor>();
foreach(var t in interceptors)
{
list.Add((IInterceptor)request.Container.Resolve(t));
}
return list.ToArray();
}),
serviceKey);
}
Parameters.Of.Type
が注入する Interceptor を取り出せればよい。