Skip to content

Instantly share code, notes, and snippets.

@MarkPflug
Created April 21, 2023 15:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarkPflug/dea8301c161f099344841ac256839f27 to your computer and use it in GitHub Desktop.
Save MarkPflug/dea8301c161f099344841ac256839f27 to your computer and use it in GitHub Desktop.
RedditCsvFileResult
// this is a reworking of the CsvFileResult code posted to the CSharp Reddit:
// https://www.reddit.com/r/csharp/comments/12sip6r/oom_on_custom_fileresult_streaming_from_queryable/
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
public class CSVFileResult<T> : FileResult where T : class
{
private readonly IQueryable<T> _data;
public CSVFileResult(string contentType, IQueryable<T> data, string fileDownloadName) : base(contentType)
{
_data = data;
FileDownloadName = fileDownloadName;
}
struct FieldDefinition
{
public FieldDefinition(PropertyInfo first, PropertyInfo[]? second)
{
this.First = first;
this.Second = second;
}
public PropertyInfo First { get; }
public PropertyInfo[]? Second { get; }
}
static bool IsPrimitive(Type type)
{
return
type == typeof(string) ||
type == typeof(int) ||
type == typeof(DateTime);
}
static string Escape(string s)
{
// double up the quotes.
return s.Replace("\"", "\"\"");
}
static async Task WriteValue(TextWriter writer, object? val)
{
await writer.WriteAsync('\"');
var str = val?.ToString() ?? string.Empty;
str = Escape(str);
await writer.WriteAsync(str);
await writer.WriteAsync('\"');
}
public async override Task ExecuteResultAsync(ActionContext context)
{
var response = context.HttpContext.Response;
context.HttpContext.Response.Headers.Add("Content-Disposition", new[] { "attachment; filename=" + FileDownloadName });
using var writer = new StreamWriter(response.Body);
var recordType = typeof(T);
var props = recordType.GetProperties();
// a data structure to hold the nested property info
List<FieldDefinition> fields = new List<FieldDefinition>();
// write the headers, and build the nested property data structure.
var idx = 0;
foreach (var prop in props)
{
if (idx++ > 0)
{
await writer.WriteAsync(',');
}
var propType = prop.PropertyType;
if (IsPrimitive(propType))
{
// technically, I think a property name could contain a quote
// but, probably not in the context of an EF query.
await writer.WriteAsync(prop.Name);
}
else
{
var second = prop.PropertyType.GetProperties();
foreach (var prop2 in second)
{
propType = prop2.PropertyType;
if (IsPrimitive(propType))
{
if (idx++ > 0)
{
await writer.WriteAsync(',');
}
await writer.WriteAsync(prop2.Name);
}
}
fields.Add(new FieldDefinition(prop, second));
}
}
await writer.WriteLineAsync();
// headers are now written.
foreach (var row in _data)
{
idx = 0;
foreach (var f in fields)
{
if (idx++ > 0)
{
await writer.WriteAsync(',');
}
var prop = f.First;
var propType = prop.PropertyType;
var val = prop.GetValue(row, null);
if (IsPrimitive(propType))
{
await WriteValue(writer, val);
}
else
{
if (f.Second != null)
{
foreach (var prop2 in f.Second)
{
propType = prop2.PropertyType;
if (IsPrimitive(propType))
{
var val2 = prop.GetValue(val, null);
await WriteValue(writer, val);
}
}
}
}
}
await writer.WriteLineAsync();
}
await writer.FlushAsync();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment