Thursday, September 10, 2020

Daisy-chaining dependency injection via events

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.

using System;
 
namespace EventsFromBaseClass
{
    public class ProvideLoggingProviderEventArgs : EventArgs
    {
        public BaseLoggingProvider loggingProvider;
    }
 
    public  abstract class BaseLoggingProvider
    {
        public abstract string GetName();
        public abstract void Log(string s);
    }
 
    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;

using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
 
namespace EventsFromBaseClass
{
    public class ProvideDataProviderEventArgs : EventArgs
    {
        public BaseDataProvider dataProvider;
    }
 
    public abstract class BaseDataProvider
    {
        public event EventHandler<ProvideLoggingProviderEventArgs> ProvideLoggingProvider;
 
        private BaseLoggingProvider _LoggingProvider = null;
        public BaseLoggingProvider LoggingProvider  // In real life this is protected
        {
            get
            {
                if (_LoggingProvider == null)
                    if (ProvideLoggingProvider == null)
                        _LoggingProvider = new ProductionLoggingProvider();
                    else
                    {
                        ProvideLoggingProviderEventArgs e = new ProvideLoggingProviderEventArgs();
                        ProvideLoggingProvider.Invoke(this, e);
                        _LoggingProvider = e.loggingProvider;
                    }
                return _LoggingProvider;
            }
        }
 
        public abstract void OpenConnection();
        public abstract void CloseConnection();
        public abstract Settings GetSettings();
        public abstract bool IsConnectionOpen();
        public abstract string GetName();
    }
 
    public class ProductionDataProvider : BaseDataProvider
    {
        private SqlConnection sqlConnection = null;
 
        public override void CloseConnection()
        {
            LoggingProvider.Log("Closing connection");
            sqlConnection.Close();
            sqlConnection = null;
        }
 
        public override Settings GetSettings()
        {
            LoggingProvider.Log("Getting settings");
            if (IsConnectionOpen())
                return new Settings() { IsProductionMode = true };  // This would be read from the database
            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");
            sqlConnection = new SqlConnection("Server=(local);database=master;Trusted_Connection=true");
            sqlConnection.Open();
        }
 
        public override string GetName()
        {
            return "Production data provider";
        }
    }
 
    public class TestDataProvider : BaseDataProvider
    {
        private bool isConnectionOpen = false;
 
        public override void CloseConnection()
        {
            LoggingProvider.Log("Closing connection");
            isConnectionOpen = false;
        }
 
        public override Settings GetSettings()
        {
            LoggingProvider.Log("Getting settings");
            if (IsConnectionOpen())
                return new Settings() { IsProductionMode = false };
            else
                throw new Exception("Connection is not open");
        }
 
        public override bool IsConnectionOpen()
        {
            return isConnectionOpen;
        }
 
        public override void OpenConnection()
        {
            LoggingProvider.Log("Opening connection");
            isConnectionOpen = true;
        }
 
        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.


using System;
 
namespace EventsFromBaseClass
{
    public class Settings
    {
        public bool IsProductionMode;
    }
 
    public class Configuration
    {
        public event EventHandler<ProvideDataProviderEventArgs> ProvideDataProvider;
        public event EventHandler<ProvideLoggingProviderEventArgs> ProvideLoggingProvider;
 
        private BaseDataProvider _DataProvider = null;
        public BaseDataProvider DataProvider    // In real life this is protected or private
        {
            get
            {
                if (_DataProvider == null)
                    if (ProvideDataProvider == null)
                        _DataProvider = new ProductionDataProvider();
                    else
                    {
                        ProvideDataProviderEventArgs e = new ProvideDataProviderEventArgs();
                        ProvideDataProvider.Invoke(this, e);
                        _DataProvider = e.dataProvider;
                    }
                _DataProvider.ProvideLoggingProvider += Provider_ProvideLoggingProvider;
                return _DataProvider;
            }
        }
 
        private BaseLoggingProvider _loggingProvider = null;
        private void Provider_ProvideLoggingProvider(object sender, ProvideLoggingProviderEventArgs e)
        {
            if (_loggingProvider == null)
            {
                if (ProvideLoggingProvider == null)
                    _loggingProvider = new ProductionLoggingProvider();
                else
                {
                    // Pass it up the chain
                    ProvideLoggingProviderEventArgs e2 = new ProvideLoggingProviderEventArgs();
                    ProvideLoggingProvider.Invoke(this, e2);
                    _loggingProvider = e2.loggingProvider;
                }
            }
            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.


using System;
 
namespace EventsFromBaseClass
{
    class Program
    {
        static void Main(string[] args)
        {
            bool isProductionMode = false;
 
            Configuration configuration = new Configuration();
            configuration.ProvideDataProvider += Configuration_ProvideDataProvider;
            configuration.ProvideLoggingProvider += Configuration_ProvideLoggingProvider;
 
            isProductionMode = configuration.GetSettings().IsProductionMode;
 
            Console.WriteLine(string.Format("isProductionMode = {0}", isProductionMode));
            Console.WriteLine(string.Format("Data Provider name is {0}", configuration.DataProvider.GetName()));
            Console.WriteLine(string.Format("Logging name is {0}", configuration.DataProvider.LoggingProvider.GetName()));
        }
 
        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.



Now it is time to write a test that injects test classes. Add an NUnit test project to the solution and call it EventsFromBaseClassTest. Add a reference to the EventsFromBaseClassProject. Rename the class ConfigurationTest and make the code look like this...

using NUnit.Framework;
using EventsFromBaseClass;
using System;
 
namespace EventsFromBaseClassesTest
{
    public class Tests
    {
        [Test]
        public void Test1()
        {
            bool isProductionMode = false;
            Configuration configuration = new Configuration();
            configuration.ProvideDataProvider += Configuration_ProvideDataProvider;
            configuration.ProvideLoggingProvider += Configuration_ProvideLogging;
 
            isProductionMode = configuration.GetSettings().IsProductionMode;
 
            Assert.IsFalse(isProductionMode);
            Assert.AreEqual("Test data provider", configuration.DataProvider.GetName());
            Assert.AreEqual("Test logging provider", configuration.DataProvider.LoggingProvider.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