这个想法很简单,适用于其他容器,不限于.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));
};
}
}
}
答案 0 :(得分:1)
您看到的简短版本不是错误,您只是误解了生命范围和强制依赖的一些细节。
首先,来自Autofac文档的几个背景参考文献:
IDisposable
项以及如何选择退出该项。Owned<T>
选择退出时使用的IDisposable
关系类型。这些文档中的一些重要内容会直接影响您的情况:
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>>