Search code examples
c#unit-testingdependency-injectioninversion-of-controlabstraction

Create a list of custom objects with Inversion of Control paradigm


I want to adapt Dependency Injection and Inversion of Control into my daily development. Say I have an object type of SomeObject (implements the interface ISomeObject). I have a class which consumes this object called Data which implements the IData interface.

public interface ISomeObject {
    int ID;
    string Name;
    bool IsAwesome;

    void DoSomeStuffIfAwesome();
}

public Class SomeObject : ISomeObject {
    int ID;
    string Name;
    bool IsAwesome;

    void DoSomeStuffIfAwesome() { /*stuff happens here*/ }        
}

public interface IData {
    List<ISomeObject> GetSomeObjects();
}

public Class Data : IData {
    List<ISomeObject> GetSomeObjects()
    {
        List<ISomeObject> objects = new List<ISomeObject>; // ??? Maybe and cast later?

        //do some SQL stuff and get a SqlDataReader object called reader
        while(reader.Read()) {
             //ISomeObject someObj = ???
             //Read into the someObj.ID, someObj.Name and someObj.IsAwesome fields
             objects.add(someObj);
        }
        return objects;
    }
}    

The GetSomeObjects() method produces a list of ISomeObject objects. But I don't want Data.cs to have anything related to SomeObject hardcoded into it. I want some form of Dependency Injection to resolve the issue at runtime. What's the best way to handle this? I've considered the following:

1. Pass instance of SomeObject into Data's constructor. This way I can get its type with .GetType(), store that into a private System.Type variable in Data.cs, and use Activator.CreateInstance in the loop to create new objects to be added to the list. Data would need to know about the SomeObject class specifically to cast, if I understand it correctly.

2. Pass an instance of my IoC container to Data's constructor and just resolve the object type using container.Resolve<ISomeObject>(). This would make unit testing the GetSomeObjects() method difficult without utilizing my IoC container. I've read I shouldn't utilize the IoC container during unit testing, and should manually pass in what I need into methods.

3. Pass an ISomeObject object that has been instantiated as a SomeObject - I would then use that to create the object via some built in method, such as SomeObject.GenerateList(IDataReader reader).


Solution

  • You can delegate the creation of the object out to something else;

    public interface ISomeObjectFactory {
        ISomeObject Create(IDataReader reader);
    }
    

    which has a single responsibility to create instances of ISomeObject

    using System.Collections.Generic;
    using System.Data;
    
    public interface IDbConnectionFactory {
        ///<summary>
        ///  Creates a connection based on the given database name or connection string.
        ///</summary>
        IDbConnection CreateConnection(string nameOrConnectionString);
    }
    
    public class Data : IData {
        private IDbConnectionFactory dbConnectionFactory;
        ISomeObjectFactory someObjectFactory;
        private string CONNECTION_STRING = "Connection string here";
    
        public Data(IDbConnectionFactory dbConnectionFactory, ISomeObjectFactory objectFactory) {
            this.dbConnectionFactory = dbConnectionFactory;
            this.someObjectFactory = objectFactory;
        }
    
        public List<ISomeObject> GetSomeObjects() {
            var objects = new List<ISomeObject>();
            //do some SQL stuff and return a data reader
            using (var connnection = dbConnectionFactory.CreateConnection(CONNECTION_STRING)) {
                using (var command = connnection.CreateCommand()) {
                    //configure command to be executed.
                    command.CommandText = "SELECT * FROM SOMEOBJECT_TABLE";
                    connnection.Open();
                    using (var reader = command.ExecuteReader()) {
                        while (reader.Read()) {
                            //...Logic to populate item
                            var someObject = someObjectFactory.Create(reader);
                            if (someObject != null)
                                objects.Add(someObject);
                        }
                    }
                }
            }
    
            return objects;
        }
    }
    

    that way Data is only dependent on abstractions and not on concretions. Those can be determined/configured in the composition root at runtime.