4
\$\begingroup\$

Looking for a way to efficiently use 3rd party CSV reader without making a dependency on it. I would also prefer to make my class be available everywhere (not just in IoC container injectable Services), so I decided to go with a questionable decision – configure through the static property. Can you see a way to reduce amount of library code or make configuration more obvious without losing library handiness?

Repository at GitHub.

Project dependencies are:

enter image description here

And solution looks like this:

enter image description here

MyProject.Demo registers CSV library parser dependency and parses the CSV file:

class Program
{
    static void Main(string[] args)
    {
        CsvRowReader.Use();

        var text = "First,Last\nJohn,Doe\n";
        var file = NameFile.Parse(text);
        foreach (var name in file)
            WriteLine($"{name.First} {name.Last}");
    }
}

Business logic assembly MyProject defines NameFile as:

public class NameFile : RowFile<NameFile, FullName>
{
    protected override IEnumerable<FullName> Read(RowReader reader)
    {
        using(reader)
            while (reader.Read())
                yield return new FullName
                {
                    First = reader.Get<string>("First"),
                    Last = reader.Get<string>("Last")
                };
    }
}

Where FullName is:

public class FullName
{
    public string First { get; set; }
    public string Last { get; set; }
}

Using library class RowFile<TFile, TRow>:

public abstract class RowFile<TFile, TRow> : Enumerable<TRow>
    where TFile : RowFile<TFile, TRow>, new()
{
    public static readonly TFile Empty = new TFile();

    public static TFile Parse(string text) =>
        Load(new StringReader(text));

    public static TFile Load(string filePath) =>
        Load(File.OpenText(filePath));

    public static TFile Load(Stream stream) =>
        Load(new StreamReader(stream));

    public static TFile Load(TextReader reader)
    {
        var file = new TFile();
        file.Rows = file.Read(RowReader.Create(reader));
        return file;
    }

    public sealed override IEnumerator<TRow> GetEnumerator() =>
        Rows.GetEnumerator();

    protected abstract IEnumerable<TRow> Read(RowReader read);
    IEnumerable<TRow> Rows { get; set; } = new TRow[0];
}

Where:

public abstract class RowReader : IDisposable
{
    public static Func<TextReader, RowReader> Create { get; protected set; }
    public abstract void Dispose();
    public abstract bool Read();
    public abstract T Get<T>(string name);
}

And:

public abstract class Enumerable<T> : IEnumerable<T>
{
    public abstract IEnumerator<T> GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() =>
        GetEnumerator();
}

3rd party CSV reader dependency is incapsulated in MyProject.CsvHelper project:

public class CsvRowReader : RowReader
{
    public static void Use() =>
        Create = reader => new CsvRowReader(reader);

    CsvRowReader(TextReader reader)
    {
        Reader = new CsvReader(reader);
        Reader.Read();
        Reader.ReadHeader();
    }

    CsvReader Reader { get; }

    public override void Dispose() =>
        Reader.Dispose();

    public override bool Read() =>
        Reader.Read();

    public override T Get<T>(string name) =>
        Reader.GetField<T>(name);
}

I don’t like all those mutability things, but do not see a better solution at the moment...

\$\endgroup\$
2
  • \$\begingroup\$ Could you point me (us?) to those mutability things? I'm not sure I see the issue in this design. \$\endgroup\$
    – t3chb0t
    Jun 19 '18 at 6:12
  • \$\begingroup\$ @t3chb0t RowFile.Rows & RowReader.Create properties are both mutable. I understand Create but Rows looks a little bit ugly :) \$\endgroup\$ Jun 19 '18 at 6:25
2
\$\begingroup\$
public static TFile Load(TextReader reader)
{
    var file = new TFile();
    file.Rows = file.Read(RowReader.Create(reader));
    return file;
}

In order to get rid of the mutable property file.Rows I suggest taking a look at Autofac's factory delegate for creating TFile immutably and injecting this delegate as a dependency.

Alternatively you could register them via named and keyed services and pick the right factory by TKey with Type as key. I use this techniques quite often.

I guess other dependency injection frameworks could do similar magic.

\$\endgroup\$
3
  • \$\begingroup\$ I went with settable property to skip non-default ctor creation. This way there is no need to redefine delegating ctor again and again for each concrete File class... Not sure if there is an other way to do it. \$\endgroup\$ Jun 19 '18 at 14:03
  • \$\begingroup\$ @DmitryNogin I find this vicious-circle a little bit funny :-) you're saying the mutable Rows property is ugly but at the same time you actually refuse to unuglyfy it with proper dependency injection because you want a simple creation via where T : new() - this is a clear case of the catch-22 :-P \$\endgroup\$
    – t3chb0t
    Jun 19 '18 at 14:15
  • \$\begingroup\$ Yep, exactly :) There will be need in Activator.CreateInstance, etc for non default ctors. There is no way in C# to define TFile type parameter constraint for non-default ctor, so it would be ugly too :) \$\endgroup\$ Jun 19 '18 at 14:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.