In yesterday's blog post daisy-chaining-dependency-injection.html I looked ay how we can daisy-chain dependencies via events by pushing requests for dependencies up the chain. It worked quite well, but there was a lot of plumbing required in the base classes.
Today I'm going to look at daisy-chaining dependency injection via the constructor, which is a very popular way to do dependency injection. We will have a very similar scenario. A configuration class uses a data provider which uses a logging provider. We need to be able to substitute a test data provider and a test logging provider during our unit tests.
This walk though will reveal a problem with the technique of daisy-chaining dependency injection through constructors.
Start by creating a C#, .Net Core console project in Visual Studio and call it DaisyChainedDIViaCtors. Add a class called LoggingProvider. We will create a base logging provider class and inherited production and test classes. Note the base class could have been an interface, but the data provider has to have a base class so I'm using base classes at all levels for consistency.
namespace DaisyChainedDIViaCtors
public abstract class BaseLoggingProvider
{
abstract public string GetName();
public class ProductionLoggingProvider : BaseLoggingProvider
public override string GetName()
return "Production logging provider";
public override void Log(string s)
// Write to log file
}
}
public class TestLoggingProvider : BaseLoggingProvider
public override string GetName()
return "Test logging provider";
public override void Log(string s)
Console.WriteLine(s);
}
}
}
namespace DaisyChainedDIViaCtors
public abstract class BaseDataProvider
{
private BaseLoggingProvider _loggingProvider = null;
get { return _loggingProvider; }
public BaseDataProvider(BaseLoggingProvider loggingProvider)
if (loggingProvider == null)
this.loggingProvider = loggingProvider;
public abstract string GetName();
public class ProductionDataProvider : BaseDataProvider
private SqlConnection sqlConnection;
public ProductionDataProvider(BaseLoggingProvider loggingProvider = null) : base(loggingProvider) { }
public override void CloseConnection()
loggingProvider.Log("Closing connection");
sqlConnection = null;
public override string GetName()
return "Production data provider";
public override Settings GetSettings()
loggingProvider.Log("Getting settings");
throw new System.Exception("Connection is not open");
public override bool IsConnectionOpen()
return (sqlConnection != null && sqlConnection.State == System.Data.ConnectionState.Open);
public override void OpenConnection()
loggingProvider.Log("Opening connection");
}
}
public class TestDataProvider : BaseDataProvider
private bool isConnectionOpen = false;
public TestDataProvider(BaseLoggingProvider loggingProvider = null) : base(loggingProvider) { }
public override void CloseConnection()
loggingProvider.Log("Closing connection");
public override string GetName()
return "Test data provider";
public override Settings GetSettings()
loggingProvider.Log("Getting settings");
throw new System.Exception("Connection is not open");
public override bool IsConnectionOpen()
return (isConnectionOpen);
public override void OpenConnection()
loggingProvider.Log("Opening connection");
}
}
public class Settings
{
public bool isProductionMode;
public class Configuration
{
private BaseDataProvider dataProvider;
public Configuration(BaseDataProvider dataProvider = null)
if (dataProvider is null)
this.dataProvider = dataProvider;
public string GetDataProviderName()
return dataProvider.GetName();
public string GetDataProviderLoggingProviderName()
return dataProvider.loggingProvider.GetName();
public Settings GetSettings()
Settings settings;
dataProvider.OpenConnection();
settings= dataProvider.GetSettings();
dataProvider.CloseConnection();
return settings;
}
}
namespace DaisyChainedDIViaCtors
class Program
{
static void Main(string[] args)
bool isProductionMode = false;
// Instantiate configuration to use a production data provider and a production logging provider
Configuration configuration = new Configuration();
Console.WriteLine(string.Format("IsProductionMode is {0}", isProductionMode));
}
}
Lets do some testing. Add a new NUnit test project to the solution and call it DaisyChainedDIViaCtorsTest. Rename the unit test class to DaisyChainedDIViaCtorsTest. We will write a test that will instantiate Configuration with a test data provider and test logging provider.
namespace DaisyChainedDIViaCtorsTest
public class Tests
{
[Test]
public void TestProvidersTest()
bool IsProductionMode = false;
// Instantiate Configuration to use a test data provider and a test logging provider
BaseLoggingProvider loggingProvider = new TestLoggingProvider();
IsProductionMode = configuration.GetSettings().isProductionMode;
Assert.IsFalse(IsProductionMode);
Assert.AreEqual("Test data provider", configuration.GetDataProviderName());
}
}
This test is important for two reasons. First, it is the most likely scenario. Second, it highlights a problem with this approach.
Because the unit test instantiates the data provider, it is responsible for injecting the desired dependencies. In this case it is only a logging provider. This means the logging provider must be instantiated first, then used to instantiate the data provider. But what if some methods in the logging provider needed a data provider? We would have to inject a data provider into the logging provider via its constructor. How would we do this?
Dependency Injection via the constructor does not provide enough granularity to deal with this scenario on its own. We can work around this with method or property dependency injection but if we have to use those too, why not use them only?
Perhaps there's a way to do dependency injection via the constructor but not by passing actual instances. Perhaps we could just pass enough information for the class to create the instances if and when it needs to. I'll be examining this possibility later.
No comments:
Post a Comment