Search code examples
c#.netgenericssuperclass

Generics and supertypes


At my current project, we parse CSV files we receive from an external supplier. However, since the supplier will support XML files in the future, I want to provide an easy way to change our code if management decides we should use the XML format.

For this to happen, our 'worker' classes should only reference data classes, without knowing that the source is a CSV or XML file. However, we do have some tools (used for debugging and testing) that are specifically written for one source file (CSV at the moment).

Probably that description is a bit unclear, but I hope the following example will provide you with enough information to help me out. Irrelevant functionality has been stripped from the classes, classes/interfaces have been renamed and the overall solution has been much simplfied just for the sake of the example. At the moment, I have the following setup.

Data 'base' classes (can be anything really): These classes are returned by (one of) the parser(s). The only thing they really do is contain data (can be considered as kind of DTO's).

public class Person
{
    public string FirstName { ... };
    public string LastName { ... };
    public int Age { ... };

    public Person()
    {
    }
}

ICsvObject interface: An interface for CSV data objects. The most important thing here is the LoadFromCsv method, because it will be used by the CsvParser class

public interface ICsvObject
{
    int CsvLineNumber { get; set; }
    void LoadFromCsv(IList<string> columns);
}

CSV data classes: These usually inherit from a data class and implement the ICsvObject interface

public class CsvPerson : Person
{
    public int CsvLineNumber { get; set; }
    public void LoadFromCsv(IList<string> columns)
    {
        if (columns.count != 3)
            throw new Exception("...");

        this.FirstName = columns[0];
        this.LastName = columns[1];
        this.Age = Convert.ToInt32(columns[2]);
    }
}

IParser interface: This interface provides a way for other classes to reference a parser without knowing the source file type.

public interface IParser<T> where T : new()
{
    IList<T> ReadFile(string path);
    IList<T> ReadStream(Stream sSource);
    IList<T> ReadString(string source);
}

CsvParser class: This class implements the IParser interface and provides a way to parse CSV files. In the future, one may decide to provide a parser for XML files.

public class CsvParser<CsvObjectType> : IParser<CsvObjectType> where CsvObjectType : new(), ICsvObject
{
    public IgnoreBlankLines { get; set; }

    public ReadFile(string path)
    {
        ...
    }

    public ReadStream(string path)
    {
        ...
    }

    public ReadString(string path)
    {
        List<CsvObjectType> result = new ...;
        For each line in the string
        {
            // Code to get the columns from the current CSV line
            ...

            CsvObjectType item = new CsvObjectType();
            item.CsvLineNumber = currentlinenumber;
            item.LoadFromCsv(columns);
            result.add(item);
        }
        return result;
    }
}

Now I've explained the situation a bit, let's get to the problem: 'Worker' classes shouldn't bother with the type of parser we're using. All they should receive from the parser is a list of data objects (Person for example), they don't need the extra information that is provided by the ICsvObject interface (CsvLineNumber in this example and also other things in the real situation). However, other tools SHOULD have the ability to get to the extra information (debug/test programs ...).

So, what I actually want is the following:

ParserFactory class: This class returns the correct parser for a specific data type. When switching to XML in the future, one has to create the XML parser and change the factory class. All other classes calling the factory methods should receive a valid IParser class instead of a specific parser.

public class ParserFactory
{
    //Instance property
    ...

    public IParser<Person> CreatePersonParser()
    {
        return new CsvParser<CsvPerson>();
    }
}

Doing this, worker classes will call the factory method, no matter what type of parser we're using. The ParseFile method can be called afterwards to provide a list of 'base' data classes. Returning a Csv parser is OK (it implements the IParser interface). The generic type however is not supported. Returning CsvParser<Person> would be valid for the factory, but the Person class doesn't implement the ICsvObject interface and cannot be used together with the CsvParser due to the generic constraint.

Returning a CsvParser class or an IParser would require the calling class to know which parser we're using so that's not an option. Creating a CsvParser class using two generic type inputs (one for the CsvObject type and one for the returning type) also won't work, because other tools should be able to access the extra information provided by the ICsvObject interface.

Also worth mentioning. This is an old project which is being modified. It's still .NET 2.0. However, when answering, you MAY use newer techniques (like extension methods or LINQ methods). Answering the question both in a .NET 2.0 and newer way will get you much more kudo's :-)

Thanks!


Solution

  • Thanks for looking into it.

    I've managed to find a solution by creating a proxy class used by worker classes:

    public class CsvParserProxy<CsvObjectType, ResultObjectType> : IParser<ResultObjectType> where CsvObjectType : new(), ResultObjectType, ICsvObject where ResultObjectType : new()
    {
        private object _lock;
        private CsvParser<CsvObjectType> _CsvParserInstance;
        public CsvParser<CsvObjectType> CsvParserInstance {
            get {
                if (this._CsvParserInstance == null) {
                    lock ((this._lock)) {
                        if (this._CsvParserInstance == null) {
                            this._CsvParserInstance = new CsvParser<CsvObjectType>();
                        }
                    }
                }
    
                return _CsvParserInstance;
            }
        }
    
        public IList<ResultObjectType> ReadFile(string path)
        {
            return this.Convert(this.CsvParserInstance.ReadFile(path));
        }
    
        public IList<ResultObjectType> ReadStream(System.IO.Stream sSource)
        {
            return this.Convert(this.CsvParserInstance.ReadStream(sSource));
        }
    
        public IList<ResultObjectType> ReadString(string source)
        {
            return this.Convert(this.CsvParserInstance.ReadString(source));
        }
    
        private List<ResultObjectType> Convert(IList<CsvObjectType> TempResult)
        {
            List<ResultObjectType> Result = new List<ResultObjectType>();
            foreach (CsvObjectType item in TempResult) {
                Result.Add(item);
            }
    
            return Result;
        }
    }
    

    The factory class then creates CsvParserProxies which return the base data object. Others can directly create CsvParser classes if they want the extra information from CsvObjects.