In my earlier post dependency-injection-using-events-to.html I examined how we could do dependency injection via events. In reality we need to inject dependencies into objects created by objects we directly instantiate. For example, we instantiate a configuration object and use dependency injection to tell it to instantiate a particular data provider. But that data provider needs to instantiate a specific logging object, etc. In a real life scenario there can be a dozen objects that need to be injected which gets complicated.
By using events to perform only-if-needed dependency injection, we can daisy chain them. So when the data provider needs to log something it can ask the configuration object what logging mechanism to use and the configuration object simply asks the calling object and passes the answer back down. This plumbing can be hidden in base classes.
The calling object or test runner needs to provide an event handler to specify the data provider and another for the logger. We can daisy-chain properties or constructors too which I will do in later posts.
Start a new C# .Net Core console app called EventsFromBaseClass. Add a new class called LoggingProvider.cs. We will put a LoggingProvider base class and a Production and Test derived class here. I could have used an interface instead of a base class, but the data provider will need an abstract base class so I decided to use one here too for consistency. I also define the event args that is used to pass the logging provider instance back from the event handler.
namespace EventsFromBaseClass
public class ProvideLoggingProviderEventArgs : EventArgs
public BaseLoggingProvider loggingProvider;
public abstract class BaseLoggingProvider
{
public abstract string GetName();
public class ProductionLoggingProvider : BaseLoggingProvider
public override string GetName()
return "Production logging provider";
public override void Log(string s)
// Write to a log file
}
}
public class TestLoggingProvider : BaseLoggingProvider
public override string GetName()
return "Test logging provider";
public override void Log(string s)
Console.WriteLine(s);
}
}
}
Now we can define our data providers. The data providers will need to ask the caller which kind of logging provider they should use. So they define a ProvideLoggingProvider event. The data providers expose some typical data provider functionality. In addition, the LoggingProvider property will raise the ProvideLoggingProvider event if it needs to do some logging and doesn't currently have a logging provider. This will ask the calling object what kind of logging provider it should use.
using System;
namespace EventsFromBaseClass
public class ProvideDataProviderEventArgs : EventArgs
public BaseDataProvider dataProvider;
public abstract class BaseDataProvider
{
public event EventHandler<ProvideLoggingProviderEventArgs> ProvideLoggingProvider;
private BaseLoggingProvider _LoggingProvider = null;
{
get
{
if (_LoggingProvider == null)
{
ProvideLoggingProviderEventArgs e = new ProvideLoggingProviderEventArgs();
}
return _LoggingProvider;
}
public abstract void OpenConnection();
public class ProductionDataProvider : BaseDataProvider
private SqlConnection sqlConnection = null;
public override void CloseConnection()
LoggingProvider.Log("Closing connection");
sqlConnection = null;
public override Settings GetSettings()
LoggingProvider.Log("Getting settings");
else
throw new Exception("Connection is not open");
public override bool IsConnectionOpen()
return (sqlConnection != null && sqlConnection.State == ConnectionState.Open);
public override void OpenConnection()
LoggingProvider.Log("Opening connection");
}
public override string GetName()
return "Production data provider";
}
public class TestDataProvider : BaseDataProvider
private bool isConnectionOpen = false;
public override void CloseConnection()
LoggingProvider.Log("Closing connection");
public override Settings GetSettings()
LoggingProvider.Log("Getting settings");
throw new Exception("Connection is not open");
public override bool IsConnectionOpen()
return isConnectionOpen;
public override void OpenConnection()
LoggingProvider.Log("Opening connection");
public override string GetName()
return "Test data provider";
}
}
The configuration object just has the task of fetching a Settings object. We don't need production and test versions of this - it's the class we're testing. However, we do need to tell it what type of data provider and what type of logging provider to use. We will do this when it raises ProvideDataProvider and ProvideLoggingProvider events. It can ask for a logging provider either when it needs one or when its data provider asks for one. Look at the DataProvider and LoggingProvider properties.
namespace EventsFromBaseClass
public class Settings
{
public bool IsProductionMode;
public class Configuration
{
public event EventHandler<ProvideDataProviderEventArgs> ProvideDataProvider;
private BaseDataProvider _DataProvider = null;
{
get
{
if (_DataProvider == null)
{
ProvideDataProviderEventArgs e = new ProvideDataProviderEventArgs();
}
_DataProvider.ProvideLoggingProvider += Provider_ProvideLoggingProvider;
return _DataProvider;
}
private BaseLoggingProvider _loggingProvider = null;
if (_loggingProvider == null)
if (ProvideLoggingProvider == null)
{
// Pass it up the chain
ProvideLoggingProviderEventArgs e2 = new ProvideLoggingProviderEventArgs();
}
}
e.loggingProvider = _loggingProvider;
}
public Settings GetSettings()
Settings settings;
DataProvider.OpenConnection();
settings = DataProvider.GetSettings();
DataProvider.CloseConnection();
return settings;
}
}
Now it is time to do some dependency injection. Alter program.cs to look like this. You can see we define two event handlers. If you put a break point on Configuration_ProvideDataProvider you will see the data provider asks configuration what kind of logging provider it should use and configuration asks us.
namespace EventsFromBaseClass
class Program
{
static void Main(string[] args)
bool isProductionMode = false;
Configuration configuration = new Configuration();
configuration.ProvideLoggingProvider += Configuration_ProvideLoggingProvider;
isProductionMode = configuration.GetSettings().IsProductionMode;
Console.WriteLine(string.Format("isProductionMode = {0}", isProductionMode));
private static void Configuration_ProvideLoggingProvider(object sender, ProvideLoggingProviderEventArgs e)
e.loggingProvider = new ProductionLoggingProvider();
private static void Configuration_ProvideDataProvider(object sender, ProvideDataProviderEventArgs e)
e.dataProvider = new ProductionDataProvider();
}
}
Although there is quite a lot of wiring up required, it is mostly in the base classes so the inherited classes can be kept focused on doing what they were written to do. If you run the program now you will see the production providers are being used.
namespace EventsFromBaseClassesTest
public class Tests
{
[Test]
public void Test1()
bool isProductionMode = false;
configuration.ProvideLoggingProvider += Configuration_ProvideLogging;
isProductionMode = configuration.GetSettings().IsProductionMode;
Assert.IsFalse(isProductionMode);
Assert.AreEqual("Test data provider", configuration.DataProvider.GetName());
private static void Configuration_ProvideLogging(object sender, ProvideLoggingProviderEventArgs e)
e.loggingProvider = new TestLoggingProvider();
private static void Configuration_ProvideDataProvider(object sender, ProvideDataProviderEventArgs e)
e.dataProvider = new TestDataProvider();
}
}
The test defines Configuration and the event handlers in the same way as Program.cs but the event handlers return test providers instead of production providers. Our asserts pass, which tells us the test providers are being used.
If you run the test it will pass.
No comments:
Post a Comment