Dependency injection is a great tool to decouple your software architecture and manage your dependency graph.
At the most fundamental level dependency injection means handing dependencies, ideally as abstractions rather than concretions, as a parameter of some sort instead of directly coupling them to the consuming logic.
This allows to define a foundation for a type of dependency at a low-level in your code, use it in a medium level and decide on the concrete implementation and the proper initialisation of the dependency on a high level.
Example: A logging class could be agnostic to its sink and its source, as long as strings come in and strings go out. So at the foundation level you could create an IStringSource
and an IStringSink
type, use these in a feature level Logging
class and implement a LoggingEventSource : IStringSource
and a ConsoleWriter : IStringSink
in the project layer, where you feed them into the Logger
class.
General Use of the concept
The general, architectural use is that you can decouple your code much more easily. This makes testing and reusing your code simpler and less stressful. Some things might even be impossible to test properly without DI.
Session state management is such an example. If you have your own session state management for an ASP.NET (Core) application testing it in an independent way when relying on the ASP.NET (Core) Controller.HttpContext.Session
is impossible (or at least unbelievably painful). You would have to boot up a whole ASP.NET application and even then all tests would share a common state, breaking the concept of independence. But if you inject an ISession
object to your state management instead of referencing HttpContext.Session
directly, you can build your own concretion that does exactly what you need. In my case that was making a plain old instance field of Dictionary<string, byte[]>
look and behave like a session.
This is also the main means to what Robert C. “Uncle Bob” Martin calls the “Dependency Inversion Principle”. It’s the codification of the example above: instead of relying on concretions that might be less stable than the relying code, rely on abstractions and use the concretion as a value for an abstract input (that may it be a parameter value for instantiated types or a generic type declaration for static ones).
I like to use DI parameters as optional parameters and have a default way to get them if left blank. In the given example that would mean having an optional ISession
parameter and if left blank asking HttpContext
for a session. This way you have the decoupling needed for testing while keeping slim signatures against the rest of your application and without needing the caller to know how to get an ISession instance.
Dependency Inversion Example
IStringSink:
1 2 3 4 5 6 7 | namespace CoreSkills.Examples.Foundation.Logging { public interface IStringSink { void Print(string input); } } |
IStringSource:
1 2 3 4 5 6 7 8 9 10 11 | using System; namespace CoreSkills.Examples.Foundation.Logging { public interface IStringSource { event EventHandler<string> NewLineSourced; void SourceNewLine(string input); } } |
Logger:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using CoreSkills.Examples.Foundation.Logging; namespace CoreSkills.Examples.Feature.Logging { public class Logger { public Logger(IStringSource source, IStringSink sink) { Source = source; Sink = sink; Source.NewLineSourced += (sourceObject, input) => sink?.Print(input); } public IStringSource Source { get; } public IStringSink Sink { get; } public void Log(string input) { Source.SourceNewLine(input); } } } |
ConsoleWriter:
1 2 3 4 5 6 7 8 9 10 11 12 13 | using System; using CoreSkills.Examples.Foundation.Logging; namespace CoreSkills.Examples.Project.Logging { public class ConsoleWriter : IStringSink { public void Print(string input) { Console.WriteLine(input); } } } |
LoggingEventSource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | using System; using CoreSkills.Examples.Foundation.Logging; namespace CoreSkills.Examples.Project.Logging { public class LoggingEventSource : IStringSource { public event EventHandler<string> NewLineSourced; public void SourceNewLine(string input) { NewLineSourced?.Invoke(this, input); } } } |
Calling Example:
1 2 3 4 5 | ConsoleWriter writer = new ConsoleWriter(); LoggingEventSource source = new LoggingEventSource(); Logger logger = new Logger(source, writer); logger.Log("Some string"); logger.Source.SourceNewLine("Event-sourced string"); |
The ASP.NET Core Way
Another key situation where Dependency Injection can be of great value is when you rely on a specific read-only state and want the state management to be in a very clearly defined area. One important situation in ASP.NET Core is getting application settings from the appsettings.json
file(s).
ASP.NET Core allows to bind a type as IOptions
in the startup.cs
. This instance with all its state then can be injected into controller constructors and actions as a parameter.
Now you can use your configuration in your whole application and the only place it needs to be initialized and manipulated is the very place where it belongs: the Startup class!
To improve the structure and the segregation of concerns ASP.NET Core gives you the tools to split up the appsettings.json to different sections that depict individual options classes, so you can only inject what your class really needs.
The ASP.NET Core automated DI registration for Action-Methods knows 3 distinct so called “life times”:
Transient: Instantiated freshly every time the injection is requested.
Scoped: Created once per http request.
Singleton: Created when needed for the first time and then reused until application reboot.
The constructor-injected configuration de facto has a singleton lifetime.
ASP.NET Core DI Examples
appsettings.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | { "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning" } }, "CollectionConfig" : { "Dict": { "some key": "some Value", "another key": "better value", "nonplusultra key": "best value possible!" }, "Array": [ 3, 2, 12, -1, 0] }, "MixedConfig": { "IsHilarious" : true, "SeriousString": "Why so serious?", "SomeNumber": -1, "ThisValueDoesNotExist": true } } |
Options models:
BaseConfig:
1 2 3 4 5 6 7 8 | namespace CoreSkills.Examples.AspNetcore.DependencyInjection.Models { public abstract class BaseConfig { public string Type => GetType().Name; public new abstract string ToString(); } } |
EmptyBaseConfig:
1 2 3 4 5 6 7 8 9 10 | namespace CoreSkills.Examples.AspNetcore.DependencyInjection.Models { public class EmptyBaseConfig : BaseConfig { public override string ToString() { return string.Empty; } } } |
CollectionConfig:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using System.Collections.Generic; using System.Linq; namespace CoreSkills.Examples.AspNetcore.DependencyInjection.Models { public class CollectionConfig : BaseConfig { public Dictionary<string, string> Dict { get; set; } = new Dictionary<string, string>(); public int[] Array { get; set; } = new int[0]; public override string ToString() { return "Dict: " + string.Join("; ", Dict.Select(pair => pair.Key + " => " + pair.Value)) + " || " + "Array: [" + string.Join(",", Array) + "]"; } } } |
MixedConfig:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using System; using System.Globalization; namespace CoreSkills.Examples.AspNetcore.DependencyInjection.Models { public class MixedConfig : BaseConfig { public int SomeNumber { get; set; } public string SeriousString { get; set; } public bool IsHilarious { get; set; } public DateTime UnreferencedTime { get; set; } = DateTime.MinValue; public override string ToString() { return "SomeNumber: " + SomeNumber + " || SeriousString: " + SeriousString + " || IsHilarous: " + IsHilarious + " || UnreferencedTime: " + UnreferencedTime.ToString(CultureInfo.InvariantCulture); } } |
TransientConfig:
1 2 3 4 5 6 7 8 9 10 11 12 | namespace CoreSkills.Examples.AspNetcore.DependencyInjection.Models { public class TransientConfig : BaseConfig { public string Value { get; set; } public override string ToString() { return "Value: " + Value; } } } |
startup.cs:
using services.Configure
for constructor injection.
1 2 3 4 5 6 7 8 9 10 | public void ConfigureServices(IServiceCollection services) { services.Configure<CollectionConfig>(Configuration.GetSection("CollectionConfig")); services.Configure<MixedConfig>(Configuration.GetSection("MixedConfig")); services.AddSingleton<TransientConfig>(new TransientConfig { Value = "Proudly presented by startup.cs" }); services.AddMvc(); } |
Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | using CoreSkills.Examples.AspNetcore.DependencyInjection.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace CoreSkills.Examples.AspNetcore.DependencyInjection.Controllers { public class HomeController : Controller { private readonly CollectionConfig collections; private readonly MixedConfig mixed; public HomeController(IOptions<CollectionConfig> collection, IOptions<MixedConfig> mixed) { collections = collection.Value; this.mixed = mixed.Value; } [Route("{type?}")] public IActionResult Index(string type, [FromServices] TransientConfig transient) { switch (type) { case "collections": return View(collections); case "mixed": return View(mixed); case "Transient": return View(transient); default: return View(new EmptyBaseConfig()); } } } } |
index.cshtml:
1 2 3 4 5 6 7 8 9 10 11 12 | @using CoreSkills.Examples.AspNetcore.DependencyInjection.Models @model BaseConfig <h1>ModelType: @Model.Type</h1> @Model.ToString() also try: <ul> <li><a href="/">None</a></li> <li><a href="/Collections">Collections</a></li> <li><a href="/Mixed">Mixed</a></li> <li><a href="/Transient">Transient</a></li> </ul> |
As usual the full code inclusive the omitted comments and tests to see usage can be found on github.
Closing words
The downside: so far I have not found a satisfactory way to automatically and directly inject IOptions<>
(or their plain values) to non-ASP.NET Core-derivative classes. You can add those as parameter, but they won’t be injected automatically. So far the best way I found to inject options to custom classes is to retrieve them in the controller and pipe them to where you need them; even if the intermediate classed have no own use for them. If you solved this issue leave a comment. It is a nuisance to inject things manually that should be there automatically.