如何通过Func从SingleInstace组件中解析InstancePerLifetimeScope组件?

时间:2018-02-02 17:43:57

标签: autofac

这个想法很简单,适用于其他容器,不限于.Net: 从请求上下文中引用的单例组件引用瞬态组件,后者又引用请求范围的组件(某些UnitOfWork)。

我预计Autofac会在两种情况下解析相同的范围组件: - 当我直接从请求范围请求它时 - 当我通过调用Func<>来请求它时 不幸的是,现实情况有点不同--Autofac将SingleInstance组件粘贴到根范围并解析InstancePerLifetimeScope组件 引入内存泄漏(!!!)的根组件,因为UnitOfWork是一次性的并且被根范围跟踪(尝试使用匹配的Web请求范围只会找不到更具误导性的请求范围)。

现在我想知道这种行为是设计还是漏洞?如果是设计我不确定用例是什么以及为什么它与其他容器不同。

示例如下(包括工作SimpleInjector案例):

namespace AutofacTest
{
using System;
using System.Linq;
using System.Linq.Expressions;

using Autofac;

using NUnit.Framework;

using SimpleInjector;
using SimpleInjector.Lifestyles;

public class SingletonComponent
{
    public Func<TransientComponent> Transient { get; }

    public Func<ScopedComponent> Scoped { get; }

    public SingletonComponent(Func<TransientComponent> transient, Func<ScopedComponent> scoped)
    {
        Transient = transient;
        Scoped = scoped;
    }
}

public class ScopedComponent : IDisposable
{
    public void Dispose()
    {
    }
}

public class TransientComponent
{
    public ScopedComponent Scoped { get; }

    public TransientComponent(ScopedComponent scopedComponent)
    {
        this.Scoped = scopedComponent;
    }
}

class Program
{
    static void Main(string[] args)
    {
        try
        {
            AutofacTest();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        try
        {
            SimpleInjectorTest();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }

   private static void AutofacTest()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<ScopedComponent>().InstancePerLifetimeScope();
        builder.RegisterType<SingletonComponent>().SingleInstance();
        builder.RegisterType<TransientComponent>();

        var container = builder.Build();

        var outerSingleton = container.Resolve<SingletonComponent>();

        using (var scope = container.BeginLifetimeScope())
        {
            var singleton = scope.Resolve<SingletonComponent>();
            Assert.That(outerSingleton, Is.SameAs(singleton));

            var transient = scope.Resolve<TransientComponent>();
            var scoped = scope.Resolve<ScopedComponent>();

            Assert.That(singleton.Transient(), Is.Not.SameAs(transient));
            // this fails
            Assert.That(singleton.Transient().Scoped, Is.SameAs(scoped));
            Assert.That(transient.Scoped, Is.SameAs(scoped));

            Assert.That(singleton.Scoped(), Is.SameAs(scoped)); // this fails
            Assert.That(singleton.Transient(), Is.Not.SameAs(transient));
        }
    }

    private static void SimpleInjectorTest()
    {
        var container = new SimpleInjector.Container();
        container.Options.AllowResolvingFuncFactories();
        container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

        container.Register<ScopedComponent>(Lifestyle.Scoped);
        container.Register<SingletonComponent>(Lifestyle.Singleton);
        container.Register<TransientComponent>(Lifestyle.Transient);
        container.Verify();

        var outerSingleton = container.GetInstance<SingletonComponent>();

        using (var scope = AsyncScopedLifestyle.BeginScope(container))
        {
            var singleton = container.GetInstance<SingletonComponent>();
            Assert.That(outerSingleton, Is.SameAs(singleton));

            var transient = container.GetInstance<TransientComponent>();
            var scoped = container.GetInstance<ScopedComponent>();

            Assert.That(singleton.Transient(), Is.Not.SameAs(transient));
            Assert.That(singleton.Transient().Scoped, Is.SameAs(scoped));
            Assert.That(transient.Scoped, Is.SameAs(scoped));

            Assert.That(singleton.Scoped(), Is.SameAs(scoped));
            Assert.That(singleton.Transient(), Is.Not.SameAs(transient));
        }
    }
}

public static class SimpleInjectorExtensions
{
    public static void AllowResolvingFuncFactories(this ContainerOptions options)
    {
        options.Container.ResolveUnregisteredType += (s, e) =>
            {
                var type = e.UnregisteredServiceType;

                if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(Func<>))
                {
                    return;
                }

                Type serviceType = type.GetGenericArguments().First();

                InstanceProducer registration = options.Container.GetRegistration(serviceType, true);

                Type funcType = typeof(Func<>).MakeGenericType(serviceType);

                var factoryDelegate = Expression.Lambda(funcType, registration.BuildExpression()).Compile();

                e.Register(Expression.Constant(factoryDelegate));
            };
    }
}

}

1 个答案:

答案 0 :(得分:1)

您看到的简短版本不是错误,您只是误解了生命范围和强制依赖的一些细节。

首先,来自A​​utofac文档的几个背景参考文献:

  • Controlling Scope and Lifetime解释了很多关于生命范围和层次结构的工作原理。
  • Captive Dependencies说明为什么通常不应该将每个实例的实例或依赖于实例的依赖实例范围的项目放入单个部分。
  • Disposal讨论了Autofac如何自动处理IDisposable项以及如何选择退出该项。
  • Implicit Relationship Types描述了Owned<T>选择退出时使用的IDisposable关系类型。

这些文档中的一些重要内容会直接影响您的情况:

  • Autofac跟踪IDisposable组件,以便它们可以与生命周期范围一起自动处理。这意味着它将保存对任何已解析的IDisposable对象的引用直到父生命周期范围得到解决。
  • 您可以通过在构造函数中使用IDisposable将组件注册为ExternallyOwned 来选择退出Owned<T>跟踪正在注入的参数。 (而不是IDependency参与Owned<IDependency>。)
  • 单身人士居住在根生命周期范围内。这意味着,只要您解决单身人士,就会从根生命周期范围解析。如果它是IDisposable,它将在根生命周期范围内被跟踪,并且在该根范围(容器本身)被处理之前不会被释放。
  • Func<T>依赖关系与其注入的对象绑定到相同的生命周期范围。如果您有单例,则表示Func<T>将解析事物来自与单例相同的生命周期范围 - 根生存期范围。如果你有一些依赖于实例的内容,Func<T>将被附加到拥有组件所在的任何范围。

知道这一点,你就会明白为什么你的单身人士(从Func<T>开始)不断尝试从根生命周期范围内解决这些问题。您还可以看到为什么会出现内存泄漏情况 - 您尚未选择不处理由Func<T>解决的问题的处理跟踪。

所以问题是,你如何解决它?

选项1:重新设计

一般来说,最好通过Func<T>来反转单身人士与你必须解决的事情之间的关系;或者完全停止使用单例,并将其作为较小的终身范围。

例如,假设您有一些IDatabase服务需要IPerformTransaction才能完成任务。旋转数据库连接的代价很高,因此您可以将其设为单例。你可能会有这样的事情:

public class DatabaseThing : IDatabase
{
  public DatabaseThing(Func<IPerformTransaction> factory) { ... }
  public void DoWork()
  {
    var transaction = this.factory();
    transaction.DoSomethingWithData(this.Data);
  }
}

因此,像昂贵的东西一样,使用Func<T>来生成便宜的东西并使用它。

反转这种关系看起来像这样:

public PerformsTransaction : IPerformTransaction
{
  public PerformsTransaction(IDatabase database) { ... }
  public void DoSomethingWithData()
  {
    this.DoSomething(this.Database.Data);
  }
}

这个想法是你解决了事务的问题,它将单例作为一个依赖。较便宜的物品可以很容易地与儿童一生的范围一起处理(即按要求),但单身人士将保留。

如果可以的话,重新设计会更好,因为即使使用其他选项,您也很难将“每个请求实例”的事情变成单例。 (无论如何,从强制依赖和线程角度来看,这都是一个坏主意。)

选项2:放弃单身人士

如果你不能重新设计,一个好的第二选择就是让单身人士的生命周期......不是单身人士。让它为每个范围的实例或依赖于每个实例,并停止使用Func<T>。让所有内容从子生命周期范围内解决,并在处理范围时进行处置。

我认识到由于各种原因并不总是可行的。但如果 可能,那就是逃避问题的另一种方法。

选项3:使用ExternallyOwned

如果您无法重新设计,可以将单身人士消耗的一次性物品注册为ExternallyOwned

builder.RegisterType<ThingConsumedBySingleton>()
       .As<IConsumedBySingleton>()
       .ExternallyOwned();

这样做会告诉Autofac不跟踪一次性用品。你不会有内存泄漏。您负责自行处理已解决的对象。您还将从根生命周期范围中获取它们,因为单例正在注入Func<T>

public void MethodInsideSingleton()
{
  using(var thing = this.ThingFactory())
  {
    // Do the work you need to and dispose of the
    // resolved item yourself when done.
  }
}

选项4:Owned<T>

如果您不想总是手动处理您正在使用的服务 - 您只想处理单例内部的服务 - 您可以正常注册它但消耗{{ 1}}。然后单例将按预期解决问题,但容器将不会跟踪它以进行处置

Func<Owned<T>>