Simplifying Dependency Injection with Functions over Interfaces

In my last post, I showed how using a function delegate can be used to create named or keyed dependency injection resolutions.

When I was writing the demo code, it struck me that the object orientated code I was writing seemed to be bloated for what I was trying to achieve.

Now don’t get me wrong, I am a big believer in the SOLID principles, but when the interface has only a single function, having to create multiple class implementations seems overkill.

This was the ‘aha’ moment you sometimes get as a developer where reading and going to user groups or conferences plants seeds in your mind that make you think about problems differently. In other words, if the only tool you have in your tool-belt is a hammer, then every problem looks like a nail; but if you have access to other tools, then you can choose the right tool for the job.

Over the past few months, I have been looking more at functional programming. This was initially triggered by the excellent talk ‘Functional C#’ given by Simon Painter (a YouTube video from the Dot Net Sheffield user group is here, along with my own talk – plug, plug, here).

In the talk, Simon advocates using functional programming techniques in C#. At the end of the talk, he recommends the Functional Programming in C# book by Enrico Buonanno. It is in Chapter 7 of this book that the seed of this blog was planted.

In short, if your interface has only a single method that is a pure function, register a function delegate instead of an interface.

This has several benefits

  • There is a less code to write
  • The intent of what is being provided is not masked by having an interface getting in the way – it is just the function exposed via a delegate
  • There is no object creation of interface implementations, so less memory allocations and may be faster to initially execute (as there is no object creation involved)
  • Mocking is easier – you are just providing an implementation of a function signature without the cruft of having to hand craft an interface implementation or use a mocking framework to mock the interface for you.

So with this in mind, I revisited the demo from the last post and performed the following refactoring:

  • Replaced the delegate signature to perform the temperature conversion instead of returning an interface implementation that has a method
  • Moved the methods in the class implementations to static functions within the startup class (but could easily be a new static class)
  • Change the DI registration to return the results from the appropriate static function instead of using the DI container to find the correct implementation and forcing the caller to execute the implementation’s method.

As can be seen from the two code listings, Listing 1 (Functional) is a lot cleaner than Listing 2 (Object Orientated).

Listing 1

Listing 2

Having done this, it also got me thinking about how I approach other problems. An example of this is unit-testable timestamps.

Previously, when I needed to use a timestamp to indicate the current date and time, I would create an interface of ICurrentDateTime that would have a property for the current datetime. The implementation for production use would be a wrapper over the DateTime.Now property, but for unit testing purposes would be a mock with a fixed date and time to fulfill a test criteria.

Whilst not a pure function, the same approach used above can be applied to this requirement, by creating a delegate to return the current date and time and then registering the delegate to return the system’s DateTime.Now.

This achieves the same goal of decoupling code from the system implementation via an abstraction, but negates the need to create an unnecessary object and interface to simply bridge to the underlying system property.

If you are interested in looking at getting into functional programming while staying within the comfort zone of C#, I highly recommend Enrico’s book.

The demo of both the OO and Functional approaches can be found in the GitHub project at https://github.com/configureappio/NamedDiDemo.

 

 

 

Getting Dependencies by Name or Key using the .NET Core Container (Part 2)

If you have come here from a search engine, I suggest reading Part 1 first to get some background as to the problem that this post solves.

In Part 1, I gave the background as to why you may have a requirement to have a way of resolving instances from a DI container using a string name or some other key.

In this part, I propose a solution that leaves your code ignorant of any DI container and that is easily mockable for unit tests.

Making Use of Strongly Typed Delegates

A key thing to understand about DI containers is that the service type that is registered is exactly that – a type, not just interfaces and classes. Therefore, you can register delegates as well.

The idea of using delegates for resolution is not new.  There is an example in the Autofac documentation and this article on c-sharpcorner.

However, for .NET Core, when you search for something like ‘.NET Core named dependency injection’, you tend to only find the answer buried in StackOverflow answers.

That is why I have written this post, so that I can find the answer later myself, and also hopefully help other find it too.

Func<T> Delegates Vs. Strongly Typed Delegates

Before getting into a detail of using delegates as service types, we need to talk about generic Func delegates vs strongly typed delegates.

The generic Func type was introducted in C# 3.0 and is a generic delegate included in the System namespace that has multiple overloads that accept between zero and sixteen input parameters (denoted T1 to T16) and a single output parameter (TResult). These are convenience types so that as a developer you do not need to repeatedly create your own customer delegates.

For the purposes of retrieving an instance of a type from the DI container, the various Func delegates can be used as the type for service registration and as a parameter to the constructors of classes. E.g. Func<string, IServiceType>.

However, I have two reasons for not using this approach.

  1. If you change the signature of the service registration to a different overload of Func, you have no tools in the IDE to reflect that change throughout all the references (as the dependency resolution does not take place until runtime). Instead you will need to perform a text ‘search and replace’ task across your whole code base (and hope that there are no other uses of the generic signature for other purposes). By using a strongly typed delegate, this is a unique type and therefore, when changing the signature of the delegate, the compiler will indicate when references and instances are broken (or a tool like Reshaper will do the work for you)
  2. Though this is probably a very rare occurrence, you may have a need to have two different factories with the same signature. If the generic Func<T, IServiceType> is used, it will not be possible to differentiate the two factories. With strongly typed delegates, it is the delegate type that is registered and referenced in the constructor, so the same signature can be used with two different delegate types and the DI container will resolve tHem correctly.

How Does Using Typed Delegates Help?

By using typed delegates, the code that requires the named/keyed dependency is ignorant of the DI container technology that is resolving the instance for it.

This differs from other approaches where either the consuming class itself or an intercepting factory class takes the IServiceProvider as a dependency in its constructor and has logic to resolve the instance itself which defeats the purpose of dependency injection as it is creating a glue between the classes.

By registering the delegate within the DI container registration code, it keeps the codebase (outside of Di registration) ignorant of how the name/key resolution is working.

The Demo

For the rest of this post, I am going to use a test project (which can be found on GitHub at https://github.com/configureappio/NamedDiDemo) that converts temperatures to the Kelvin scale from four different scales – Centigrade, Fahrenheit, Rankine and Kelvins.

To do this, there are four formulas that are needed

Calculation Formula
Celcius(°C) to Kelvin(K) T(K) = T(°C) + 273.15
Fahrenheit(°F) to Kelvin(K) T(K) =  (T(°F) + 459.67) x 5 / 9
Rankine to Kelvin(K) T(K) = T(°R) × 5/9
Kelvin(K) to Kelvin(K) T(K) = T(K)

Each formula is implemented as a mapper class with an implementation of the IKelvinMapper interface as defined below.

Each of the implementations is registered with the DI container

For the caller to retrieve the correct conversion, we need a delegate that uses the input temperature scale in order to retrieve the correct mapper and return the mapper as the IKelvinMapper interface

Lastly, a lookup function is registered with the DI container against the delegate type as the service type to perform the lookup of the correct mapper. To keep things simple, the lookup is based on the first (capitalised) letter of the input temperature scale (normally as more robust lookup would be used, but I’ve tried to keep it simple so as not to distract from the core purpose of what I am demonstrating).

With the returned interface implementation, a conversation from the input temperature scale and value can be made to a consistent value in the Kelvin scale.

So How Does It Work?

The demo works on a very simple basis of leveraging the DI container to do all the hard work of determining which mapping converter is required. The caller just needs to know that it uses the delegate to take in the input temperature scale and value and return the value in Kelvins.

Object Instantiation

Depending on the complexity of the type to be instantiated, there are three ways to instantiate the object:

  1. Perform the instantiation with a ‘new’ keyword. This is fine if the constructor of the type has no dependencies and a new instance is required on each occasion. However, I would discourage this approach as it is not an efficient way of managing the lifetimes of the objects and could lead to problems with garbage collection due to long lived objects
  2. Rely on the DI container to instantiate the object. This is the approach taken in the demo. This has two benefits. The first is that if the object can be a singleton, then the DI container takes care of that for you provided the type has been registered as a singleton lifetime when registered. The second is that if the type has dependencies in the constructor, the DI container will resolve these for you
  3. Lastly, if one or more parameters to the type constructor need the be generated from the delegate input (and therefore not directly resolvable from prior DI registrations), but other parameters can be resolved, then the ActivatorUtilities CreateInstance method can be used to create an instance using a hybrid of provided parameters and using the DI container to resolve those parameters not provided.

This is where you take advantage of the ability to register a service resolution using a lambda expression as it provides access to the IServiceProvider container instance to retrieve instances from the service provider for (2) and (3) above.

Deciding Which Type to Instantiate

For simple keys, you can use a case statement inside a lambda expression to derive the type required and let the DI container perform the object instantiation for you. (This is the approach I have used in the demo project).

However, if you have many different items or complex keys, you may want to consider other ways of deriving the service type required.

For example, by registering a singleton Dictionary<string, serviceType> to map keys to types, if there are many keys, you may gain a performance boost from the hash lookup algorithm.

Instead of using the out-of-the-box generic dictionary thought, I recommend creating a dedicated class that implements IDictionary<string, Type> for the same reasons that I recommend using custom delegates over the generic Func<T> – it removes any ambiguity if there is (deliberately or inadvertently) more than one registration of the generic dictionary.

Care with Scoped Lifetimes

Careful thought needs to be given to the lifetime of both the factory delegate and the object being retrieved from the DI container.

Typically, as factory classes have no state,  they can usually be registered with a singleton lifetime. The same applies to the delegate being used as the factory or to access the factory class.

Where things get complicated is when the service type that the factory is instantiating for you has been registered in the DI container as having a scoped lifetime.

In this case, the resolution within the factory fails as the DI container cannot resolve scoped dependencies for constructor injection inside a singleton or transient lifetime object (in our case, the singleton factory delegate).

There are two ways to get around this problem.

(i) Register the factory delegate as scoped as well

(ii) Change the singleton factory delegate to take an IServiceProvider as part of the function signature. It is then the caller’s responsibility to pass the correctly scoped IServiceProvider to the delegate. However, this effectively takes us back to a service locator pattern again.

Accessing the Delegate for Calling

Now that we have abstracted the mapper instance generation into a delegate function, the client just needs to be able to resolve the instance.

For classes that want to use the mapper (and are also registered with the DI container), this is simply a case of making the delegate a constructor parameter.

Conclusion

Hopefully, these two posts have provided some insight into how the limitations of the Microsoft DI container to support named or keyed resolutions can be worked around without having to resort to using a third-party container or custom service locator pattern implementation.

Whilst not part of this two-part blog post, I have written another post about simplifying the demo to work in a more functional way by replacing the interface implementations with direct functions to (a) reduce the amount of code and (b) reduce the number of memory allocations required to create the objects.