From 40794fcc336124870c67fe1dacf9e8a027b3b9f6 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Wed, 6 May 2015 23:29:54 -0700 Subject: [PATCH] Custom stream writer which avoids writing the BOM and does not flush or close the stream. --- .../ActionResults/ViewExecutor.cs | 94 +---- .../Formatters/JsonOutputFormatter.cs | 3 +- .../HttpResponseStreamWriter.cs | 235 +++++++++++ .../ActionResults/ViewExecutorTest.cs | 32 +- .../Formatters/JsonOutputFormatterTests.cs | 6 +- .../HttpResponseStreamWriterTest.cs | 398 ++++++++++++++++++ .../JsonResultTest.cs | 2 +- 7 files changed, 642 insertions(+), 128 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs index 8d0d049d53..1b0a030efd 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs @@ -64,98 +64,10 @@ namespace Microsoft.AspNet.Mvc response.ContentType = contentTypeHeader.ToString(); - var wrappedStream = new StreamWrapper(response.Body); - - using (var writer = new StreamWriter(wrappedStream, encoding, BufferSize, leaveOpen: true)) + using (var writer = new HttpResponseStreamWriter(response.Body, encoding)) { - try - { - var viewContext = new ViewContext(actionContext, view, viewData, tempData, writer); - await view.RenderAsync(viewContext); - } - catch - { - // Need to prevent writes/flushes on dispose because the StreamWriter will flush even if - // nothing got written. This leads to a response going out on the wire prematurely in case an - // exception is being thrown inside the try catch block. - wrappedStream.BlockWrites = true; - throw; - } - } - } - - private class StreamWrapper : Stream - { - private readonly Stream _wrappedStream; - - public StreamWrapper(Stream stream) - { - _wrappedStream = stream; - } - - public bool BlockWrites { get; set; } - - public override bool CanRead - { - get { return _wrappedStream.CanRead; } - } - - public override bool CanSeek - { - get { return _wrappedStream.CanSeek; } - } - - public override bool CanWrite - { - get { return _wrappedStream.CanWrite; } - } - - public override void Flush() - { - if (!BlockWrites) - { - _wrappedStream.Flush(); - } - } - - public override long Length - { - get { return _wrappedStream.Length; } - } - - public override long Position - { - get - { - return _wrappedStream.Position; - } - set - { - _wrappedStream.Position = value; - } - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _wrappedStream.Read(buffer, offset, count); - } - - public override long Seek(long offset, SeekOrigin origin) - { - return Seek(offset, origin); - } - - public override void SetLength(long value) - { - SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - if (!BlockWrites) - { - _wrappedStream.Write(buffer, offset, count); - } + var viewContext = new ViewContext(actionContext, view, viewData, tempData, writer); + await view.RenderAsync(viewContext); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs index 783dfa997f..1b4ab69cf9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs @@ -70,8 +70,7 @@ namespace Microsoft.AspNet.Mvc var response = context.ActionContext.HttpContext.Response; var selectedEncoding = context.SelectedEncoding; - using (var nonDisposableStream = new NonDisposableStream(response.Body)) - using (var writer = new StreamWriter(nonDisposableStream, selectedEncoding, 1024, leaveOpen: true)) + using (var writer = new HttpResponseStreamWriter(response.Body, selectedEncoding)) { WriteObject(writer, context.Object); } diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs new file mode 100644 index 0000000000..e4d848c21e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs @@ -0,0 +1,235 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Writes to the using the supplied . + /// It does not write the BOM and also does not close the stream. + /// + public class HttpResponseStreamWriter : TextWriter + { + private const int DefaultBufferSize = 1024; + private readonly Stream _stream; + private Encoder _encoder; + private byte[] _byteBuffer; + private char[] _charBuffer; + private int _charBufferSize; + private int _charBufferCount; + + public HttpResponseStreamWriter(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize) + { + } + + public HttpResponseStreamWriter([NotNull] Stream stream, [NotNull] Encoding encoding, int bufferSize) + { + _stream = stream; + Encoding = encoding; + _encoder = encoding.GetEncoder(); + _charBufferSize = bufferSize; + _charBuffer = new char[bufferSize]; + _byteBuffer = new byte[encoding.GetMaxByteCount(bufferSize)]; + } + + public override Encoding Encoding { get; } + + public override void Write(char value) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(); + } + + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } + + public override void Write(char[] values, int index, int count) + { + if (values == null) + { + return; + } + + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(); + } + + CopyToCharBuffer(values, ref index, ref count); + } + } + + public override void Write(string value) + { + if (value == null) + { + return; + } + + var count = value.Length; + var index = 0; + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + FlushInternal(); + } + + CopyToCharBuffer(value, ref index, ref count); + } + } + + public override async Task WriteAsync(char value) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(); + } + + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } + + public override async Task WriteAsync(char[] values, int index, int count) + { + if (values == null) + { + return; + } + + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(); + } + + CopyToCharBuffer(values, ref index, ref count); + } + } + + public override async Task WriteAsync(string value) + { + if (value == null) + { + return; + } + + var count = value.Length; + var index = 0; + while (count > 0) + { + if (_charBufferCount == _charBufferSize) + { + await FlushInternalAsync(); + } + + CopyToCharBuffer(value, ref index, ref count); + } + } + + // We want to flush the stream when Flush/FlushAsync is explicitly + // called by the user (example: from a Razor view). + + public override void Flush() + { + FlushInternal(true, true); + } + + public override async Task FlushAsync() + { + await FlushInternalAsync(true, true); + } + + // Do not flush the stream on Dispose, as this will cause response to be + // sent in chunked encoding in case of Helios. + protected override void Dispose(bool disposing) + { + FlushInternal(flushStream: false, flushEncoder: true); + } + + private void FlushInternal(bool flushStream = false, bool flushEncoder = false) + { + if (_charBufferCount == 0) + { + return; + } + + var count = _encoder.GetBytes(_charBuffer, 0, _charBufferCount, _byteBuffer, 0, flushEncoder); + if (count > 0) + { + _stream.Write(_byteBuffer, 0, count); + } + + _charBufferCount = 0; + + if (flushStream) + { + _stream.Flush(); + } + } + + private async Task FlushInternalAsync(bool flushStream = false, bool flushEncoder = false) + { + if (_charBufferCount == 0) + { + return; + } + + var count = _encoder.GetBytes(_charBuffer, 0, _charBufferCount, _byteBuffer, 0, flushEncoder); + if (count > 0) + { + await _stream.WriteAsync(_byteBuffer, 0, count); + } + + _charBufferCount = 0; + + if (flushStream) + { + await _stream.FlushAsync(); + } + } + + private void CopyToCharBuffer(string value, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + + value.CopyTo( + sourceIndex: index, + destination: _charBuffer, + destinationIndex: _charBufferCount, + count: remaining); + + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } + + private void CopyToCharBuffer(char[] values, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + + Buffer.BlockCopy( + src: values, + srcOffset: index * sizeof(char), + dst: _charBuffer, + dstOffset: _charBufferCount * sizeof(char), + count: remaining * sizeof(char)); + + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } + } +} + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewExecutorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewExecutorTest.cs index 746ec2e992..f03d72a5f0 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/ViewExecutorTest.cs @@ -18,9 +18,6 @@ namespace Microsoft.AspNet.Mvc { public class ViewExecutorTest { - // The buffer size of the StreamWriter used in ViewResult. - private const int ViewResultStreamWriterBufferSize = 1024; - public static TheoryData ViewExecutorSetsContentTypeAndEncodingData { get @@ -85,39 +82,16 @@ namespace Microsoft.AspNet.Mvc Assert.Equal(expectedContentData, memoryStream.ToArray()); } - public static IEnumerable ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData - { - get - { - yield return new object[] { 30, 0 }; - - if (TestPlatformHelper.IsMono) - { - // The StreamWriter in Mono buffers 2x the buffer size before flushing. - yield return new object[] { ViewResultStreamWriterBufferSize * 2 + 30, ViewResultStreamWriterBufferSize }; - } - else - { - yield return new object[] { ViewResultStreamWriterBufferSize + 30, ViewResultStreamWriterBufferSize }; - } - } - } - - // The StreamWriter used by ViewResult an internal buffer and consequently anything written to this buffer - // prior to it filling up will not be written to the underlying stream once an exception is thrown. - [Theory] - [MemberData(nameof(ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrownData))] - public async Task ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrown(int writtenLength, int expectedLength) + [Fact] + public async Task ExecuteAsync_DoesNotWriteToResponse_OnceExceptionIsThrown() { // Arrange - var longString = new string('a', writtenLength); + var expectedLength = 0; var view = new Mock(); view.Setup(v => v.RenderAsync(It.IsAny())) .Callback((ViewContext v) => { - view.ToString(); - v.Writer.Write(longString); throw new Exception(); }); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/JsonOutputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/JsonOutputFormatterTests.cs index d87c5facb0..8ff99b3b3d 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/JsonOutputFormatterTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/JsonOutputFormatterTests.cs @@ -142,11 +142,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test.Formatters var formattedContent = "\"" + content + "\""; var mediaType = string.Format("application/json; charset={0}", encodingAsString); var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding); - var preamble = encoding.GetPreamble(); - var data = encoding.GetBytes(formattedContent); - var expectedData = new byte[preamble.Length + data.Length]; - Buffer.BlockCopy(preamble, 0, expectedData, 0, preamble.Length); - Buffer.BlockCopy(data, 0, expectedData, preamble.Length, data.Length); + var expectedData = encoding.GetBytes(formattedContent); var memStream = new MemoryStream(); var outputFormatterContext = new OutputFormatterContext diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs new file mode 100644 index 0000000000..9f02a6de7c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs @@ -0,0 +1,398 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class HttpResponseStreamWriterTest + { + [Fact] + public async Task DoesNotWriteBOM() + { + // Arrange + var memoryStream = new MemoryStream(); + var encodingWithBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); + var writer = new HttpResponseStreamWriter(memoryStream, encodingWithBOM); + var expectedData = new byte[] { 97, 98, 99, 100 }; // without BOM + + // Act + using (writer) + { + await writer.WriteAsync("abcd"); + } + + // Assert + Assert.Equal(expectedData, memoryStream.ToArray()); + } + + [Fact] + public async Task DoesNotFlush_UnderlyingStream_OnClosingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello"); + writer.Close(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + } + + [Fact] + public async Task DoesNotFlush_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello"); + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + } + + [Fact] + public async Task DoesNotClose_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello"); + writer.Close(); + + // Assert + Assert.Equal(0, stream.CloseCallCount); + } + + [Fact] + public async Task DoesNotDispose_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.WriteAsync("Hello world"); + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.DisposeCallCount); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnClose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Close(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnDispose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Fact] + public void NoDataWritten_Flush_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + writer.Flush(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void FlushesBuffer_OnFlush(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + writer.Write(new string('a', byteLength)); + + // Act + writer.Flush(); + + // Assert + Assert.Equal(1, stream.FlushCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Fact] + public async Task NoDataWritten_FlushAsync_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + await writer.FlushAsync(); + + // Assert + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(0, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnFlushAsync(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + await writer.FlushAsync(); + + // Assert + Assert.Equal(1, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteChar_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + for (var i = 0; i < byteLength; i++) + { + writer.Write('a'); + } + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteCharArray_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + writer.Write((new string('a', byteLength)).ToCharArray()); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + for (var i = 0; i < byteLength; i++) + { + await writer.WriteAsync('a'); + } + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharArrayAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + await writer.WriteAsync((new string('a', byteLength)).ToCharArray()); + } + + // Assert + Assert.Equal(byteLength, stream.Length); + } + + [Theory] + [InlineData("你好世界", "utf-16")] + [InlineData("こんにちは世界", "shift_jis")] + [InlineData("హలో ప్రపంచ", "iso-8859-1")] + [InlineData("வணக்கம் உலக", "utf-32")] + public async Task WritesData_InExpectedEncoding(string data, string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); + + // Act + using (writer) + { + await writer.WriteAsync(data); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + [Theory] + [InlineData('ん', 1023, "utf-8")] + [InlineData('ん', 1024, "utf-8")] + [InlineData('ん', 1050, "utf-8")] + [InlineData('你', 1023, "utf-16")] + [InlineData('你', 1024, "utf-16")] + [InlineData('你', 1050, "utf-16")] + [InlineData('こ', 1023, "shift_jis")] + [InlineData('こ', 1024, "shift_jis")] + [InlineData('こ', 1050, "shift_jis")] + [InlineData('హ', 1023, "iso-8859-1")] + [InlineData('హ', 1024, "iso-8859-1")] + [InlineData('హ', 1050, "iso-8859-1")] + [InlineData('வ', 1023, "utf-32")] + [InlineData('வ', 1024, "utf-32")] + [InlineData('வ', 1050, "utf-32")] + public async Task WritesData_OfDifferentLength_InExpectedEncoding( + char character, + int charCount, + string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + string data = new string(character, charCount); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); + + // Act + using (writer) + { + await writer.WriteAsync(data); + } + + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + private class TestMemoryStream : MemoryStream + { + private int _flushCallCount; + private int _flushAsyncCallCount; + private int _closeCallCount; + private int _disposeCallCount; + + public int FlushCallCount { get { return _flushCallCount; } } + + public int FlushAsyncCallCount { get { return _flushAsyncCallCount; } } + + public int CloseCallCount { get { return _closeCallCount; } } + + public int DisposeCallCount { get { return _disposeCallCount; } } + + public override void Flush() + { + _flushCallCount++; + base.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + _flushAsyncCallCount++; + return base.FlushAsync(cancellationToken); + } + + public override void Close() + { + _closeCallCount++; + base.Close(); + } + + protected override void Dispose(bool disposing) + { + _disposeCallCount++; + base.Dispose(disposing); + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs index d914d1d9a2..597d24f292 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs @@ -112,7 +112,7 @@ namespace Microsoft.AspNet.Mvc public async Task ExecuteResultAsync_UsesPassedInFormatter() { // Arrange - var expected = Enumerable.Concat(Encoding.UTF8.GetPreamble(), _abcdUTF8Bytes); + var expected = _abcdUTF8Bytes; var context = GetHttpContext(); var actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor());