【问题标题】:How to deal with run-time parameters when using lifetime scoping?使用生命周期范围时如何处理运行时参数?
【发布时间】:2012-03-09 04:37:34
【问题描述】:

警告,前面的帖子很长。

我最近一直在思考这个问题,我正在努力在这里找到一个令人满意的解决方案。我将使用 C# 和 autofac 作为示例。

问题

IoC 非常适合构建大型无状态服务树。我解析服务并将数据仅传递给方法调用。太好了。

有时,我想将数据参数传递给服务的构造函数。这就是工厂的用途。我没有解析服务,而是解析了它的工厂并使用参数调用 create 方法来获取我的服务。工作量不大,但还可以。

有时,我希望我的服务在一定范围内解析到同一个实例。 Autofac 提供了InstancePerLifeTimeScope(),非常方便。它允许我始终解析到执行子树中的同一个实例。很好。

有时我想将这两种方法结合起来。我想要构造函数中的数据参数并有实例范围。我还没有找到令人满意的方法来实现这一点。

解决方案

1。初始化方法

不用将数据传递给构造函数,只需将其传递给Initialize 方法。

界面:

interface IMyService
{
    void Initialize(Data data);
    void DoStuff();
}

类:

class MyService : IMyService
{
    private Data mData;
    public void Initialize(Data data)
    {
        mData = data;
    }

    public void DoStuff()
    {
        //...
    }
}

注册:

builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();

用法:

var myService = context.Resolve<IMyService>();
myService.Init(data);

// somewhere else
var myService = context.Resolve<IMyService>();

在第一次解析服务并调用 Initialize 后,我可以愉快地在相同的上下文中解析并获得相同的初始化实例。我不喜欢在调用Initialize 之前我有一个无法使用的对象。在我调用 Initialize() 之前,存在实例将被解析并在其他地方使用的危险。

2。支架图案

这是一种包含对数据对象的引用的模式,而不是注入数据对象本身,而是注入持有者对象。

界面:

interface IMyService
{
    void DoStuff();
}

类:

class MyService : IMyService
{
    private Data mData;
    public MyService(IDataHolder dataHolder)
    {
        mData = dataHolder.Data;
    }

    public void DoStuff()
    {
        //...
    }
}

注册:

builder.RegisterType<MyService>().As<IMyService>();
builder.RegisterType<DataHolder>().As<IDataHolder>().InstancePerLifetimeScope();

用法:

var holder = context.Resolve<IDataHolder>();
holder.Data = data;

// somewhere else
var myService = context.Resolve<IMyService>();

当我将保存实例的责任转移到不同的类时,这会好一些。我现在也可以在其他服务中使用该持有人。另一个优点是我可以在必要时热交换持有人中的数据。我不喜欢它混淆代码并添加另一个我必须在测试期间模拟的接口。

3。让容器持有实例

界面:

interface IMyService
{
    void DoStuff();
}

类:

class MyService : IMyService
{
    private Data mData;
    public MyService(Data data)
    {
        mData = dataHolder.Data;
    }

    public void DoStuff()
    {
        //...
    }
}

注册:

builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();

用法:

var myServiceFactory = context.Resolve<Func<Data, IMyService>>();
myServiceFactory(data);

// somewhere else
var myService = context.Resolve<IMyService>();

没错。我不会在任何地方存储工厂调用的结果,因为 autofac 会为我存储它。这对于任何会阅读代码的人来说都是非常令人惊讶的。我不确定 autofac 是否甚至打算像这样使用。这样做的好处是我既不需要额外的初始化方法,也不需要额外的类来保存实例。

问题

您对此有何看法?您如何处理运行时数据参数和生命周期范围的情况?我错过了更好的方法吗?

【问题讨论】:

  • 这是一个绝妙的问题,正是我需要找出的问题。太可惜了,它没有得到这么少的关注。
  • 我加了一个赏金。让我们看看3年后是否有帮助。这个问题是在我广泛使用 Autofac 作用域的 WPF 应用程序的上下文中。在 MVC/WebAPI 中,这个问题不会出现,因为事物的本质往往更加无状态。
  • 据我在 Autofac 文档中看到的。 InstancePerLifetimeScope 需要一个 BeginLifetimeScope() 调用才能工作。你的例子不完整吗?请提供至少 2 个作用域的完整代码块,用于了解您如何使用 autofact 来查看您是否遗漏了什么。
  • 好问题!您是否考虑过将服务和数据完全拆分?我的意思是从服务中删除数据(并使其完全无状态)并在服务中的每个操作上传递相关数据?

标签: c# dependency-injection inversion-of-control autofac object-lifetime


【解决方案1】:

Autofac 现在通过对生命周期范围的扩展支持开箱即用。 BeginLifetimeScope() 方法有一个重载,它采用 Action&lt;ContainerBuilder&gt;,允许添加特定于该生命周期范围的新注册。因此,对于给定的示例,它看起来像:

var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
var container = builder.Build();

using(var scope = container.BeginLifetimeScope(
  builder =>
  {
    builder.RegisterInstance(new Data(....));
  }))
{
  // References to 'IMyService' will always be resolved to the same instance within this lifetime scop
  // References to 'Data' will be resolved to the instance registered just for this lifetime scope.
  var svc = scope.Resolve<IMyService>();
}

【讨论】:

  • 这就是我最后所做的。
【解决方案2】:

大多数时候,运行时数据是您需要在任何进程中传递的非静态信息,例如数学函数中的x,因此处理它的最简单方法是在函数中使用参数:

class MyService : IMyService
{
    public MyService(){}

    public void DoStuff(Data mData)
    {
        //...
    }
}

var myService = context.Resolve<IMyService>();
myService.DoStuff(data);

但是,假设您的示例只是一个示例,并且您之所以问是因为您的类需要保留运行时数据以运行更多进程,并且您不想在每个函数中传递相同的参数:

1.- 如果您不放松每个 Resolve 中的运行时数据范围,您可以使用 TypedParameter 解决:

Ej:

//initilization
var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
var container = builder.Build();

//any point of your app
Data mData = new Data("runtimeData"); // must to be accesible in every place you Resolve

using(var scope = container.BeginLifetimeScope())
{

  var service = scope.Resolve<IMyService>(new TypedParameter(typeof(Data), mData));
service.DoStuff();
}

using(var scope = container.BeginLifetimeScope())
{

  var service2 = scope.Resolve<IMyService>(new TypedParameter(typeof(Data), mData));
service2.DoStuff();
}

2.- 如果您在解析的每个地方都没有对运行时数据的引用,您可以RegisterInstance 何时何地创建运行时数据。由于Direct Depency Policy,Autofac 应该对mData 实例进行识别

//initilization
var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
var container = builder.Build();

//where you create or modify runtime data. When runtime data changes you have to update the container again.
var mData = new Data("runtimeData");
updatedBuilder= new ContainerBuilder();
updatedBuilder.RegisterInstance(mData).As<Data>
updatedBuilder.Update(builder);

//in any point of your app
using(var scope = updatedBuilder.BeginLifetimeScope())
    {

      var service = scope.Resolve<IMyService>();
    service.DoStuff();
    }

//in any other point of your app
    using(var scope = updatedBuilder.BeginLifetimeScope())
    {

      var service2 = scope.Resolve<IMyService>();
    service2.DoStuff();
    }

【讨论】:

    【解决方案3】:

    许多 IoC 框架支持注册工厂函数(或 lambda 表达式),该函数将容器/作用域/解析上下文本身的实例作为其参数之一。

    这允许使用额外的间接级别,以及使用唯一标识上下文或范围的信息。此外,许多还提供挂钩,例如事件处理程序或从生命周期范围类派生的选项,以与正在启动或结束的范围进行交互。

    原则

    对于 AutoFac 和您的具体示例,以下原则将起作用,在注册中使用额外的间接级别。

    // Inject `Data` instance resolved from current scope.
    builder.Register<IMyService>(ctx => new MyService(ctx.Resolve<Data>()));
    
    // Extra level of indirection, get a "factory" for a 'Data' instance.
    builder.Register<Data>(ctx => ctx.Resolve<Func<Data>>()()).InstancePerLifetimeScope();
    
    // The indirection resolves to a map of scopes to "factory" functions. 
    builder.Register<Func<Data>>(ScopedDataExtensions.GetFactory);
    

    我们可以使用上下文/作用域上任何可用的唯一属性来构造这个映射。

    // Maps scopes to data "factories".
    public static class ScopedDataExtensions
    {
        private static readonly ConcurrentDictionary<object, Func<Data>> _factories = new ConcurrentDictionary<object, Fund<Data>>();
    
        public static Func<Data> GetFactory(this IComponentContext ctx) 
        {
            var factory = default(Func<Data>);
            return _factories.TryGetValue(ctx.ComponentRegistry, out factory) ? factory : () => null;
        }
        public static void SetFactory(this ILifetimeScope scope, Func<Data> factory)
        {
            _factories[scope.ComponentRegistry] = factory;
        }
    }
    

    我们可以像这样使用它来提供“本地”数据实例以注入到我们的作用域服务实例中。

    var myData = new Data("nested");
    nestedScope.SetFactory(() => myData);
    // ...
    var myService = nestedScope.Resolve<IMyService>();
    

    下面是 AutoFac 的更完整和通用的示例。

    此模式的通用扩展类

    public static class AutofacScopeExtensions
    {
        // Map from context => factories per type
        public static readonly ConcurrentDictionary<object, ConcurrentDictionary<Type, object>> _factories =
            new ConcurrentDictionary<object, ConcurrentDictionary<Type, object>>();
    
        private static class ScopedFactoryFor<T>
        {
            public static Func<T> DefaultFactory = () => default(T);
            public static Func<T> GetFactory(ConcurrentDictionary<Type, object> fromContext)
            {
                object factory;
                return (fromContext.TryGetValue(typeof(T), out factory)) ? (Func<T>)factory : DefaultFactory;
            }
        }
    
        public static IRegistrationBuilder<T, SimpleActivatorData, SingleRegistrationStyle> 
            WithContextFactoryFor<T>(this ContainerBuilder builder, Func<T> defaultFactory = null)
        {
            if (defaultFactory != null)
                ScopedFactoryFor<T>.DefaultFactory = defaultFactory;
            builder.Register<Func<T>>(AutofacScopeExtensions.GetFactory<T>);
            return builder.Register<T>(ctx => ctx.Resolve<Func<T>>()());
        }
        public static IContainer BuildContainer(this ContainerBuilder builder)
        {
            var container = builder.Build();
            container.ChildLifetimeScopeBeginning += OnScopeStarting;
            return container;
        }
        public static ILifetimeScope SetScopeFactory<T>(this ILifetimeScope scope, Func<T> factory)
        {
            ScopeMapFor(scope)[typeof(T)] = factory;
            return scope;
        }
        public static ILifetimeScope SetScopeValue<T>(this ILifetimeScope scope, T instance)
        {
            return SetScopeFactory(scope, () => instance);
        }
        public static Func<T> GetFactory<T>(IComponentContext ctx)
        {
            return ScopedFactoryFor<T>.GetFactory(ScopeMapFor(ctx));
        }
    
        private static ConcurrentDictionary<Type, object> ScopeMapFor(IComponentContext ctx)
        {
            return _factories.GetOrAdd(ctx.ComponentRegistry, x => new ConcurrentDictionary<Type, object>());
        }
        private static void OnScopeStarting(object sender, LifetimeScopeBeginningEventArgs evt)
        {
            evt.LifetimeScope.ChildLifetimeScopeBeginning += OnScopeStarting;
            evt.LifetimeScope.CurrentScopeEnding += OnScopeEnding; // so we can do clean up.
        }
        private static void OnScopeEnding(object sender, LifetimeScopeEndingEventArgs evt)
        {
            var map = default(ConcurrentDictionary<Type, object>);
            if (_factories.TryRemove(evt.LifetimeScope.ComponentRegistry, out map))
                map.Clear();
        }
    }
    

    允许使用以下语法进行注册:

    builder.WithContextFactoryFor<Data>(() => new Data("Default")).InstancePerLifetimeScope();
    builder.Register<IMyService>(ctx => new MyService(ctx.Resolve<Data>()));
    

    然后像这样解决:

    // ...
    var myData = new Data("Some scope");
    // ...
    context.SetScopeFactory(() => myData);
    
    // ...
    
    // Will inject 'myData' instance.
    var myService = context.Resolve<IMyService>();
    

    更简单的选择

    如果您显式启动嵌套作用域,并且在您这样做时,您知道如何创建作用域 Data 实例,则可以跳过扩展类并在创建时将“工厂”委托注册到嵌套作用域它:

    var nestedScope = container.BeginLifetimeScope(
        "L2", 
        x => x.RegisterInstance<Func<Data>>(() => new Data("nested")));
    

    【讨论】:

    • 但是 OP 正在询问运行时数据;即用户输入数据。该数据在Register时间是未知的,应该在Resolve时间注入或者在resolve之前用数据更新容器。
    • @jlvaquero,我明白了。这意味着需要一些额外的工作来解决这个问题,而无需更改服务或数据类本身,也不需要任何特殊的Resolve 参数。相应地更新了答案。
    • 为什么要避免解决参数使事情复杂化?有时需要解决争论,这对我来说没问题。我错过了什么吗?
    • 使用这种模式的主要原因是允许使用作用域依赖注入(即它将解决已设置的依赖项),而不是依赖于服务定位器样式模式,在这种模式中您需要引用范围,并使用显式的Resolve 调用。如果这些是您不关心的事情并且范围是“手动”创建的(而不是像 Web 请求那样自动创建),那么最简单的方法是在构建子实例时注册范围数据实例,如“更简单的替代方案”示例。
    • 我明白了。我缺少像 ASP.NET MVC 那样的 DI 集成。但是在那种情况下,你怎么能在 MVC 执行 context.Resolve&lt;IMyController&gt;(); 并且服务被注入到控制器之前执行 context.SetScopeFactory(() =&gt; myData); 等等。你知道,整个依赖图。
    【解决方案4】:

    如果我理解正确,您希望通过将对象创建委托给容器同时将一些参数传递给其构造函数来使用工厂。

    这是在 Castle Windsor 中使用typed factory facility 实现的。

    我们要解析的示例类:

    public interface IMyService
    {
        void Do();
    }
    
    public class MyService : IMyService
    {
        private readonly Data _data;
        private readonly IDependency _dependency;
    
        public MyService(Data data, IDependency dependency)
        {
            _data = data;
            _dependency = dependency;
        }
    
        public void Do()
        {
            throw new System.NotImplementedException();
        }
    }
    
    public class Data
    {    
    }
    
    public interface IDependency
    {         
    }
    
    public class Dependency : IDependency
    {
    }
    

    我们创建一个工厂接口:

    public interface IMyServiceFactory
    {
        IMyService Create(Data data);
        void Release(IMyService service);
    }
    

    我们不会实现这个接口,因为 Castle Windsor 将使用动态代理生成一个实现。这里有一个重要的细节:工厂方法中的参数名称(数据)和构造函数中的参数名称(数据)应该匹配。

    然后我们进行注册并尝试解析值。

    [Test]
    public void ResolveByFactory()
    {
        WindsorContainer container = new WindsorContainer();
        container.AddFacility<TypedFactoryFacility>();
        container.Register(Component.For<IMyServiceFactory>().AsFactory());
        container.Register(Component.For<IMyService>().ImplementedBy<MyService>().LifestyleScoped());
        container.Register(Component.For<IDependency>().ImplementedBy<Dependency>().LifestyleScoped());
    
        IMyServiceFactory factory = container.Resolve<IMyServiceFactory>();
    
        IMyService myService1;
        IMyService myService2;
    
        using (container.BeginScope())
        {
            myService1 = factory.Create(new Data());
            myService2 = factory.Create(new Data());
    
            myService1.Should().BeSameAs(myService2);
        }
    
        using (container.BeginScope())
        {
            IMyService myService3 = factory.Create(new Data());
    
            myService3.Should().NotBeSameAs(myService1);
            myService3.Should().NotBeSameAs(myService2);
        }
    } 
    

    您会看到在同一范围内创建的对象是相同的引用。让我知道这是否是您想要的行为。

    【讨论】:

      【解决方案5】:

      我对此的看法是,您已经尽了最大努力。我对此唯一的烦恼是 Autofac 在帮助您管理这些生命周期范围方面并没有真正做得很好,因此您只能在某个地方调用他们的BeginLifetimeScope。 它们可以嵌套。

      另一方面,Ninject 可以做一些非常酷的事情,不需要让你的大脑由内而外。他们的named scope extension 使您可以创建一个 (gasp) 命名范围,并在该范围内绑定对象的生命周期。如果您正在使用工厂(从问题来看,显然您是),您还需要使用上下文保存扩展,以便从工厂激活的东西从工厂被激活的命名范围获得生命周期管理。绑定最终看起来像这样:

      var scopeName = "Your Name Here";
      
      Bind<TopLevelObject>().ToSelf().DefinesNamedScope(ScopeName);
      Bind<ISomeScopedService>().To<SomeScopedService>().InNamedScope(ScopeName);
      // A minor bit of gymnastics here for factory-activated types to use
      //  the context-preservation extension.
      Bind<FactoryActivatedType>().ToSelf().InNamedScope(ScopeName);
      Bind<IFactoryActivatedType>().ToMethod(x => x.ContextPreservingGet<FactoryActivatedType>());
      

      这方面的好处在于,这些绑定的范围特定地绑定到命名的范围,而不是仅仅绑定到链上最近的生命周期范围。恕我直言,它使这些对象的生命周期更加可预测。

      【讨论】:

      • Autofac 已将生命周期范围命名为 as well
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-04
      • 1970-01-01
      相关资源
      最近更新 更多