net-feature-tests
net-feature-tests copied to clipboard
DI Named and Keyed service Registration feature
Debatable if it is good practice or not, but it may be important and decisive factor for someone. Especially when migrating from Container that supports it.
Using DryIoc as example.
Named:
container.Register<IService, Service>(named: "for test");
container.Resolve<IService>("for test");
Keyed:
container.Register<IService, Service>(named: SomeEnum.Special);
container.Resolve<IService>(SomeEnum.Special);
I have considered that (and it is pretty widely implemented).
The thing is -- my goal is not only to compare, but also encourage feature parity for important features. For that I exclude features that don't seem important/best practice as I don't want to penalize frameworks that skip these.
For example, XML configuration was widely implemented at some point, but now it is not really used much -- mostly because it wasn't a great idea to start with.
I'll keep this open so that other people can provide some feedback as well -- I might reconsider if I see compelling scenarios.
Lately I've got an interesting setup in scope of this issue in DryIoc.
I suppose such setups are not particularly rare, and I am not sure how to handle them without Key/Name or Metadata support in Container without changing the domain model. (it is a bit simplified, full version is here)
public interface ITwoVariants { }
internal class FirstVariant : ITwoVariants { }
internal class SecondVariant : ITwoVariants { }
public interface IArea
{
ITwoVariants OneVariant { get; set; }
}
public class Area : IArea
{
public ITwoVariants OneVariant { get; set; }
public Area( ITwoVariants oneVariant) {OneVariant = oneVariant; }
}
public interface IComponent
{
IArea Area1 { get; set; }
IArea Area2 { get; set; }
}
internal class Component : IComponent
{
public IArea Area1 { get; set; }
public IArea Area2 { get; set; }
public Component(IArea area1, IArea area2)
{
Area1 = area1;
Area2 = area2;
}
}
Component area1 should be injected with FirstVariant dependency and area2 with SecondVariant.
In DryIoc interesting registrations could be done as following:
// keys:
public enum Areas { First, Second }
// registrations:
container.Register<ITwoVariants, FirstVariant>(serviceKey: Areas.First);
container.Register<ITwoVariants, SecondVariant>(serviceKey: Areas.Second);
container.Register<IArea, Area>(serviceKey: Areas.First,
with: Parameters.Of.Type<ITwoVariants>(serviceKey: Areas.First);
container.Register<IArea, Area>(serviceKey: Areas.Second,
with: Parameters.Of.Type<ITwoVariants>(serviceKey: Areas.Second));
container.Register<IComponent, Component>(
with: Parameters.Of
.Name("area1", serviceKey: Areas.First)
.Name("area2", serviceKey: Areas.Second));
What are your thoughts?
Regards, Maksim
Why are you injecting two instances of the same abstraction (IArea) into a single constructor (Component). That seems like a Liskov Substitution Principle violation to me. What happens if you (accidentally) switch the constructor arguments around (i.e. inject the first area into area2 and the second area into area1)? Will the application still work? If not, you are breaking LSP.
Hello, You are probably right about fragile design in this case. I've got the example as is. But what if you have a lot of legacy/existing code like that? Should container dictate domain model change or is it up to developer to decide, and you can start using container as unobtrusive tool, which guides you to better practices and pit-of-success with well-designed API and documentation?
It is my personal opinion though, and may be more relevant to issue #21.
Regards, Maksim
The design philosophy of Simple Injector is to have a design that pushes developers into the right design, by making it easy to do the right thing and harder (but not impossible) to do the 'wrong' thing. Since I am the main driver behind Simple Injector's design, you can imagine my view on this.
But even with a container that doesn't have this design philosophy, you will never be able to use the container as an unobtrusive tool in an application that doesn't follow the SOLID principles. Without SOLID, it is already hard to keep your application maintainable, but to make things worse, it becomes REALLY painful to configure your container if you don't follow SOLID. Dependency Injection has the tendency to expose SOLID violations; problems that are already there, but now stick out like a sore thumb. So even with less 'obstrusive' containers, you will feel this about the same pain.
@dadhi I looked at your use case, but it is hard for me to give feedback without understanding what variants are in this case. Basically the question is -- why is one of them correct for one area, and other for other?
I had similar things designed in very different ways depending on actual situation:
- Variants might know whether they should apply to a certain area
- Area might know whether certain quality of variants makes them applicable
- Another component might define a strategy for binding variant to an area
- A scope key might define what goes where (e.g. multi-tenants)
- Others (including service key)
Btw most of those designs would prefer a list of areas over two distinct parameters -- so the key becomes a part of the architecture, allowing to select an area and/or dependencies as needed after the resolution process.
If you provide a specific use case (where variants represent application components with descriptive names) it would be easier for me to discuss design options.
I am trying to figure out how to use dependency injection and this questions is plaguing me as well, I can see what @dadhi is talking about.
What if you have a couple of IQueue objects that you need to take from one, do something and enqueue into another (depending on something in the source object)
ie:
Class QueueProcessor {
public QueueProcessor(IQueue inQueue, IQueue processedQueue, IQueue errorQueue)
{
}
}
How would DI be used in this case? What would be the most correct way of doing this?
Any container allows you to make this registration with a lambda expression much like the following:
container.Register<QueueProcessor>(() => new QueueProcessor(
inQueue: new InQueue(),
processedQueue: new ProcessedQueue(),
errorQueue: new ErrorQueue());
or
container.Register<QueueProcessor>(() => new QueueProcessor(
inQueue: container.GetInstance<InQueue>(),
processedQueue: container.GetInstance<ProcessedQueue>(),
errorQueue: container.GetInstance<ErrorQueue>());
Any reason why this wouldn't work in your case?
Well they are all of the same type IQueue, so yes there is a way to do that, but I thought the idea was not to use container.GetInstance or Resolve since it's basically a service locator then.
Why not...
public enum QType { In, Out, Error }
c.Register<IQueueProcessor>(made:
Made.Of(() => new QueueProcessor(Arg.Of<IQueue>(QType.In), Arg.Of<IQueue>(QType.Out), Arg.Of<IQueue>(QType.Error))));
Hi @mika76, I think you misunderstood the Service Locator (anti) pattern. As Mark Seemann explains quite clearly here: "A DI container encapsulated in a Composition Root is not a Service Locator - it's an infrastructure component". Or look at it from a different perspective, the down sides that the Service Locator pattern has (hiding class dependencies, causing run-time errors instead of compile-time errors, and making code more difficult to maintain) are not applicable to the situation where you call the container from within the composition root, because you won't be hiding any dependencies, will not be causing run-time errors, and since your application code is not affected, it has no influence on the maintainability of your application.