mercredi 14 janvier 2015

Fully automated CSV generator based on reflection and performance issues

In my project I have models that uses ASP.NET data annotations:



public class MyModel {
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm}", ApplyFormatInEditMode = true)]
public DateTime MyDate { get; set; }

[DisplayFormat(DataFormatString = "{0:0.00}", ApplyFormatInEditMode = true)]
public decimal MyDecimal { get; set; }
}


I wanted to take advantage of it and use it to automatically generate CSV file from list of models like the one presented before. The idea was to enumerate all items, enumerate all properties of model, extract values and format attribute and convert it to string using extracted format. So I've created something like that (comments included):



public static FileStreamResult ToCsv<T>(IEnumerable<T> list, string separator)
{
var properties = typeof(T).GetProperties(); // Getting properties

var sb = new StringBuilder();

foreach(var item in list) // Enumerating list
{
foreach(var property in properties) // Enumerating properties
{
var displayFormatAttribute =
property.GetCustomAttributes(
typeof(DisplayFormatAttribute), false).FirstOrDefault()
as DisplayFormatAttribute; // Getting DisplayFormat attribut
var displayValue = string.Empty;

if(displayFormatAttribute != null && displayFormatAttribute.DataFormatString != null) // checking if attribute is present and has format set
{
var format = displayFormatAttribute.DataFormatString.Substring(3, displayFormatAttribute.DataFormatString.Length - 4); // extracting format (from for.ex {0:yyyy-MM-dd}
var value = property.GetValue(item, null); // getting value

if(value != null) // checking if values is not null
{
var toStringMethod = property.PropertyType.GetMethods().FirstOrDefault(x =>
x.Name == "ToString" && x.GetParameters().Count() == 2 &&
x.GetParameters().ElementAt(0).ParameterType == typeof(string) &&
x.GetParameters().ElementAt(1).ParameterType == typeof(IFormatProvider)); // finding ToString(string, IFormatProvider) method, mostly used to convert DateTime and decimal.

if(toStringMethod != null)
{
displayValue = (string)toStringMethod.Invoke(value, new object[] { format, CultureInfo.CurrentCulture }); // running ToString method
}
}
}
else
{
displayValue = Convert.ToString(property.GetValue(item, null)); // if no displayformat specified do default conversion
}

sb.Append(displayValue).Append(separator);
}

sb.AppendLine();
}

var memoryStream = new MemoryStream();
var writer = new StreamWriter(memoryStream);
writer.Write(sb.ToString());
writer.Flush();
memoryStream.Position = 0;

return new FileStreamResult(memoryStream, "text/plain");
}


Actually, I've identified some problems and done some fixing:



  1. I've extracted format from attribute on the beggining and saved it to the list.


  2. I've resigned from finding ToString method and invoking it through Reflection. Changed it to checking type, casting and doing ToString without Reflection.


    if(value is DateTime) displayValue = ((DateTime)value).ToString(format); etc...




Unfortunatelly, the performance boost isn't significant enough. Generating CSV from list of 500 items takes minutes what is unacceptable. It seems that last bootleneck is:



var value = property.GetValue(item, null); // getting value


But it's the only thing, that I have no idea how to optimize. Please share your advices.






Aucun commentaire:

Enregistrer un commentaire