Конфигурация контейнера DryIOC для ввода свойств

Я по всему миру ищу простой пример того, как настроить контейнер DryIoc так, чтобы он просто вставлял зависимости как свойства так же, как он вводит аргументы конструктора.

Учитывая следующий рабочий пример...

Регистрация контейнера:

    public static void Register(HttpConfiguration config)
    {
        var c = new Container().WithWebApi(config);

        c.Register<IWidgetService, WidgetService>(Reuse.Singleton);
        c.Register<IWidgetRepository, WidgetRepository>(Reuse.Singleton);
    }

Сервис виджетов:

public class WidgetService : IWidgetService
{
    private readonly IWidgetRepository _widgetRepository;

    public WidgetService(IWidgetRepository widgetRepository)
    {
        _widgetRepository = widgetRepository;
    }

    public IList<Widget> GetWidgets()
    {
        return _widgetRepository.GetWidgets().ToList();
    }
}

Репозиторий виджетов:

public class WidgetRepository : IWidgetRepository
{
    private readonly IList<Widget> _widgets;

    public WidgetRepository()
    {
        _widgets = new List<Widget>
        {
            new Widget {Name = "Widget 1", Cost = new decimal(1.99), Description = "The first widget"},
            new Widget {Name = "Widget 2", Cost = new decimal(2.99), Description = "The second widget"}
        };
    }

    public IEnumerable<Widget> GetWidgets()
    {
        return _widgets.AsEnumerable();
    }
}

Как необходимо изменить конфигурацию для поддержки Widget Service, который выглядит следующим образом, где DryIoc будет внедрять WidgetRepository в качестве свойства?

Требуемый сервис виджетов:

public class WidgetService : IWidgetService
{
    public IWidgetRepository WidgetRepository { get; set; }

    public IList<Widget> GetWidgets()
    {
        return WidgetRepository.GetWidgets().ToList();
    }
}

Неудачные попытки

Я попробовал эти изменения конфигурации, но они, кажется, не влияют на включение свойства в Widget Service.

ПОПЫТКА 1:

public static void Register(HttpConfiguration config)
    {
        var c = new Container().WithWebApi(config);

        // Seems logical - no luck
        c.InjectPropertiesAndFields(PropertiesAndFields.Auto);

        c.Register<IWidgetService, WidgetService>(Reuse.Singleton);
        c.Register<IWidgetRepository, WidgetRepository>(Reuse.Singleton);
    }

Попытка 2:

    public static void Register(HttpConfiguration config)
    {
        var c = new Container(Rules.Default.With(propertiesAndFields: PropertiesAndFields.Auto))
                    .WithWebApi(config);

        c.Register<IWidgetService, WidgetService>(Reuse.Singleton);
        c.Register<IWidgetRepository, WidgetRepository>(Reuse.Singleton);
    }

Я также попробовал вышеупомянутое с PropertiesAndFields.AllТоже не повезло.

ПРИМЕЧАНИЕ. Я понимаю, что внедрение свойства не является рекомендуемым подходом и что внедрение по конструктору является предпочтительным по многим причинам. Тем не менее, я хочу знать, как сделать оба правильно.

Обновить

Следуя совету @ dadhi, я изменил попытку #2 для инициализации контейнера следующим образом:

var c = new Container(Rules.Default.With(propertiesAndFields: PropertiesAndFields.All(withNonPublic: false,
                                                            withPrimitive: false,
                                                            withFields: false,
                                                            ifUnresolved: IfUnresolved.Throw)))
.WithWebApi(config);

но потом я получил эту ошибку:

{
    "Message" : "An error has occurred.",
    "ExceptionMessage" : "An error occurred when trying to create a controller of type 'WidgetController'. Make sure that the controller has a parameterless public constructor.",
    "ExceptionType" : "System.InvalidOperationException",
    "StackTrace" : "   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)\r\n   at System.Web.Http.Controllers.HttpControllerDescriptor.CreateController(HttpRequestMessage request)\r\n   at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()",
    "InnerException" : {
        "Message" : "An error has occurred.",
        "ExceptionMessage" : "Type 'IOCContainerTest.DryIOC.Controllers.WidgetController' does not have a default constructor",
        "ExceptionType" : "System.ArgumentException",
        "StackTrace" : "   at System.Linq.Expressions.Expression.New(Type type)\r\n   at System.Web.Http.Internal.TypeActivator.Create[TBase](Type instanceType)\r\n   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.GetInstanceOrActivator(HttpRequestMessage request, Type controllerType, Func`1& activator)\r\n   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)"
    }
}

Теперь DryIoc пытается инициализировать мой WidgetController с помощью конструктора без аргументов, которого у меня не было. Я предполагаю, так как правила меняются на PropertiesAndFields.All(...)DryIoc пытается использовать инъекцию свойства для всех зарегистрированных объектов.

public class WidgetController : ApiController
{
    private readonly IWidgetService _widgetService;

    public WidgetController(IWidgetService widgetService)
    {
        _widgetService = widgetService;
    }

    // GET api/<controller>
    public List<WidgetSummary> Get()
    {
        return _widgetService.GetWidgetSummaries();
    }
}

Я пытался инициализировать Widget Service (показанный выше), используя внедрение свойства, но WidgetController используя внедрение конструктора. Возможно, я не могу сделать оба, но я думал, что PropertiesAndFields.Auto Правило позволило бы для обоих. Я изменил WidgetController, чтобы он также настраивался на внедрение свойств. Тогда я не получаю никаких исключений из DryIoc, но Widget Service заканчивается нулем в WidgetController. Вот обновленный WidgetController.

public class WidgetController : ApiController
{
    public IWidgetService WidgetService { get; set; }

    // GET api/<controller>
    public List<WidgetSummary> Get()
    {
        return WidgetService.GetWidgetSummaries();
    }
}

Автоматическое введение свойства все еще кажется неуловимым.

Обновление 2

После долгих проб и ошибок (и предложений от @dadhi) я решил использовать инжекцию конструктора в моем WidgetController и указать внедрение свойства при регистрации других сервисов. Это позволяет перенести код, использующий внедрение свойств, теперь на внедрение конструктора с течением времени, за исключением контроллеров. Вот моя обновленная регистрация контейнера:

    public static void Register(HttpConfiguration config)
    {
        var c = new Container(Rules.Default).WithWebApi(config);

        var autoInjectProps = Made.Of(propertiesAndFields: PropertiesAndFields.Auto);

        c.Register<IWidgetService, WidgetService>(Reuse.Singleton, autoInjectProps);
        c.Register<IWidgetRepository, WidgetRepository>(Reuse.Singleton, autoInjectProps);
    }

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

Обновление 3

Обновил конфигурацию контейнера в соответствии с предложением @ dadhi следующим образом в другой попытке подключить все (включая WidgetController) как внедрение свойства:

    public static void Register(HttpConfiguration config)
    {
        var c = new Container(Rules.Default.With(propertiesAndFields: PropertiesAndFields.All(withNonPublic: false, withPrimitive: false, withFields: false, ifUnresolved: IfUnresolved.Throw)))
                    .WithWebApi(config, throwIfUnresolved: type => type.IsController());

        c.Register<IWidgetService, WidgetService>(Reuse.Singleton);
        c.Register<IWidgetRepository, WidgetRepository>(Reuse.Singleton);
    }

Похоже, что это, по крайней мере, дает исключение, которое имеет некоторый смысл и, возможно, объясняет, почему контроллеры обрабатываются по-разному, когда я настраиваю контейнер для использования PropertiesAndFields.All(..):

{
    "Message" : "An error has occurred.",
    "ExceptionMessage" : "An error occurred when trying to create a controller of type 'WidgetController'. Make sure that the controller has a parameterless public constructor.",
    "ExceptionType" : "System.InvalidOperationException",
    "StackTrace" : "   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)\r\n   at System.Web.Http.Controllers.HttpControllerDescriptor.CreateController(HttpRequestMessage request)\r\n   at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()",
    "InnerException" : {
        "Message" : "An error has occurred.",
        "ExceptionMessage" : "Unable to resolve HttpConfiguration as property \"Configuration\"\r\n  in IOCContainerTest.DryIOC.Controllers.WidgetController.\r\nWhere no service registrations found\r\n  and number of Rules.FallbackContainers: 0\r\n  and number of Rules.UnknownServiceResolvers: 0",
        "ExceptionType" : "DryIoc.ContainerException",
        "StackTrace" : "   at DryIoc.Throw.It(Int32 error, Object arg0, Object arg1, Object arg2, Object arg3)\r\n   at DryIoc.Container.ThrowUnableToResolve(Request request)\r\n   at DryIoc.Container.DryIoc.IContainer.ResolveFactory(Request request)\r\n   at DryIoc.ReflectionFactory.InitPropertiesAndFields(NewExpression newServiceExpr, Request request)\r\n   at DryIoc.ReflectionFactory.CreateExpressionOrDefault(Request request)\r\n   at DryIoc.Factory.GetExpressionOrDefault(Request request)\r\n   at DryIoc.Factory.GetDelegateOrDefault(Request request)\r\n   at DryIoc.Container.ResolveAndCacheDefaultDelegate(Type serviceType, Boolean ifUnresolvedReturnDefault, IScope scope)\r\n   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.GetInstanceOrActivator(HttpRequestMessage request, Type controllerType, Func`1& activator)\r\n   at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)"
    }
}

2 ответа

Решение

Первое внимание должно быть изменено на:

public static void Register(HttpConfiguration config)
{
    var c = new Container().WithWebApi(config);

    c.Register<IWidgetService, WidgetService>(Reuse.Singleton);
    c.Register<IWidgetRepository, WidgetRepository>(Reuse.Singleton);

    // Resolve service first then inject properties into it.
    var ws = c.Resolve<IWidgetService>();
    c.InjectPropertiesAndFields(ws);
}

Вторая попытка одного только кода должна работать, но это может быть что-то еще. Чтобы узнать это, вы можете изменить правила на:

PropertiesAndFields.All(withNonPublic: false, withPrimitives: false, withFields: false, ifUnresolved: IfUnresolved.Throw)

Тем не менее, лучшая альтернатива - указать точное свойство для точного обслуживания:

c.Register<IWidgetService, WidgetService>(Reuse.Singleton, 
     made: PropertiesAndFields.Of.Name("WidgetRepository"));

или строго набрал:

c.Register<IWidgetService, WidgetService>(
    Made.Of(() => new WidgetService { WidgetRepository = Arg.Of<IWidgetRepository>() }),
    Reuse.Singleton);

Обновить:

DryIoc разработан с минимальными неожиданностями по умолчанию: тип с одним конструктором для DI и без введения свойства. Но вы можете выбрать значения по умолчанию, чтобы упростить миграцию:

IContainer c = new Container(Rules.Default.With(
    FactoryMethod.ConstructorWithResolvableArguments, 
    propertiesAndFields: PropertiesAndFilds.Auto);

Это может быть достаточно для вашего случая.

Если нет, вы можете добавить правила:

    .WithFactorySelector(Rules.SelectLastRegisteredFactory())
    .WithTrackingDisposableTransients()
    .WithAutoConcreteTypeResolution()

Обновление 2:

Этот тест работал для меня.

[Test]
public void Controller_with_property_injection()
{
    var config = new HttpConfiguration();
    var c = new Container()
        .With(rules => rules.With(propertiesAndFields: PropertiesAndFields.Auto))
        .WithWebApi(config, throwIfUnresolved: type => type.IsController());

    c.Register<A>(Reuse.Singleton);

    using (var scope = config.DependencyResolver.BeginScope())
    {
        var propController = (PropController)scope.GetService(typeof(PropController));
        Assert.IsNotNull(propController.A);
    }
}

public class PropController : ApiController
{
    public A A { get; set; }
}

public class A {}

Но меняется PropertiesAndFields.Auto в
PropertiesAndFields.All(false, false, false, IfUnresolved.Throw) произвел ошибку из вашего обновления 3.

Обновление 3:

Спасибо за загрузку примера репо, это помогло найти проблему.

DryIoc PropertiesAndFields.Auto Правило вставит все объявленные свойства и свойства базового класса, что приводит к ошибке для некоторых свойств, определенных в base ApiController учебный класс. Хорошо, что Auto это просто предопределенное правило, и вы можете определить свое собственное, чтобы исключить свойства базового класса:

private static IEnumerable<PropertyOrFieldServiceInfo> DeclaredPublicProperties(Request request)
{
    return (request.ImplementationType ?? request.ServiceType).GetTypeInfo()
        .DeclaredProperties.Where(p => p.IsInjectable())
        .Select(PropertyOrFieldServiceInfo.Of);
}

затем создайте контейнер следующим образом:

var c = new Container()
    .With(rules => rules.With(propertiesAndFields: DeclaredPublicProperties))
    .WithWebApi(config, throwIfUnresolved: type => type.IsController());

Я представил PR с исправлением.

Кстати, в будущей / следующей версии DryIoc я предоставлю API для упрощения выбора базовых или объявленных свойств.

Хорошо, проигнорируйте мой последний комментарий, возможно, не относящийся к делу. Что касается трассировки стека исключений, то кажется, что WebAPI возвращается к использованию Activator.CreateInstance для контроллера, и это потому, что DryIoc не может разрешить его. Но запасной вариант маскирует фактическую ошибку DryIoc. Чтобы узнать это, попробуйте:

container.WithWebApi(throwIfUnresolved: type => type.IsController());
Другие вопросы по тегам