DryIOC Container configuration for property inject

2019-06-28 06:38发布

问题:

I have search far and wide for a simple example of how to configure a DryIoc container to simply inject dependencies as properties the same way that it injects constructor args.

Given the following working example...

Container registration:

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

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

Widget Service:

public class WidgetService : IWidgetService
{
    private readonly IWidgetRepository _widgetRepository;

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

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

Widget Repository:

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

How does the configuration need to change to support a WidgetService that looks like this where DryIoc will inject the WidgetRepository as a property?

Desired Widget Service:

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

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

FAILED ATTEMPTS

I have tried these config changes, but they seem to have no effect in enabling property injection on the WidgetService.

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

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

I have also tried the above with PropertiesAndFields.All, also with no luck.

NOTE: I understand that property injection is not the recommended approach and that constructor injection is preferred for many reasons. However, I want to know how to do both correctly.

Update

Following @dadhi's advice, I changed attempt #2 to initialize the container like:

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

but then I received this error:

{
    "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 now seems to be attempting to initialize my WidgetController by using a no-arg constructor which I didn't have. I am assuming since the rules change to PropertiesAndFields.All(...), DryIoc is attempting to use property injection for all registered items.

public class WidgetController : ApiController
{
    private readonly IWidgetService _widgetService;

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

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

I was attempting to initialize the WidgetService (shown above) using property injection, but the WidgetController using constructor injection. Perhaps I can't do both, but I was thinking that the PropertiesAndFields.Auto rule would have allowed for both. I changed the WidgetController to be setup for property injection as well. Then I get no exceptions from DryIoc, but WidgetService ends up null in the WidgetController. Here's the updated WidgetController.

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

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

Automatic property injection still seems elusive.

Update 2

After much trial and error (and suggestions from @dadhi), I settled for using constructor injection in my WidgetController and specifying property injection when registering other services. This allows for migrating code that utilizes property injection now to constructor injection over time, with the exception of controllers. Here's my updated container registration:

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

I would love to eventually figure out the golden setting that would work for both controllers and other service, but they seem to act differently. But, this is a workable enough solution for now.

Update 3

Updated the container configuration per @dadhi's suggestion to the following in another attempt at wiring up everything (including WidgetController) as property injection:

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

This seems to at least yield an exception that makes some sense and perhaps explain why the controllers are treated different when I setup the container to use 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)"
    }
}

回答1:

The first attemt should be changed to:

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

The second attempt from the code alone should work, but it may be something else. To find out you may change the rules to:

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

The better alternative though, would be to specify exact property for exact service:

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

or strongly typed:

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

Update:

DryIoc designed with the defaults of least surprise: type with single constructor for DI and without property injection. But you can opt-in the defaults to simplify migration:

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

It may be enough for your case.

If not, you may add rules:

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

Update 2:

This test worked for me.

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

But changing PropertiesAndFields.Auto to
PropertiesAndFields.All(false, false, false, IfUnresolved.Throw) produced the error from your update 3.

Update 3:

Thank you for uploading the sample repo, it helped to find the problem.

DryIoc PropertiesAndFields.Auto rule will inject all declared and base class properties, which causes the error for some properties defined in base ApiController class. The good thing is that Auto is just a predefined rule, and you can define your own to exclude the base class properties:

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

then create the container like this:

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

I have submitted the PR with the fix.

BTW In future / next DryIoc version I will provide the API to simplify base or declared only property selection.



回答2:

Ok, ignore my last comment, probably non relevant. Looking into exception stacktrace, it seems that WebAPI falls back to using Activator.CreateInstance for controller, and that is because DryIoc is unable to resolve it. But the fallback masks the actual DryIoc error. To find it out please try:

container.WithWebApi(throwIfUnresolved: type => type.IsController());