2019-02-27

Handling multiple implementations of an interface with Dependency Injection in .Net Core

When you have an interface with multiple implementations and you don't want to use a 3rd party DI framework, you'll need to work around some limitations of the 'native' DI implementation in .Net Core.

To illustrate the problem; let's assume you have an IGreeter interface and two classes that implement it; the HelloGreeter and the HiGreeter. I won't be writing out any code, I'm sure you can imagine what this looks like. We can register both types in .Net by calling, for example, services.AddScoped<IGreeter, HelloGreeter>(); and services.AddScoped<IGreeter, HiGreeter>();. However, when we have a class that requires an IGreeter this will always get the HiGreeter since that was the last registered IGreeter . If we want a more specific greeter we're going to have to disambiguate somehow.

There are some solutions to this problem as, for example, the solutions described by Edi Wang and Dejan Stojanovic. I liked the train of thought of Randy Miller but that would introduce a dependency on a 3rd party which, for whatever reason (usually licensing or strict corporate policies), isn't always an option. I also like poke's answer on StackOverflow.

In the end I gave it a bit of thought and rolled my own. It's pretty simple.

This class is, essentially, nothing more than a specialized KeyValuePair<TKey, TValue>, except that it requires an Enum as a key. It does require C# 7.3 or later because of that, but I like this requirement because it makes your code a lot more refactor-safe than relying on strings (as seen in, for example, the earlier mentioned poke's answer).

Next, we need a provider:

In short, what this provider does is it creates a dictionary of (lazy initialized) factory functions. Other than that it doesn't do very much besides throw an exception with some descriptive message whenever the lookup of a registration failed.

Now, here's where this all started: I have a controller and sometimes it may need to read a physical file from disk and at other times it may need to read a file from an embedded resource. For this you can use a PhysicalFileProvider and a ManifestEmbeddedFileProvider, both described in a bit more detail here. And both implement the IFileProvider interface.

Now let's have a look at our controller:

As you'll note it expects a KeyedRegistrationProvider<FileProviderType, IFileProvider> as constructor argument. The FileProviderType is an enum I introduced to disambiguate the fileproviders:

Which leaves us with only one more thing to do: register the types for DI:

And there you have it. I happen to register the fileproviders as singletons but this works just as well with AddScoped and AddTransienient with the possible 'gotcha' that you need to think a bit harder on when an instance of the actual class is created and when an instance of the KeyedRegistrationProvider is created. As poke put it:
In general, named dependencies are a sign that you are not designing your dependencies properly. If you have two different dependencies of the same type, then this should mean that they may be interchangeably used. If that’s not the case and one of them is valid where the other is not, then that’s a sign that you may be violating the Liskov substitution principle.

[...]

With all that being said, sometimes you really want something like this and having a numerous number of subtypes and separate registrations is simply not feasible. In that case, there are proper ways to approach this though.
Given the circumstances I think this method works okay and is quite nice. It's generic (the introduced KeyedRegistration and KeyedRegistrationProvider are reusable) and and refactor-safe (doesn't depend on magic strings). It does require the instantiated class (our FileController) to have some knowledge it probably shouldn't have (i.e. it should have a constructor accepting two IFileProviders instead of a single KeyedRegistrationProvider<FileProviderType, IFileProvider> as constructor argument). If you really insist on that then you should look into other DI frameworks which, I'm sure, can provide that functionality perfectly fine.

No comments:

Post a Comment