Wednesday, April 24, 2019

Lazy Dependency Injection

One problem with Dependency Injection is that the calling object does not know which, if any, of the dependencies it is injecting is going to be used. When creating the dependency objects is expensive, we can improve performance by using just-in-time location of dependencies (yes, I know we're using Lazy, but bear with me). One solution is to perform dependency injection via events which allows the called code to say "I need one of these things RIGHT NOW, give me one!"

We will have to define a custom event handler and a custom event args to return the injected dependency. The cLogTime class will raise this event when it needs to log something, but doesn't know which logger to use.

Start a new C# WPF project called LazyDependencyInjection.


We will be using MEF to locate our logger so add a reference to System.ComponentModel.Composition.

Let's work from the bottom up by adding a new class called ILogger. It will contain the interfaces and classes we need to do logging. One of the three logger classes defined here will be lazily injected.

using System;
using System.Diagnostics;
using System.Windows;
using System.ComponentModel.Composition;

namespace LazyDependencyInjection
{
    public interface ILogger
    {
        void Print(String Msg);
    }

    public interface ILoggerData
    {
        eLogType Type { get; }
    }

    public enum eLogType
    {
        Window,
        MsgBox,
        Output
    }


    [Export(typeof(ILogger))]
    [ExportMetadata("Type", eLogType.MsgBox)]
    public class LogToMsgBox : ILogger
    {
        public void Print(string Msg)
        {
            MessageBox.Show(Msg);
        }
    }

    [Export(typeof(ILogger))]
    [ExportMetadata("Type", eLogType.Output)]
    public class LogToOutput : ILogger
    {
        public void Print(string Msg)
        {
            Debug.WriteLine(Msg);
        }
    }

    [Export(typeof(ILogger))]
    [ExportMetadata("Type", eLogType.Window)]
    public class LogToWindow : ILogger
    {
        public void Print(string Msg)
        {
            Application.Current.MainWindow.Content = Msg;
        }
    }
}


Next add a class called cLogTime. This will determine the time and log it using a logger passed from MainWindow. Instead of being passed the Logger instance via the constructor or a property, we will raise the WhichLogger event to ask MainWindow which logger to use. This allows cLogTime to request the Logger instance at the moment it first needs one.


using System;

namespace LazyDependencyInjection
{
    public class WhichLoggerEventArgs : EventArgs
    {
        public ILogger Logger;
    }

    public delegate void WhichLoggerEventHandler(object sender, WhichLoggerEventArgs e);

    class cLogTime
    {
        public event WhichLoggerEventHandler WhichLogger;
        ILogger Logger = null;

        public void LogTime()
        {
            if (Logger == null)
            {
                WhichLoggerEventArgs e = new WhichLoggerEventArgs();
                WhichLogger?.Invoke(this, e);
                Logger = e.Logger;
            }
            Logger.Print(DateTime.Now.ToString());
        }
    }
}

Now we can write the MainWindow code which does not create the Logger instance until cLogTime actually asks for it.


using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Windows;

namespace LazyDependencyInjection
{
    public partial class MainWindow : Window
    {
        [ImportMany] IEnumerable<Lazy<ILogger, ILoggerData>> Loggers;

        public MainWindow()
        {
            InitializeComponent();

            cLogTime TimeLogger = new cLogTime();
            TimeLogger.WhichLogger += TimeLogger_WhichLogger;
            TimeLogger.LogTime();
        }

        private void TimeLogger_WhichLogger(object sender, WhichLoggerEventArgs e)
        {
            CompositionContainer _container;
            const eLogType LogType = eLogType.Window;

            AggregateCatalog catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly()));
            _container = new CompositionContainer(catalog);
            _container.ComposeParts(this);
            e.Logger = Loggers.FirstOrDefault(l => (l.Metadata.Type == LogType)).Value;
        }
    }
}

The result of all this work is pretty unimpressive.


Now, in MainWindow, replace eLogType.Window with eLogType.MsgBox and run it again. Are you impressed now? No? Oh, well.



No comments:

Post a Comment