Think of a scenario where we want more than one instance of a given service; or when we have to make the decision about whether to instantiate a component/service at runtime. For such scenarios, injecting a service as a
Direct Dependency or
Lazy Dependency will not be enough. Instead, injecting the service dependency as a Func will do, as it provides the dynamism we require.
Consider the following example, where we have a PaymentProcessor class which has a dependency on the IPaymentGateway service. And, the decision whether to instantiate the payment gateway or not, is based on the user's choice of PaymentMode (say cash or card). The below code shows the use of a Func in this scenario
- public interface IPaymentGateway
- {
- void InitiatePayment(TransactionDetails transactionDetails);
- }
-
- public class PaymentProcessor
- {
- private Func<IPaymentGateway> _paymentGatewayInitializer;
- public PaymentProcessor(Func<IPaymentGateway> paymentGatewayInitializer)
- {
- _paymentGatewayInitializer = paymentGatewayInitializer;
- }
-
- public void ProcessPayment(TransactionDetails transactionDetails)
- {
- if(transactionDetails.PaymentMode != PaymentMode.Cash)
- {
- using(var paymentGateway = _paymentGatewayInitializer())
- {
- paymentGateway.InitiatePayment(amount);
- }
- }
- }
- }
It is important to note that there can be a scenario where a user chooses to pay in cash. In this case, we don't need to instantiate IPaymentGateway. Also, we shall have a new instance of the class implementing IPaymentGateway, every time we initiate a new transaction.
Why not Lazy<T>?
I know, you must be thinking
why not use Lazy initialization here? What is the benefit of using Func over Lazy in this scenario? If we use Lazy, the code would look like the following
- public class PaymentProcessor
- {
- private Lazy<IPaymentGateway> _paymentGateway;
- public PaymentProcessor(Lazy<IPaymentGateway> paymentGateway)
- {
- _paymentGateway = paymentGateway;
- }
-
- public void ProcessPayment(TransactionDetails transactionDetails)
- {
- if(transactionDetails.PaymentMode != PaymentMode.Cash)
- {
- _paymentGateway.Value.InitiatePayment(amount);
- }
- }
- }
While everything looks good at first, there is a problem. When the payment gateway is initialized, it is actually resolved in the same scope as the PaymentProcessor. This means that the payment gateway will be held in memory until the time PaymentProcessor is released. And therefore, we will always have the same instance of payment gateway in every transaction.
Register & Resolve
For this article, I'm using Autofac as the DI Container. The registration of the components will be quite simple as shown in the below code
- var builder = new ContainerBuilder();
- builder.RegisterType<TransactionDetails>().InstancePerLifetimeScope();
- builder.RegisterType<PaymentProcessor>().InstancePerLifetimeScope();
- builder.RegisterType<IPaymentGateway>().InstancePerLifetimeScope();
-
- var container = builder.Build();
- using(var scope = container.BeginLifetimeScope())
- {
-
-
- var paymentProcessor = scope.Resolve<PaymentProcessor>();
- paymentProcessor.ProcessPayment(transactionDetails);
- }
Because the IPaymentGateway has been registered as a Func, the container will not resolve it with the PaymentProcessor. In fact, the container leaves it up to the PaymentProcessor to initialize IPaymentGateway using the auto-generated factory method. Therefore, as we can see in the above code, we are initializing the IPaymentGateway using the _paymentGatewayInitializer.
Lifetime Scope
In the above example, we have registered the IPaymentGateway as InstancePerLifetimeScope(). This is to say that, every time we resolve the Func<IPaymentGateway> dependency, we will get a new instance of IPaymentGateway. Behind the scenes, Autofac creates an internal scope in which the dependency is resolved. In our case, inside the using block. When we don't need the object anymore, the scope is released and therefore the dependency object is disposed of.
However, if we register a component as SingleInstance() and resolve Func<T> multiple times, we will get the same object instance every time.
Summary
While injecting dependencies, we need to take extra care about when the dependency must be initialized and for how long it should stay in memory. Though there is a lot more to that, being able to identify the nature of our dependency is a good start.