Подтвердить что ты не робот

Как работать с параметрами времени выполнения при использовании охвата по времени?

Предупреждение, длинный пост впереди.

В последнее время я много думал об этом, и я изо всех сил стараюсь найти здесь удовлетворительное решение. Я буду использовать С# и autofac для примеров.

Проблема

IoC отлично подходит для построения больших деревьев служб без состояния. Я разрешаю услуги и передаю данные только вызовам метода. Отлично.

Иногда я хочу передать параметр данных в конструктор службы. Для чего нужны фабрики. Вместо разрешения службы я разрешаю его factory и вызываю метод create с параметром для получения моей услуги. Еще немного работы, но хорошо.

Время от времени я хочу, чтобы мои службы разрешали один и тот же экземпляр в определенной области. Autofac обеспечивает InstancePerLifeTimeScope(), что очень удобно. Это позволяет мне всегда разрешать один и тот же экземпляр внутри подкаталога выполнения. Хорошо.

И бывают случаи, когда я хочу объединить оба подхода. Я хочу, чтобы параметр data в конструкторе и имел экземпляры в области. Я не нашел удовлетворительного способа выполнить это.

Решение

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>();

Это правильно. Я не сохраняю результат вызова factory в любом месте, потому что autofac сохраняет его для меня. Это очень удивительно для любого, кто прочитает код. Я не уверен, что autofac даже предназначался для такого использования. Хорошая вещь в том, что мне не нужен ни дополнительный метод инициализации, ни дополнительный класс для хранения экземпляра.

Вопрос

Как вы относитесь к этому? Как вы справляетесь с ситуацией с параметрами данных во время выполнения и охватом времени? Не хватает ли лучшего подхода?

4b9b3361

Ответ 1

Autofac теперь поддерживает это из коробки с расширением к областям времени жизни. Метод BeginLifetimeScope() имеет перегрузку, которая принимает Action<ContainerBuilder>, что позволяет добавлять новые регистрации, специфичные только для этой области действия. Таким образом, для данного примера это выглядит примерно так:

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.- Если вы не потеряете объем данных во время выполнения в каждом Решении, вы можете решить с помощью 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, когда и где вы создаете времени выполнения. Autofac должен inyect 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

Я считаю, что вы сделали так хорошо, как можете. Единственное, что у меня есть, это то, что Autofac на самом деле не очень помогает вам управлять этими областями времени жизни, поэтому вы застряли, называя их BeginLifetimeScope где-то. И они могут быть вложенными. Mind Blown.

Ninject, с другой стороны, делает некоторые действительно классные вещи, которые не требуют поворота вашего мозга наизнанку. Их именованный расширение области расширения позволяет вам создать именованную область (gasp) с именем и привязать время жизни объектов в пределах этой области. Если вы используете фабрики (очевидно, вы, судя по этому вопросу), вы также захотите использовать расширение сохранения контекста, чтобы материал, активированный из заводов, получал управление жизненным циклом из именованной области, в которой был активирован factory, Привязки заканчиваются, выглядя примерно так:

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>());

Приятная часть этого заключается в том, что область этих привязок привязана к именованной области, а не просто привязана к любой ближайшей продолжительности жизни в цепочке. ИМХО, это делает жизнь этих объектов намного более предсказуемой.

Ответ 4

Многие рамки IoC поддерживают регистрацию функции factory (или лямбда-выражения), которая принимает в качестве одного из своих аргументов экземпляр самого контекста контейнера/области/разрешения.

Это позволяет использовать дополнительные уровни косвенности, а также использовать информацию, которая однозначно идентифицирует контекст или область. Кроме того, многие из них обеспечивают перехваты, такие как обработчики событий или возможность получения из класса видимости жизненного цикла, для взаимодействия с запущенной или завершенной областью.

Принцип

Для 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, который можно создать, вы можете пропустить класс расширения и зарегистрировать делегат "factory" с вложенной областью, когда вы его создаете:

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

Ответ 5

Если вы правильно поняли, что хотите использовать фабрики, делегируя создание объекта в контейнер, передавая некоторые параметры его конструктору.

Это реализовано в Castle Windsor с напечатано factory объект.

Примеры классов, которые мы хотим решить:

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
{
}

Создаем интерфейс factory:

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

Мы не будем реализовывать этот интерфейс, потому что Castle Windsor будет генерировать реализацию с помощью Dynamic Proxy. Здесь важная деталь: имя параметра (данные) в методе factory и тот, который должен быть в конструкторе.

Затем мы делаем регистрацию и пытаемся разрешить значения.

[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);
    }
} 

Вы увидите, что объект, созданный в той же области действия, - это те же ссылки. Дайте мне знать, если это поведение, которое вы хотите.