Using Ninject in Class Libraries – Best Practices

cclasslibrariesninjectservice-locator

Looking for some help getting my head around ninject and DI.

Using the simple examples I've found online everything works nicely but trying to do something more complex is causing headaches.

I have a class library that receives files, analyses them and stores the result in a database. This library can be called from various parts of a business; windows service, website, windows application, etc.

Please see this example code:

class FileProcessor : IFileProcessor
{
    public string Process(string fileName)
    {
        using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
        {
            BinaryReader binaryReader = new BinaryReader(fileStream);
            string hexString = BinaryFunctions.BytesToHexString(binaryReader.ReadBytes(2));
            string result = "";

            if(hexString == "123")
            {
                // I would like to inject the required BLLs into this constructor
                FileType1Processor p = new FileType1Processor(fileStream);
                result = p.Process();
            }
            else if(hexString == "456")
            {
                // I would like to inject the required BLLs into this constructor
                FileType2Processor p = new FileType2Processor(fileStream);
                result = p.Process();
            }

            return result;
        }
    }
}

I am stuck on how I would get the dependencies into the FileType processors irrespective of what type of application it's being used in.

Do I keep the kernel alive in all applications and throw that at the library?
Do I setup a service locator and use that?
Do I create a new kernel at the root of the library?
Am I looking at this all wrong?

Any help would be much appreciated.

Best Answer

You're doing quite a lot in a single class, that being said let's see what we can do.

First lets modify the code so that we can better support OCP. We will do this by introducing a dictionary that will contain your processors and a related interface IFileTypeProcessor.

interface IFileTypeProcessor
{
    // not sure what string you intended to return
    string Process(FileStream fileStream);
}

class FileProcessor : IFileProcessor
{
    private readonly IDictionary<string, IFileTypeProcessor> processors;
    public FileProcessor(IDictionary<string, IFileTypeProcessor> processors)
    {
            this.processors = processors
    }

    public string Process(string fileName)
    {
        using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
        {
            BinaryReader binaryReader = new BinaryReader(fileStream);
            string hexString = BinaryFunctions.BytesToHexString(binaryReader.ReadBytes(2));
            string result = "";

            if(!this.processors.ContainsKey(hexString)) 
            {
                throw new YourApplicationSpecificException();
            }
            return this.processors[hexString].Process(fileStream);
        }
    }
}

Next let's introduce a new class called FirstTwoByteHexKeyProvider that implements a new interface IKeyREader which will then become an additional dependency on the CTOR. We'll also include some guard clauses to help our users because we like them.

interface IKeyReader
{
    string GetKeyAsHexString(FileStream filestream)
}

class FirstTwoByteHexKeyProvider : IKeyReader
{
    public string GetKeyAsHexString(FileStream fileStream)
    {
        if(fileStream)
        { 
            throw new ArgumentNullException(nameof(fileStream));
        }

        BinaryReader binaryReader = new BinaryReader(fileStream);
        return BinaryFunctions.BytesToHexString(binaryReader.ReadBytes(2));
    }
}

class FileProcessor : IFileProcessor
    {
        private readonly IDictionary<string, IFileTypeProcessor> processors;
        private readonly IKeyReader keyReader;
        public FileProcessor(IDictionary<string, IFileTypeProcessor> processors, IKeyReader keyReader)
        {
                if(processors == null)
                { 
                    throw new ArgumentNullException(nameof(fileStream));
                }

                if(keyReader == null)
                { 
                    throw new ArgumentNullException(nameof(keyReader));
                }

                this.processors = processors
                this.keyReader = keyReader
        }

        public string Process(string fileName)
        {
            using (FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                string hexString = this.keyReader.GetKeyAsHexString(fileStream);

                if(!this.processors.ContainsKey(hexString)) 
                {
                    throw new YourApplicationSpecificException();
                }
                return this.processors[hexString].Process(fileStream);
            }
        }
    }

Now on to dependencies, do you need to support only Ninject? if so it could be as simple as using NinjectModules:

public class FileProcessorNinjectModule : NinjectModule
{
    public override void Load() 
    {
        Bind<IFileProcessor>().To<FileProcessor>();
        Bind<IKeyReader>().To<FirstTwoByteHexKeyProvider>();
        Bind<IDictionary<string, IFileTypeProcessor>>().ToMethod(context => new Dictionary<string,IFileTypeProcessor>
        {
            ["123"] = new FileType1Processor(),
            ["456"] = new FileType2Processor(),
        });
    }
}

Once you have the module built tell your customers to use some form of Dynamic Module Loading and Bob's your uncle.

I wrote this quickly here and didn't test it via compiler but I'm sure it'll work with a bit of correction on your part but it should be enough to get you going.

If you have to support more than one form of DI then I'd recommend exposing an interface that will allow the customer to provide their own DI Container to you and your package will then use that to resolve dependencies much the same way that ASP.NET Core does it today.

Lastly you may consider ripping out the action of actually reading the file into another class and making the binary result of this "file read" an argument that get's passed in to your process method instead of the fileName since as it stands this will be a bit of a pain to test.

If you don't want to do that then you at the very least can extract the "new FileStream(..." into an internal virtual function which will give you the ability to MOQ around this system call in unit testing.

Hope that helps...