Tuesday, September 15, 2020

Dependency Injection via properties

 Dependency Injection via properties is one of the three recognized patterns for dependency injection. The other two are via  constructors and methods. It is the simplest to implement but the trickiest to consume. The major constraints are that timing is critical and no code in the constructor can require a dependency. I'm going to walk through creating a daisy-chained dependency injection using properties using the same program/configuration/data provider/logging provider architecture that I've been using in all my dependency injection posts.

Start a new C#, .Net Core console project in Visual Studio and call it DIViaProperties. Use NuGet to add SQLClient.

Add a class called LoggingProvider. It's the same logging provider we've been using for all the dependency injection projects.

using System;
 
namespace DIViaProperties
{
    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 log file
        }
    }
 
    public class TestLoggingProvider : BaseLoggingProvider
    {
        public override string GetName()
        {
            return "Test logging provider";
        }
 
        public override void Log(string s)
        {
            Console.WriteLine(s);
        }
    }
}
 

Add a class called DataProvider. This is similar to previous data providers except it exposes a public loggingProvider property that is intended to be set by the calling code. If the calling code has not specified a logging provider by the time we need it, we will instantiate a production logging provider. This is why timing is critical. The calling code must understand when we will need the logging provider. As a software architect, I see this as a problem. On the plus side, the dependency injection code is simpler.

using System;
using System.Data;
using System.Data.SqlClient;
 
namespace DIViaProperties
{
    public abstract class BaseDataProvider
    {
        private BaseLoggingProvider _loggingProvider = null;
        protected SqlConnection sqlConnection;
 
        public BaseLoggingProvider loggingProvider 
        {
            get
            {
                if (_loggingProvider == null)
                    _loggingProvider = new ProductionLoggingProvider();
 
                return _loggingProvider;
            }
            set
            {
                _loggingProvider = value;
            }
        }
 
        public abstract string GetName();
        public abstract void OpenConnection();
        public abstract Settings GetSettings();
        public abstract void CloseConnection();
        public abstract bool IsConnectionOpen();
    }
 
    public class ProductionDataProvider : BaseDataProvider
    {
        public override string GetName()
        {
            return "Production data provider";
        }
 
        public override void CloseConnection()
        {
            loggingProvider.Log("Closing connection");
            sqlConnection.Close();
        }
 
        public override Settings GetSettings()
        {
            loggingProvider.Log("Getting settings");
            if (IsConnectionOpen())
                return new Settings() { isProductionMode = true };
            else
                throw new Exception("Connection is closed");
        }
 
        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 class TestDataProvider : BaseDataProvider
    {
        private bool isConnectionOpen = false;
 
        public override string GetName()
        {
            return "Test data provider";
        }
 
        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 closed");
        }
 
        public override bool IsConnectionOpen()
        {
            return isConnectionOpen;
        }
 
        public override void OpenConnection()
        {
            loggingProvider.Log("Opening connection");
            isConnectionOpen = true;
        }
    }
}

Add a class called Configuration. It will instantiate the data provider. There are two models we could use.

  1. Make the top level code responsible to providing dependencies to all levels below it.
  2. Make each level responsible for providing dependencies to its direct children only

We will start by looking at option 1 first. Configuration will know about the data provider, but does not know that data provider needs a logging provider. This will be handled by the top level code.

using System;
 
namespace DIViaProperties
{
    public class Settings
    {
        public bool isProductionMode;
    }
 
    public class Configuration
    {
        private BaseDataProvider _dataProvider;
 
        public BaseDataProvider dataProvider   
        {
            get
            {
                if (_dataProvider == null)
                    _dataProvider = new ProductionDataProvider();
 
                return _dataProvider;
            }

            set
            {
                _dataProvider = value;
            }
        }
 
        public Settings GetSettings()
        {
            Settings settings;
            dataProvider.OpenConnection();
            settings = dataProvider.GetSettings();
            dataProvider.CloseConnection();
            return settings;
        }
    }
}

Modify Program.cs to look like this. We are using the production providers so it doesn't have to do much.

using System;
 
namespace DIViaProperties
{
    class Program
    {
        static void Main(string[] args)
        {
            Configuration configuration = new Configuration();
            Settings settings;
 
            settings = configuration.GetSettings();
 
            Console.WriteLine(string.Format("IsProductionMode is {0}", settings.isProductionMode));
            Console.WriteLine(string.Format("Data provider is {0}", configuration.dataProvider.GetName()));
            Console.WriteLine(string.Format("Logging provider is {0}", configuration.dataProvider.loggingProvider.GetName()));
        }
    }
}

Add a new NUnit test project and call it DIViaPropertiesTest. Add a reference to DIViaProperties and change Test1 to DIViaPropertiesTest. The code needs to know that the data provider needs a logging provider and it must pass it before calling any method that uses the data provider. If configuration used several providers that need to log, then this setup would become complex and brittle. This violates the Law of Demeter.

using DIViaProperties;
using NUnit.Framework;
using System.Diagnostics;
 
namespace DIViaPropertiesTest
{
    public class Tests
    {
        [Test]
        public void ConfigurationTest()
        {
            Configuration configuration = new Configuration();
            Settings settings;
 
            configuration.dataProvider = new TestDataProvider();
            configuration.dataProvider.loggingProvider = new TestLoggingProvider();
            settings = configuration.GetSettings();
 
            Assert.IsFalse(settings.isProductionMode);
            Assert.AreEqual("Test data provider", configuration.dataProvider.GetName());
            Assert.AreEqual("Test logging provider", configuration.dataProvider.loggingProvider.GetName());
        }
    }
}

Option 2 is to have Configuration responsible for passing dependencies to all it's children. Program.cs still needs to provide a logging provider, but it doesn't need to know how it is used. This is a slight improvement. However this means the providers must be specified bottom-up which adds complexity if two providers need each other.

Configuration now looks like this.

using System;
 
namespace DIViaProperties
{
    public class Settings
    {
        public bool isProductionMode;
    }
 
    public class Configuration
    {
        private BaseDataProvider _dataProvider;
        public BaseDataProvider dataProvider   
        {
            get
            {
                if (_dataProvider == null)
                    _dataProvider = new ProductionDataProvider();
 
                return _dataProvider;
            }
 
            set
            {
                _dataProvider = value;
                _dataProvider.loggingProvider = loggingProvider;
            }
        }
 
        private BaseLoggingProvider _loggingProvider;
        public BaseLoggingProvider loggingProvider
        {
            get
            {
                if (_loggingProvider == null)
                    _loggingProvider = new ProductionLoggingProvider();
 
                return _loggingProvider;
            }
 
            set
            {
                _loggingProvider = value;
            }
        }
 
        public Settings GetSettings()
        {
            Settings settings;
            dataProvider.OpenConnection();
            settings = dataProvider.GetSettings();
            dataProvider.CloseConnection();
            return settings;
        }
    }
}
 
DIViaPropertiesTest now looks like this. Note that the logging provider is defined before the data provider because it must be there when the data provider is defined. In a way, configuration still needs to know that the data provider needs a logging provider.

using DIViaProperties;
using NUnit.Framework;
using System.Diagnostics;
 
namespace DIViaPropertiesTest
{
    public class Tests
    {
        [Test]
        public void ConfigurationTest()
        {
            Configuration configuration = new Configuration();
            Settings settings;
 
            configuration.loggingProvider = new TestLoggingProvider();
            configuration.dataProvider = new TestDataProvider();
            settings = configuration.GetSettings();
 
            Assert.IsFalse(settings.isProductionMode);
            Assert.AreEqual("Test data provider", configuration.dataProvider.GetName());
            Assert.AreEqual("Test logging provider", configuration.dataProvider.loggingProvider.GetName());
        }
    }
}

Although the dependency injection code is simpler to write, it is more complex to consume. As we will be writing once and consuming many times, this is a false economy. This technique exposes more information about the internals of the classes we are calling than I like. As the dependencies change internally, the tests are likely to break.

My preference is still dependency injection via events or deferred via constructors.


No comments:

Post a Comment