[創意料理] 突發奇想的「延遲依賴注入(Lazy Dependency Injection)」

最常見的依賴注入(Dependency Injection)方式,就是從建構式上面,將依賴的服務一一注入,但是實務上多多少少會有一部分的 Instances,在服務被釋放之前都沒有被用到,雖然一般來說,產生 Instance 的成本不大,不過我還是想試一下,能不能將依賴注入這件事移到執行的目標方法裡面,在方法裡面有用到的服務才注入,所以就有了「延遲依賴注入(Lazy Dependency Injection)」這個題目。

我的實驗環境是這樣的,假定我有一個 MyService 的類別,實作 IMyService 這個介面,介面有定義一個 GetMyData() 方法,而 MyData 則是依賴 IMyDataAccess 取得,IMyDataAccess 則是有一個 MyDataAccess 的實作。

public interface IMyService
{
    MyData GetMyData();
}

public interface IMyDataAccess
{
    MyData QuerySingle(int id);
}

public class MyService : IMyService
{
    private readonly IMyDataAccess myDataAccess;

    public MyService(IMyDataAccess myDataAccess)
    {
        this.myDataAccess = myDataAccess;
    }

    public MyData GetMyData()
    {
        return this.myDataAccess.QuerySingle(1);
    }
}

public class MyDataAccess : IMyDataAccess
{
    public MyData QuerySingle(int id)
    {
        return new MyData { Id = id, Name = "Johnny" };
    }
}

public class MyData
{
    public int Id { get; set; }

    public string Name { get; set; }
}

一般從建構式注入服務,就差不多像上面這個樣子,底下我就開始著手把它改成延遲在方法裡面注入。

AOP(Aspect-Oriented Programming)

我想到的方式就是用 AOP 去做,我這邊使用的是 AspectInjector 這套 AOP 框架,建立一個 Aspect,在每一個方法執行之前,把方法裡面需要的服務一一注入,為此我需要一個 DependencyInjectionAspectAttribute 來實作注入的邏輯,還有 DependencyInjectAttribute 來取得方法裡面需要注入哪些服務?以及一個簡易的 ServiceCollection,也避免產生重覆的 Instance。

[Aspect(Scope.PerInstance)]
[Injection(typeof(DependencyInjectionAspectAttribute))]
public class DependencyInjectionAspectAttribute : Attribute
{
    [Advice(Kind.Before, Targets = Target.Method)]
    public void Before([Argument(Source.Metadata)] MethodBase method, [Argument(Source.Instance)] object instance)
    {
        var privateFields = instance.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

        var serviceCollection = privateFields.Single(fi => fi.FieldType == typeof(ServiceCollection)).GetValue(instance) as ServiceCollection;

        foreach (var type in method.GetCustomAttribute<DependencyInjectAttribute>().Types)
        {
            var field = privateFields.Single(fi => fi.FieldType == type);

            var implType = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(s => s.GetTypes())
                .First(p => type.IsAssignableFrom(p) && !p.IsInterface);

            var implInstance = Activator.CreateInstance(implType);

            serviceCollection.Add(type, implInstance);

            field.SetValue(instance, implInstance);
        }
    }
}

public class DependencyInjectAttribute : Attribute
{
    public DependencyInjectAttribute(params Type[] types)
    {
        this.Types = types;
    }

    public Type[] Types { get; }
}

public class ServiceCollection
{
    private readonly ConcurrentDictionary<Type, object> services;

    public ServiceCollection()
    {
        this.services = new ConcurrentDictionary<Type, object>();
    }

    public void Add(Type type, object instance)
    {
        this.services.TryAdd(type, instance);
    }

    public object Get(Type type)
    {
        return this.services.TryGetValue(type, out var instance) ? instance : default;
    }
}

因為只是 POC(Proof of concept),所以暫不考量太多細節,產生 Instance 的過程也是用最簡便的 Reflection 來做,再來就是把做好的 Attribute 放到目標方法上就大功告成了。

public class MyService : IMyService
{
    private readonly ServiceCollection serviceCollection = new ServiceCollection();
    private IMyDataAccess myDataAccess;

    [DependencyInjectionAspect]
    [DependencyInject(typeof(IMyDataAccess))]
    public MyData GetMyData()
    {
        return this.myDataAccess.QuerySingle(1);
    }
}

後續如果需要增加依賴的服務,也只要把服務實作好,加到 DependencyInjectAttribute 上就可以了。

public class MyService : IMyService
{
    private readonly ServiceCollection serviceCollection = new ServiceCollection();
    private IMyDataAccess myDataAccess;
    private IMyIdDataAccess myIdDataAccess;

    [DependencyInjectionAspect]
    [DependencyInject(typeof(IMyDataAccess), typeof(IMyIdDataAccess))]
    public MyData GetMyData()
    {
        var myId = this.myIdDataAccess.GetMyId();

        return this.myDataAccess.QuerySingle(myId);
    }
}

引入 DI Framework

如果我們有用 DI Framework,可以把 ServiceCollection 用 DI Framework 產生的 Container 取代掉,這邊我用 Autofac 來做,並且建立 LifetimeScope,避免重覆產生 Instance。

public class MyService : IMyService
{
    private readonly ILifetimeScope lifetimeScope = AutofacConfig.Container.BeginLifetimeScope();
    private IMyDataAccess myDataAccess;
    private IMyIdDataAccess myIdDataAccess;

    ...
}

[Aspect(Scope.PerInstance)]
[Injection(typeof(DependencyInjectionAspectAttribute))]
public class DependencyInjectionAspectAttribute : Attribute
{
    [Advice(Kind.Before, Targets = Target.Method)]
    public void Before([Argument(Source.Metadata)] MethodBase method, [Argument(Source.Instance)] object instance)
    {
        var privateFields = instance.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

        var lifetimeScope = privateFields.Single(fi => fi.FieldType == typeof(ILifetimeScope)).GetValue(instance) as ILifetimeScope;

        foreach (var type in method.GetCustomAttribute<DependencyInjectAttribute>().Types)
        {
            var field = privateFields.Single(fi => fi.FieldType == type);

            field.SetValue(instance, lifetimeScope.Resolve(type));
        }
    }
}

public class AutofacConfig
{
    public static IContainer Container { get; private set; }

    public static void Register()
    {
        var builder = new ContainerBuilder();

        builder.RegisterType<MyDataAccess>().As<IMyDataAccess>().InstancePerLifetimeScope();
        builder.RegisterType<MyIdDataAccess>().As<IMyIdDataAccess>().InstancePerLifetimeScope();
        builder.RegisterType<MyService>().As<IMyService>();

        Container = builder.Build();
    }
}

POC 就先到這邊,看起來延遲依賴注入(Lazy Dependency Injection)是可行的,後續還有很多可以調整的部分,像是不要用 Reflection 來存取私有欄位,改用 IL Code 來做。

C# 指南 ASP.NET 教學 ASP.NET MVC 指引
Azure SQL Database 教學 SQL Server 教學 Xamarin.Forms 教學