How to pass parameters into constructors when using IoC containers?
Asked Answered
T

2

8

Arrrgh! I am pulling my hair over here. I have been trying to use IoC containers for a little bit, and all seems fine and dandy until you hit some issue that you think would be very basic, like passing parameters into constructors.

Say I have a class somewhere with a mix of reference classes that can be resolved by IoC and value types (or some other types) that can only be resolved at runtime:

public NFLFeedUnitOfWork(NFLFileType fileType, object feed, IConverterMappings<NFLFileType> nflConverterMappings, IDbContext context)
    : base(fileType, feed, nflConverterMappings, context, ContextType.NFL)
{
    //new NFLContext(connstringname, setAutoDetectChanges)
}

In this particular example I pass in Enum (NFLFileType), object instance, 2 interface parameters and pass in one extra hardcoded property into the base constructor (ContextType.NFL)

How in the name of all gods can I do this in any IoC container?

The problem is actually 2-fold:

1.) How to pass in an object that is only known at runtime? Say for example the calling code looks like this at the moment:

protected override IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed, string connectionString)
{
    return new NFLFeedUnitOfWork(fileType, feed, new NFLConverterMappings(), new NFLContext(connectionString));
}

How can I convert this code to be using IoC? Perhaps to something like this?

protected override IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed, string connectionString)
{
    return IFLFeedUnitOfWork(fileType, feed);
}

Where the last 2 parameters are automatically resolved, and the 1st 2 I supply by myself?

2.) How can i pass in Enum, object, value types into constructor using IoC? (or maybe refrain from using it in this particular instance?)

Anyway, any help is greatly appreciated, especially on the 1st point. I am using Unity at the moment, but any other IoC container is fine as well.

I don't want to pass in an IoC container into the code either, I only want to specify it in one place at the top level.

Tierney answered 10/7, 2015 at 15:2 Comment(0)
C
10

I have a class somewhere with a mix of reference classes that can be resolved by IoC and value types (or some other types) that can only be resolved at runtime

You should prevent mixing compile time dependencies with runtime data when you compose components. Your object graphs should be static (and preferably stateless) and runtime data should be passed through the object graph using method calls after the complete object graph is constructed. This can cause a tremendous simplification in the development of your application, because it allows your object graphs to be statically verified (using a tool, unit tests or by using Pure DI) and it prevents having the trouble you are having today.

In general, you have two options to solve this:

  1. You pass the data on to lower level components through method calls.
  2. You retrieve the data by calling a method on an injected component.

Which solution to take depends on the context.

You would typically go for option one in case the data is specific to the handled request and would be part of the use case you're handling. For instance:

public interface IFeedUnitOfWorkProvider
{
    IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed);
}

Here the IFeedUnitOfWorkProvider contains a GetUnitOfWork method that requires the runtime parameters as input. An implementation might look like this:

public class FeedUnitOfWorkProvider : IFeedUnitOfWorkProvider
{
    private readonly IConverterMappings converterMappings;
    private readonly IContext context;

    public FeedUnitOfWorkProvider(IConverterMappings converterMappings,
        IContext context) {
        this.converterMappings = converterMappings;
        this.context = context;
    }

    public IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed) {
        return new NFLFeedUnitOfWork(fileType, feed, this.converterMappings,
            this.context);
    }       
}

A few things to note here:

  • All statically known dependencies are injected in the constructor, while runtime values are passed on through method calls.
  • The connectionString value is unknown to the FeedUnitOfWorkProvider. It's not a direct dependency and the provider doesn't have to know about its existence.
  • Assuming that the connectionString is a configuration value that does not change at runtime (and is typically stored in the application's configuration file), it can be injected into the NFLContext the same way as other dependencies are injected. Note that a configuration value is different from a runtime value, because it won't change during the lifetime of the application.

The second option is especially useful when you're dealing with contextual information. This is information that is important for an implementation, but should not be passed in as with the previous example. A good example of this is information about the user on whos behalf the request is running. A typical abstraction for this would look like this:

public interface IUserContext {
    string CurrentUserName { get; }
}

This interface can be injected into any consumer. Using this abstraction, the consumer can query the user name at runtime. Passing through the user name with the rest of the request data would typically be awkward, because this would allow the caller to change (or forget) the user name, making the code just harder to work with, harder to test, more error prone. Instead we can use the IUserContext:

public IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed) {
    if (this.userContext.CurrentUserName == "steven") {
        return new AdminUnitOfWork(this.context);
    }
    return new NFLFeedUnitOfWork(fileType, feed, this.converterMappings,
        this.context);
}

How a IUserContext implementation should look like will highly depend on the type of application you're building. For ASP.NET I imagine something like this:

public class AspNetUserContext : IUserContext {
    string CurrentUserName {
        get { return HttpContext.Current.User.Name; }
    }
}
Centeno answered 10/7, 2015 at 19:31 Comment(3)
+1. This shows the reason for the OP's pain points and provides good recommendations. Unity also has the ability to provide ResolverOverrides that allow you to override the existing registrations when resolving (as in Resolving Objects by Using Overrides). I hesitate to mention this because I think the advice above should be the first approach.Parrie
Such a great answer!!! Thank you so much Steven, this definitely answers most of my questions and points me in the right direction. However could you clarify something please? In the example provided for case 1, we basically create a Provider for the class calling GetUnitOfWork method. However in the implementation of GetUnitOfWork itself we still pass 2 values that are only known at runtime to the new NFLFeedUnitOfWork(... Does it mean that I also need to create 2 other Providers for NFLFileType fileType and object feed? (Which seems a bit too much, 1 provider for 1 runtime value)Tierney
@Tanuki: I can't answer that question. I don't know what those vakues are, and how they are determined. Please update your question with information about this, and I'll try to have a look.Centeno
G
1

You have a couple different options, depending on where the values come from. (I'm only familiar with Ninject, I would assume Unity has similar functionality)

If the data is dependent on another service or repository, you can bind the object to a delegate so that it's resolved when the request is fulfilled. for example:

Bind<NFLFileType>().ToMethod( context => context.Kernel.Get<IConfigProvider>().NFLFileType );

Ninject also supports bindings that only resolve under certain circumstances using the .When() syntax. for example:

Bind<NFLFileType>().ToConstant( NFLFileType.MyValue ).WhenInjectedInto<NFLFeedUnitOfWork>();

Bind<NFLFileType>().ToConstant( NFLFileType.MyValue ).When(request => request.Target.Type.GetCustomAttributes(typeof(MyValueAttribute)) != null );

If all else fails, you can bind the injected interface to a Provider that works like a Factory pattern to generate the object:

Bind<IFeedUnitOfWork>().ToProvider<UnitOfWorkProvider>();

and the Provider knows how to resolve the first 2 arguments based on the context.

Gough answered 10/7, 2015 at 18:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.