代码之家  ›  专栏  ›  技术社区  ›  Vladimir Kovalyuk

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

  •  2
  • Vladimir Kovalyuk  · 技术社区  · 7 年前

    这个想法很简单,可以在其他容器中使用,不受限制。净值: 从请求上下文中引用的单例组件引用了瞬态组件,而瞬态组件又引用了请求范围内的组件(某些工作单元)。

    我预计Autofac将在这两种情况下解析相同的作用域组件: -当我直接从请求范围请求时 -当我通过调用Func请求时(<)&燃气轮机; 不幸的是,实际情况有点不同-Autofac将SingleInstance组件粘贴到根范围,并在上解析InstancePerLifetimeScope组件 导致内存泄漏的根组件(!!!)因为UnitOfWork是一次性的,并且由根作用域跟踪(尝试使用匹配的web请求作用域只会找不到请求作用域,这更容易引起误解)。

    现在我想知道这样的行为是出于设计还是仅仅是一个bug?如果它是设计的,我不确定它的用例是什么,以及为什么它与其他容器不同。

    示例如下(包括正在使用的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 回复  |  直到 7 年前
        1
  •  3
  •   Travis Illig    7 年前

    您所看到的简短版本并不是一个bug,您只是误解了生命周期范围和捕获依赖关系的一些细节。

    首先,Autofac文档中的一些背景参考:

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

    • Autofac轨道 IDisposable可识别 组件,以便它们可以随生存期范围一起自动处置。 这意味着它会 保留对任何已解决问题的引用 IDisposable可识别 物体 直到解决父生存期范围。
    • 您可以选择退出 IDisposable可识别 跟踪 通过将组件注册为 ExternallyOwned 通过使用 拥有(<T> 在正在注入的构造函数参数中。(而不是接受 IDependency 接受 Owned<IDependency> .)
    • 单例生存在根生存期范围内。 这意味着无论何时解析单例,它都将从根生存期范围解析。如果是的话 IDisposable可识别 它将在根生存期范围内进行跟踪,直到该根范围(容器本身)被释放为止。
    • 这个 Func<T> 依赖关系与注入它的对象绑定到相同的生存期范围。 如果你有一个单身汉,那意味着 函数(&L);T> 将从与singleton相同的生存期范围(根生存期范围)解决问题。如果您有每个依赖项的实例,那么 函数(&L);T> 将附加到所属组件所在的任何范围。

    知道了这一点,你就能明白为什么你的单身汉 函数(&L);T> ,不断尝试从根生存期范围解决这些问题。您还可以看到为什么会出现内存泄漏的情况—您还没有选择退出正在解决的问题的处置跟踪 函数(&L);T> .

    所以问题是,你如何修复它?

    选项1:重新设计

    一般来说,最好将单件和您必须通过 函数(&L);T> ; 或者完全停止使用单例,让它成为更小的生存期范围。

    例如,假设你有一些 IDatabase 需要 IPerformTransaction 完成事情。数据库连接的启动成本很高,因此您可以将其设置为单例连接。然后,您可能会遇到类似的情况:

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

    所以,就像 昂贵的 要加速,请使用 函数(&L);T> 在飞行中生成便宜的东西并使用它。

    反转该关系如下所示:

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

    这样做的想法是,您将解决事务问题,并将单例作为依赖项。较便宜的项目可以很容易地与子生命周期范围一起处理(即,每个请求),但单件将保留。

    如果可以的话,最好重新设计,因为即使使用其他选项,您也很难将“每个请求的实例”之类的东西放入一个单独的实例中。(从约束依赖和线程的角度来看,这都是一个坏主意。)

    选项2:放弃单身

    如果你不能重新设计,一个好的第二选择是让单身汉的一生。。。不要成为单身汉。让它成为每个作用域的实例或每个依赖项的实例,并停止使用 函数(&L);T> . 让所有内容都从子生存期范围中得到解决,并在处理该范围时进行处理。

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

    选项3:使用 外部所有权

    如果你不能重新设计,你可以将单身者消费的一次性物品注册为 外部所有权 .

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

    这样做会告诉Autofac不要跟踪一次性产品。你不会有内存泄漏。你 负责自行处理已解析的对象。您仍然可以从根生存期范围中获取它们,因为单例将获得 函数(&L);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: 拥有(<T>

    如果你不想 总是 手动处置您正在使用的服务-您只想在单例中处理该服务-您可以将其注册为普通服务,但使用 Func<Owned<T>> . 然后singleton将按预期解决问题,但是 容器无法跟踪其进行处理 .

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