0

I am trying to create a utility class where I could pass a list of Anonymous Type (AT) and it would produce a CSV file with the AT's properties as its columns and property values as its respective data.

I have a working code but I feel it could be improved (a lot!). I inherited a class from FileResult and decorate it with my custom implementations. Here's what I have so far:

public class ExportCSVAnonymous : FileResult {
    public dynamic List {
        set;
        get;
    }

    public char Separator {
        set;
        get;
    }
    public ExportCSVAnonymous(dynamic list, string fileDownloadName, char separator = ',') : base("text/csv") {
        List             = list;
        Separator        = separator;
        FileDownloadName = fileDownloadName;
    }
        public ExportCSVAnonymous(dynamic list, string fileDownloadName, char separator = ',') : base("text/csv") {
        List             = list;
        Separator        = separator;
        FileDownloadName = fileDownloadName;
    }

    protected override void WriteFile(HttpResponseBase response) {
        var outputStream = response.OutputStream;
        using (var memoryStream = new MemoryStream()) {
            WriteList(memoryStream);
            outputStream.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
        }
    }

    private void WriteList(Stream stream) {
        var streamWriter = new StreamWriter(stream, Encoding.Default);

        WriteHeaderLine(streamWriter);
        streamWriter.WriteLine();
        WriteDataLines(streamWriter);

        streamWriter.Flush();
    }

    //I wish this part could be improved
    private void WriteHeaderLine(StreamWriter streamWriter) {
        foreach (var line in List) {
            foreach (MemberInfo member in line.GetType().GetProperties()) {
                WriteValue(streamWriter, member.Name);
            }
            break;
        }
    }

    private void WriteValue(StreamWriter writer, String value) {
        writer.Write("\"");
        writer.Write(value.Replace("\"", "\"\""));
        writer.Write("\"" + Separator);
    }

    private void WriteDataLines(StreamWriter streamWriter) {
        foreach (var line in List) {
            foreach (MemberInfo member in line.GetType().GetProperties()) {
                WriteValue(streamWriter, GetPropertyValue(line, member.Name));
            }
            streamWriter.WriteLine();
        }
    }

    private static string GetPropertyValue(object src, string propName) {
        object obj = src.GetType().GetProperty(propName).GetValue(src, null);
        return (obj != null) ? obj.ToString() : "";
    }
}

I used dynamic as a way to pass my AT inside the class. Is there better way to do this? Lastly, I want to improve the WriteHeaderLine method. Since I am using dynamic type, I cannot cast it successfully to inspect the properties of the AT. What's the best way to do this?

Jose Capistrano
  • 693
  • 1
  • 8
  • 20
  • 1
    Curious why you're bothering to invent this wheel when [someone else already did](https://joshclose.github.io/CsvHelper/examples/writing/write-anonymous-type-objects/) – Caius Jard Sep 02 '21 at 05:51
  • (and it's open source so feel free to take a look at what Josh did.. I expect it would be reflection based) – Caius Jard Sep 02 '21 at 05:53
  • Could you please elaborate which types you actually want to serialize to a file? Maybe there's something common between them, which makes them perfect for extracting a common interface. Generally I#d strongly discourage you from using `dynamic` because of what you've already mentioned. It bypases the compiler, which seldomly is a good idea. – MakePeaceGreatAgain Sep 02 '21 at 06:06
  • Normally you'd do `void Method(List items){...}` - when you have list of object of some type - can you please [edit] the question to clarify why this did not work for you? – Alexei Levenkov Sep 02 '21 at 06:09
  • @CaiusJard thanks! To be honest, I just didn't bother to research and felt I could get on a side project with our existing code. And since I am here now, I guess I could pick up something while doing this. :) – Jose Capistrano Sep 02 '21 at 06:17
  • @HimBromBeere well I want the rest of our team members to just use `.Select` and and pass it the class. @AlexeiLevenkov I need it to be Anonymous Type. It's really handy together with `IEnumerable.Select` – Jose Capistrano Sep 02 '21 at 06:20
  • The best type to represent a list of unknown elements is `IEnumerable`, not `dynamic`. There are very few good uses of dynamic. – Enigmativity Sep 02 '21 at 10:31

1 Answers1

0

To some extent I feel its a bit overkill, for purposes of writing a CSV, to use an anonymous type (or any type. really) to pass the info purely so you can pass headers too as named properties. I get it, but ask yourself what is the code really doing/what problem are you solving?

You want to write N strings to a file.

That's pretty much it. So you write some simple method:

void WriteCsvLine(path, string[] cells){
  File.AppendAllText(path, string.Join(",", cells) + Environment.NewLine);
}

And you call it like:

someContext.Employees.Select(e =>
  new [] { e.Name, e.Dept, e.Salary.ToString() }
).ToList().ForEach(x => WriteCsvLine("c:\\...", x) ;

Ah, but we don't want to pass the path every time.. So you upgrade it to be a class, take the path as a constructor arg..

Ah, but we need to escape commas.. So you upgrade it to quote the fields

Ah, but we need to provide some variable delimiter.. So you upgrade it to have another constructor arg

Ah, but we could optimize to write multiple lines at a time.. So you upgrade it to take a List<string[]> or whatever

Ah, but we need to write a header line.. So you just make the first string array you pass to be the headers instead (you can LINQ Concat your data onto a new[]{"Name","Dept","Salary"} or make it a constructor argument..)

So we've need up with something that we maybe use like this:

 var x = new CsvWriter("c:\\...", ',', new[]{"EmployeeName","DepartmentName","Salary"});
x.WriteEnumerable(someContext.Employees.Select(e => new [] { 
  e.Name, 
  e.Dept, 
  e.Salary.ToString() 
}));

But that's not very cool - surely we can do it cooler.. So you decide you'll pass a KeyValuePair<string, string>[] (or a record or a ValueTuple) where the key is the header and the value is the data.. your calling code bulks up because you're specifying the header name with the data every time..

Ah, but that's still not very cool with all those strings.. So you decide you'll pass an anonymous type where the property name is the header, and the property value is the data..

Your code has some fewer " chars but now the receiving end has become a torturous nightmare of unpacking the property names into being strings so they can be written as a header line.. (and I don't even know if you can easily control the order of columns any more)


Ends up, the problem was simple: "find a way to pass what the header of the column should be", or in other words "pass a string to a method"

..and somehow we went from:

void Print(string what){
  Console.WriteLine(what);
}

...

Print("Hello World");

to something like:

using System.Reflection; 

static void Print<T>(T what)
{
    PropertyInfo[] propertyInfos = what.GetType().GetProperties();
    Console.WriteLine(propertyInfos[0].Name.Replace("_", " "));
}

...

Print(new { Hello_World = 0 });

It'll work, but it's a fairly fairly bonkers way of "passing a string to a method" when you think about it..

..and now the boss wants the headers to have percent symbols so I'm off to work out how to get those into property names and also add another bool flag so we can skip writing the header sometimes ..

Caius Jard
  • 72,509
  • 5
  • 49
  • 80