ASP.NET Core supports dependency injection (DI), a great pattern for decoupling classes which would normally perform multiple functions inside the same region of the application. The dependency injection engine of ASP.NET Core, known as a service container, is a functional, albeit basic, way to create a well-factored application with separated concerns. With little additional code, developers can easily select dependencies to add to the container when the conditions are known at application startup, such as whether the app is running from a Pre-Prod or a Production environment.
But what if we want to use this cool thing on a condition that we'd only know at runtime, after the Startup code is long gone? For example, what if we wanted to use different implementations of an interface based on a user's type of account or the user's location? There are additional third-party service containers that augment Microsoft's stock service container with advanced functionality, and in fact, Microsoft's own docs suggest a number of solutions if theirs doesn't suit your needs. But come on, do you really want to have two of these in your app? There's an easy way to resolve dependencies at runtime with our old friend, the factory pattern.
Let's say we are writing a simple app to determine the weather for a location, determined by a postal code the user enters into a text box. One of the requirements is to support both United States ZIP codes and Canadian postal codes, which are formatted differently. Let's also say that, unfortunately, each location requires the use of a different API between the US and Canada, so we can't pick a single implementation at startup and register it. Not to fear, there is an overload on the
.Add() methods for the service container that accepts a
Func<IServiceProvider, TService> parameter. This means that instead of specifying a concrete implementation for the
TService type, we can instead inject a runtime function that takes any number of parameters and returns an object of type
TService! Here's our first crack at taking this approach:
First up, we have our central service. The most important thing to take note of here is the
IWeatherService being injected into the class constructor on line 22.
Next we have the
WeatherService implementation, just wrapping out calls to
WeatherServiceParser factory classes.
And finally, we have our central player, the
Program.cs where the
IWeatherServiceParser factories are configured with a singleton registration function on app start up.
When we run this app, a function taking a string as a parameter to determine which API to use is injected into the
IWeatherService implementation, and it runs the appropriate API based on the postal code that the user entered. This works fine, but there's a bit of a gotcha here. Notice in our injected function we are returning a new implementation each time the factory function is called. This is fine if it's only called a small number of times, but if it runs in a long loop or if the returned service has dependencies of its own that need to be injected, we could be in trouble.
So what can we do? Well, did you notice that we also have access to the service container through the
service variable inside the function in the example above? What we can do is query the container for all services implementing a certain type and then pick the one appropriate for the runtime condition using some LINQ.
Using the service container in this way allows us to take full advantage of the power of dependency injection. Instead of returning a new object, the container returns a reference to the concrete implementation for the requested type, as defined at application startup. Clear and well thought-out strategies for dependency injection like this one can help improve reliability and testability for many apps, small and large.