Pass Anonymous Type

430 Views Asked by At

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?

1

There are 1 best solutions below

0
On

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 ..