Reputation: 621
I need to return an Excel-friendly csv file with non-Latin characters from an implementation of TextOutputFormatter
. The following code shows the essentials bits:
public class CsvOutputFormatter : TextOutputFormatter
{
private readonly UTF8Encoding _encoding;
public CsvOutputFormatter()
{
_encoding = new UTF8Encoding(true);
SupportedEncodings.Add(_encoding);
SupportedMediaTypes.Add(Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse("text/csv"));
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
response.Headers.Add("Content-Disposition", $"attachment; filename=test.csv");
response.ContentType = "text/csv";
var preamble = _encoding.GetPreamble();
response.Body.Write(preamble, 0, preamble.Length);
// this works
//using (var writer = new StreamWriter(response.Body, _encoding))
// this doesn't work
using (var writer = context.WriterFactory(response.Body, _encoding))
{
var csv = new CsvWriter(writer);
csv.Configuration.HasHeaderRecord = true;
csv.Configuration.QuoteAllFields = true;
csv.WriteRecords((IEnumerable<object>)context.Object);
await writer.FlushAsync();
}
}
My main questions is why the BOM not output when using OutputFormatterWriteContext.WriterFactory
?
Side questions:
What's the added value of using OutputFormatterWriteContext.WriterFactory
instead of a regular StreamWriter
, which works just fine in this case?
Is there a way to avoid explicitly writing the BOM, e.g. have a writer calling Encoding.GetPreamble()
automatically?
I'm aware that UTF-8 with BOM is non-standard, I wonder though if there is a way to avoid it in this case?
Upvotes: 4
Views: 1166
Reputation: 25350
What's the added value of using OutputFormatterWriteContext.WriterFactory instead of a regular StreamWriter, which works just fine in this case?
Actually, you could write to HttpResponse.Body
directly if you like. The point is, as the document describes, don't use WriterFactory
when you want to write binary data.
The ASP.NET Core uses the HttpResponseStreamWriter
to write stream behind the scenes (See MemoryPoolHttpResponseStreamWriterFactory)
. This implementation exposes several methods which are quite similar to StreamWriter
. But the HttpResponseStreamWriter
uses ArrayPool<>
behind the hood. According to this document, it should improve performance when arrays are created and destroyed frequently.
My main questions is why the BOM not output when using OutputFormatterWriteContext.WriterFactory?
That's because the HttpResponseStreamWriter
doesn't write BOM
at all :
/// <summary< /// Writes to the using the supplied . /// It does not write the BOM and also does not close the stream. /// </summary< public class HttpResponseStreamWriter : TextWriter { private const int MinBufferSize = 128; internal const int DefaultBufferSize = 16 * 1024; ... }
Is there a way to avoid explicitly writing the BOM, e.g. have a writer calling Encoding.GetPreamble() automatically?
If you're using the built-in OutputFormatterWriteContext.WriterFactory
, I believe the answer is YES. You need to write the BOM header by your self if you wish.
Lastly, you should not write headers within the WriteResponseBodyAsync()
method. That's a duty of WriteResponseHeaders(ctx)
. It's better to move this codes to WriteResponseHeaders(OutputFormatterWriteContext ctx )
:
public override void WriteResponseHeaders(OutputFormatterWriteContext ctx )
{
var response = ctx.HttpContext.Response;
response.Headers.Add("Content-Disposition", $"attachment; filename=test.csv");
response.ContentType = "text/csv";
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
var preamble = _encoding.GetPreamble();
response.Body.Write(preamble, 0, preamble.Length);
using (var writer = context.WriterFactory(response.Body, _encoding))
{
var csv = new CsvWriter(writer);
csv.Configuration.HasHeaderRecord = true;
csv.Configuration.QuoteAllFields = true;
csv.WriteRecords((IEnumerable<object>)context.Object);
await writer.FlushAsync();
}
}
Upvotes: 4