Reputation: 6592
I have an interface as follows
public interface IDataProvider
{
List<string> GetData();
}
Implementation of it
public class TextDataProvider: IDataProvider
{
public TexDataProvider(string source){...}
public List<string> GetData() {...}
}
One of my services uses IDataProvider to get data. Different implementations could be injected by changing the Unity Register method with alternate implementations.
However, the source parameter of the constructor is only known at the time GetData is called. So, when Unity registers an implementation of IDataProvider, the source parameter is not known.
I understand one alternative is to move the source to GetData method and the other is to create an Abstract Factory whose CreateMethod takes the source parameter and passes it to IDataProvider implementation constructor. Then we can inject Factory instead of IDataProvider instances.
But is there a better way this can be addressed ?
Upvotes: 0
Views: 66
Reputation: 172666
The core problem here is that you are trying to build up components using runtime data. When letting your container build your object graph (consisting of the components of the application that contain the application's behaviour), those object graphs should only consist of information that is known at compile-time, or information that is fixed for the duration of the application (such as configuration values). When mixing runtime data, things start to break down quickly, because your DI configuration starts to get complicated quickly, your design starts to deteriorate (e.g. because you will start to add factory abstractions) and it becomes much harder to verify the correctness of your object graph.
Instead, runtime data should be sent through the (already existing) object graph using method calls (and property calls). Not using constructors, because constructors are used during object construction.
So two solutions come to mind. Either you change the contract of the GetData
method, or you introduce an abstraction that allows you to retrieve contextual information.
Changing the contract of GetData basically means that you pass that runtime value into the
GetData` method:
public interface IDataProvider
{
List<string> GetData(string source);
}
This makes the call to GetData
very explicit and is a good solution if all IDataProvider
implementations need that source. Using an IDataProviderFactory
with an CreateProvider(string source)
method also implies that all implementations would need that source
value, but by changing the GetData
method we prevented the use of an extra layer of abstraction.
If on the other hand this source
is implementation specific or can more be seen as contextual data, you can introduce an abstraction. The current system's time and the user on whose behalf an operation is executed are examples of contextual data. You don't want to pass this kind of information through the system from method to method. You just want this information to 'be available'. This can be done by introducing an abstraction. For instance:
public interface ISourceContext
{
string CurrentSource { get; }
}
This abstraction allows to retrieve the source for the current context (whatever a context may be, but usually a request such as a web request).
Your TextDataProvider
implementation can now depend on this new ISourceContext
abstraction:
public class TextDataProvider : IDataProvider
{
public TexDataProvider(ISourceContext sourceContext){...}
public List<string> GetData() {
// Call CurrentSource 'at runtime'; never in the ctor.
string source = this.sourceContext.CurrentSource;
...
}
}
Now you can have some kind of technology specific adapter implementation for ISourceContext
that allows supplying the application with the correct source. For instance:
public sealed class AspNetSourceContext : ISourceContext {
public string CurrentSource {
get { return HttpContext.Current.Request.QueryString["source"]; }
}
}
At first sight, this solution looks the same as using a factory abstraction, but there are a few quite important differences.
First of all, only the TextDataProvider
implementation knows about the new abstraction, where with an IDataProviderFactory
we would force extra complexity (an extra abstraction) upon all the consumers of IDataProvider
, since all consumers of IDataProvider
would need to work with the IDataProviderFactory
as well.
Besides, using the factory still complicates construction of components, because the factory would need to use the runtime value in the constructor of a component that is likely to need other (compile time) dependencies as well. This is harder to achieve, causes the creation of the object graph to be delayed and makes it harder to verify whether this graph can actually be constructed. It would force you to run the actual code that calls this factory at runtime to find out if it works, compared to knowing this directly after your application started (fail fast).
Upvotes: 4