Created
April 21, 2023 15:47
-
-
Save MarkPflug/dea8301c161f099344841ac256839f27 to your computer and use it in GitHub Desktop.
RedditCsvFileResult
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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