I've written posts describing dependency injection via constructors, methods, properties, and events but these are all homegrown implementations and I've ignored Unity, etc until now. Microsoft has introduced a simple-ish dependency injection service specifically designed for .Net Core, but usable elsewhere. I'm going to look at how to implement dependency injection using this Microsoft service using my standard application -> configuration -> data provider -> logging dependency hierarchy.
Usually you know at design time which classes will need to be injected. Although the ServiceCollection allows generic services to be added, you normally know which ones you will be adding as you design your application. In my example the logging dependency needs to be injected into the data provider and the data provider needs to be injected into the configuration.
Microsoft allows you to write extension methods for each of the injectable classes. If an injectable class needs a dependency injected itself, the extension method handles it. This hides implementation details and also relaxes dependencies to some extent. It's not perfect, but it's not bad.
Most of the code in this example is the same as previous examples so I'm just going to put the production code in one big file and highlight the new stuff.
Start a new Visual Studio 2019 C#, .Net Core console project and call it CodeDI. Use the package manager to install Microsoft.Extensions.DependencyInjection and System.Data.SqlClient. I'm going to assume you have SQLServer installed with a master database.
Make program.cs look like the code below. The most interesting part is ServiceProvider and the ServiceExtensions class. The ServiceProvider statement simply creates a collection of dependencies using the two extension methods we put in the ServiceExtensions class. That collection gets passed to the Configuration constructor so it can pull out the data provider we want it to use.
The ServiceExtensions class contains an extension method for each of the two classes we have identified as being injectable. The logging provider extension method adds a new singleton instance of the desired logging provider type (because we only need one instance). The data provider extension method builds the ServiceProvider we have so far and adds a new singleton instance of the desired data provider type with the ServiceProvider as a parameter so the data provider's constructor can pull out all the dependencies it needs. Note we don't have to know which dependencies it needs - just that it needs some.
If we switch the order of the registrations we have a problem because the logging provider won't exist when the data provider needs it. Unless we check for this, we will get some null property exceptions later. Using GetRequiredService would help by raising the exceptions at the correct place in the code.
namespace CoreDI
class Program
{
static void Main(string[] args)
ServiceProvider services = new ServiceCollection()
}
public static class ServiceExtensions
{
public static IServiceCollection RegisterLoggingProvider<T>(this IServiceCollection services) where T:BaseLoggingProvider
{
services.AddSingleton(typeof(BaseLoggingProvider), Activator.CreateInstance(typeof(T)));
{
ServiceProvider serviceProvider = services.BuildServiceProvider();
}
{
abstract public string GetName();
{
public override string GetName()
return "Production logging provider";
public override void Log(string s)
// write to log file
}
}
{
public override string GetName()
return "Test logging provider";
public override void Log(string s)
Console.WriteLine(s);
}
{
public bool isProductionMode;
{
internal BaseLoggingProvider loggingProvider = null; // This would normally be protected
public BaseDataProvider(ServiceProvider services)
loggingProvider = services.GetService<BaseLoggingProvider>();
public abstract string GetName();
{
public ProductionDataProvider(ServiceProvider services) : base(services) { }
loggingProvider.Log("Closing connection");
return "Production data provider";
loggingProvider.Log("Getting settings");
throw new Exception("Connection is not open");
return (sqlConnection != null && sqlConnection.State == System.Data.ConnectionState.Open);
loggingProvider.Log("Opening connection");
}
{
public TestDataProvider(ServiceProvider services) : base(services) { }
loggingProvider.Log("Closing connection");
return "Test data provider";
loggingProvider.Log("Getting settings");
throw new Exception("Connection is not open");
return (isConnectionOpen);
loggingProvider.Log("Opening connection");
}
{
private BaseDataProvider dataProvider;
dataProvider = services.GetService<BaseDataProvider>();
return dataProvider.GetName();
return loggingProvider.GetName();
return dataProvider.loggingProvider.GetName();
Settings settings;
}
}
If you run the program you will see the expected results.
Now let's see how we test this. Add an NUnit .Net Core, C# project to the solution and call it CoreDITest. Add references to Microsoft.Extensions.DependencyInjection and CoreDI. Rename Test.cs to ConfigurationTest.cs and make the contents look like the code below. It looks very similar to the previous tests we have done. I highlighted the interesting part.
As you can see, the only thing that changed between the production and the test code is the classes we are registering. This is how it should be.
namespace CoreDITest
public class Tests
{
[Test]
ServiceProvider services = new ServiceCollection()
Configuration configuration = new Configuration(services);
Assert.IsFalse(IsProductionMode);
}
}
Build the project. When you run the tests, they all pass. This demonstrates that the test versions of the dependencies have been injected.
No comments:
Post a Comment