如何进行懒惰注射的异步初始化

Eri*_*sch 6 c# asynchronous dependency-injection simple-injector

假设我们想要注入一个昂贵的对象(假设它从数据库初始化),所以我们通常会使用某种工厂或Lazy<T>.但是,如果我们将此对象注入使用异步操作方法的MVC或WebApi控制器,我们不希望在初始化Lazy对象时在昂贵的I/O操作上阻止这些方法,这会破坏目的使用异步.

当然,我可以创建一个异步的"initlize"方法,但这违反了许多原则.

以惰性和异步方式访问和初始化注入对象的最佳选择是什么?

Sco*_*ain 7

最简单的方法是让你注入的东西是一个Lazy<Task<T>>,工厂会看起来像是一些东西

private Lazy<Task<Foo>> LazyFooFactory()
{
    return new Lazy<Task<Foo>>(InitFoo);
}

private async Task<Foo> InitFoo()
{
    //Other code as needed
    Foo result = await SomeSlowButAsyncronousCodeToGetFoo();
    //Other code as needed
    return result;
}
Run Code Online (Sandbox Code Playgroud)

用作以下内容

private readonly Lazy<Task<Foo>> _lazyFoo

public SomeClass(Lazy<Task<Foo>> lazyFoo)
{
    _lazyFoo = lazyFoo;
}

public async Task SomeFunc()
{
    //This should return quickly once the task as been started the first call
    // then will use the existing Task for subsequent calls.
    Task<Foo> fooTask = _lazyFoo.Value; 

    //This awaits for SomeSlowButAsyncronousCodeToGetFoo() to finish the first calling
    // then will used the existing result to return immediately for subsequent calls.
    var foo = await fooTask;

    DoStuffWithFoo(foo);
}
Run Code Online (Sandbox Code Playgroud)

SomeSlowButAsyncronousCodeToGetFoo()在第一次调用_lazyFoo.Value和后续调用将使用现有Task.Result值而不重新调用工厂之前,不会调用该函数.


Ste*_*ven 6

作为对象图的一部分并由容器自动连接的所有组件应该非常轻量级创建,因为注入构造函数应该很简单。无论是运行时数据还是创建成本高的一次性数据,都不应该直接注入到作为对象图一部分的组件的构造函数中。异步甚至夸大了这一点,因为构造函数永远不可能是异步的;您不能在构造函数的主体中使用 await。

因此,如果一个组件依赖于一些昂贵的数据来创建,那么这些数据应该在构造函数之外延迟加载。这样,对象图的构建就会变得更快,并且控制器的创建不会被阻止。

因此,作为@ScottChamberlain已经说了,可能是最好的办法是混合Lazy<T>使用Task<T>成为Lazy<Task<T>>。如果将其注入Lazy<Task<T>>到“昂贵组件”的构造函数中(使该组件本身再次轻量级),您可能会获得最佳结果。这有几个明显的好处:

  • 以前昂贵的对象本身变得简单;它不再负责数据的加载。
  • 对象图变得快速且可验证。
  • 将加载策略从延迟加载更改为后台加载变得容易,系统中无需更改任何内容。

作为最后一点的例子,允许在后台加载数据可以简单地完成如下:

Task<ExpensiveData> data = Task.Run(LoadExpensiveData);

container.RegisterSingleton<ISomeClass>(
    new SomeClass(new Lazy<Task<ExpensiveData>>(() => data)));
Run Code Online (Sandbox Code Playgroud)