Reputation: 435
I'm having a little bit of trouble wrapping my head around IoC - specifically using Unity.
Suppose I have an application that I want to use to send emails. I would model it like this:
public interface IEmailSender
{
void SendEmail();
}
And then create some implementations of the interface:
public class GmailEmailSender : IEmailSender
{
public void SendEmail()
{
//code to send email using Gmail
}
}
public class YahooEmailSender : IEmailSender
{
public void SendEmail()
{
//code to send email using Yahoo
}
}
I also have a class to actually send the Emails
public class EmailSender
{
IEmailSender _emailSender;
public EmailSender(IEmailSender emailSender)
{
_emailSender= emailSender;
}
public void Send()
{
_emailSender.SendEmail();
}
}
So I understand how to configure Unity to always use one of the implementations:
IUnityContainer container = new UnityContainer().RegisterType<IEmailSender, GmailEmailSender>());
but what I'm not quite understanding is if I want to choose the YahooEmailSender based on some criteria and the GmailEmailSender based on other criteria, where do I code the logic to make that determination, and in turn inject the appropriate concrete implementation to the EmailSender constructor, without using
EmailSender emailSender = new EmailSender(new YahooEmailSender());
Upvotes: 1
Views: 1845
Reputation: 31282
Your question is fair enough. Resolving dependencies at runtime, based on some user input or configuration settings, is a well-known problem. Mark Seemann devotes separate section of his great book Dependency Injection in .NET to this problem - "6.1 Mapping runtime values to abstractions". And I can't fully agree with NightOwl888 that this is not a DI problem.
There are 2 main solutions for this problem.
First one, described by Mark Seeman in his book, is to have the factory that takes indication of selected implementation as parameter. Here is basic description how it works.
First of all, you should pass somehow which implementation you want to use. It could be just a string (e.g. "gmail", "yahoo"), but it's better to do it via enum
:
public enum EmailTarget
{
Gmail,
Yahoo,
}
Then you should define the factory itself. In common, it will look like this:
public interface IEmailSenderFactory
{
IEmailSender CreateSender(EmailTarget emailTarget);
}
Then you should provide implementation for the factory. It could be as simple as:
public class EmailSenderFactory : IEmailSenderFactory
{
public IEmailSender CreateSender(EmailTarget emailTarget)
{
switch (emailTarget)
{
case EmailTarget.Gmail:
return new GmailEmailSender();
case EmailTarget.Yahoo:
return new YahooEmailSender();
default:
throw new InvalidOperationException($"Unknown email target {emailTarget}");
}
}
}
However, in more complex cases, instances of IEmailSender
should also be created via DI container. In this case you could use factory based on IUnityContainer
:
public class EmailSenderFactory : IEmailSenderFactory
{
private readonly IUnityContainer diContainer;
public EmailSenderFactory(IUnityContainer diContainer)
{
this.diContainer = diContainer;
}
public IEmailSender CreateSender(EmailTarget emailTarget)
{
switch (emailTarget)
{
case EmailTarget.Gmail:
return diContainer.Resolve<GmailEmailSender>();
case EmailTarget.Yahoo:
return diContainer.Resolve<YahooEmailSender>();
default:
throw new InvalidOperationException($"Unknown email target {emailTarget}");
}
}
}
Then you should adjust EmailSender
and inject IEmailSenderFactory
in it. Send()
method should be extended with value of EmailTarget
enum that specifies selected sender:
public class EmailSender
{
private readonly IEmailSenderFactory senderFactory;
public EmailSender(IEmailSenderFactory senderFactory)
{
this.senderFactory = senderFactory;
}
public void Send(EmailTarget emailTarget)
{
var sender = senderFactory.CreateSender(emailTarget);
sender.SendEmail();
}
}
The last thing is proper composition root:
IUnityContainer container = new UnityContainer();
container.RegisterType<IEmailSender, GmailEmailSender>("gmail");
container.RegisterType<IEmailSender, YahooEmailSender>("yahoo");
container.RegisterType<IEmailSenderFactory, EmailSenderFactory>();
And finally when you need to send e-mail:
var sender = container.Resolve<EmailSender>();
sender.Send(EmailTarget.Gmail);
The second approach is more simple. It doesn't use the factory and is based on Unity named dependencies. With this approach your classes could be left as is. Here is the composition root:
IUnityContainer container = new UnityContainer();
container.RegisterType<IEmailSender, GmailEmailSender>("gmail");
container.RegisterType<IEmailSender, YahooEmailSender>("yahoo");
container.RegisterType<EmailSender>("gmail", new InjectionConstructor(new ResolvedParameter<IEmailSender>("gmail")));
container.RegisterType<EmailSender>("yahoo", new InjectionConstructor(new ResolvedParameter<IEmailSender>("yahoo")));
Sender is created in the following way:
var emailSender = container.Resolve<EmailSender>("gmail");
emailSender.Send();
It's up to you to decide which of these approaches to use. Purists will say that the first one is better because you don't mix specific DI container with your application logic. Actually you do, if the factory is based on DI container, but it's concentrated in one place and could be easily replaced. The second approach is however much simpler and could be used for simplest scenarios.
Upvotes: 7