diff --git a/src/Mvc/Mvc.Core/src/ContentResult.cs b/src/Mvc/Mvc.Core/src/ContentResult.cs index e1d86fc1f03f..060aa72c40bf 100644 --- a/src/Mvc/Mvc.Core/src/ContentResult.cs +++ b/src/Mvc/Mvc.Core/src/ContentResult.cs @@ -3,16 +3,22 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc { /// /// A that when executed will produce a response with content. /// - public class ContentResult : ActionResult, IStatusCodeActionResult + public class ContentResult : ActionResult, IResult, IStatusCodeActionResult { + private const string DefaultContentType = "text/plain; charset=utf-8"; + /// /// Gets or set the content representing the body of the response. /// @@ -39,5 +45,55 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + /// + /// Writes the content to the HTTP response. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + async Task IResult.ExecuteAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var response = httpContext.Response; + + ResponseContentTypeHelper.ResolveContentTypeAndEncoding( + ContentType, + response.ContentType, + DefaultContentType, + out var resolvedContentType, + out var resolvedContentTypeEncoding); + + response.ContentType = resolvedContentType; + + if (StatusCode != null) + { + response.StatusCode = StatusCode.Value; + } + + var factory = httpContext.RequestServices.GetRequiredService(); + var logger = factory.CreateLogger(); + + logger.ContentResultExecuting(resolvedContentType); + + if (Content != null) + { + response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content); + + await using (var textWriter = new HttpResponseStreamWriter(response.Body, resolvedContentTypeEncoding)) + { + await textWriter.WriteAsync(Content); + + // Flushing the HttpResponseStreamWriter does not flush the underlying stream. This just flushes + // the buffered text in the writer. + // We do this rather than letting dispose handle it because dispose would call Write and we want + // to call WriteAsync. + await textWriter.FlushAsync(); + } + } + } } } diff --git a/src/Mvc/Mvc.Core/test/ContentResultTest.cs b/src/Mvc/Mvc.Core/test/ContentResultTest.cs index afe9675a695d..db4e0c66c803 100644 --- a/src/Mvc/Mvc.Core/test/ContentResultTest.cs +++ b/src/Mvc/Mvc.Core/test/ContentResultTest.cs @@ -24,7 +24,7 @@ public class ContentResultTest MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize; [Fact] - public async Task ContentResult_Response_NullContent_SetsContentTypeAndEncoding() + public async Task ContentResult_ExecuteResultAsync_Response_NullContent_SetsContentTypeAndEncoding() { // Arrange var contentResult = new ContentResult @@ -45,6 +45,28 @@ public async Task ContentResult_Response_NullContent_SetsContentTypeAndEncoding( MediaTypeAssert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType); } + [Fact] + public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding() + { + // Arrange + var contentResult = new ContentResult + { + Content = null, + ContentType = new MediaTypeHeaderValue("text/plain") + { + Encoding = Encoding.Unicode + }.ToString() + }; + var httpContext = GetHttpContext(); + var actionContext = GetActionContext(httpContext); + + // Act + await ((IResult)contentResult).ExecuteAsync(httpContext); + + // Assert + MediaTypeAssert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType); + } + public static TheoryData ContentResultContentTypeData { get @@ -143,6 +165,36 @@ public async Task ContentResult_ExecuteResultAsync_SetContentTypeAndEncoding_OnR Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength); } + [Theory] + [MemberData(nameof(ContentResultContentTypeData))] + public async Task ContentResult_ExecuteAsync_SetContentTypeAndEncoding_OnResponse( + MediaTypeHeaderValue contentType, + string content, + string responseContentType, + string expectedContentType, + byte[] expectedContentData) + { + // Arrange + var contentResult = new ContentResult + { + Content = content, + ContentType = contentType?.ToString() + }; + var httpContext = GetHttpContext(); + var memoryStream = new MemoryStream(); + httpContext.Response.Body = memoryStream; + httpContext.Response.ContentType = responseContentType; + + // Act + await ((IResult)contentResult).ExecuteAsync(httpContext); + + // Assert + var finalResponseContentType = httpContext.Response.ContentType; + Assert.Equal(expectedContentType, finalResponseContentType); + Assert.Equal(expectedContentData, memoryStream.ToArray()); + Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength); + } + public static TheoryData ContentResult_WritesDataCorrectly_ForDifferentContentSizesData { get @@ -246,6 +298,31 @@ public async Task ContentResult_WritesDataCorrectly_ForDifferentContentSizes(str Assert.Equal(content, actualContent); } + [Theory] + [MemberData(nameof(ContentResult_WritesDataCorrectly_ForDifferentContentSizesData))] + public async Task ContentResult_ExecuteAsync_WritesDataCorrectly_ForDifferentContentSizes(string content, string contentType) + { + // Arrange + var contentResult = new ContentResult + { + Content = content, + ContentType = contentType + }; + var httpContext = GetHttpContext(); + var memoryStream = new MemoryStream(); + httpContext.Response.Body = memoryStream; + var encoding = MediaTypeHeaderValue.Parse(contentType).Encoding; + + // Act + await ((IResult)contentResult).ExecuteAsync(httpContext); + + // Assert + memoryStream.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(memoryStream, encoding); + var actualContent = await streamReader.ReadToEndAsync(); + Assert.Equal(content, actualContent); + } + private static ActionContext GetActionContext(HttpContext httpContext) { var routeData = new RouteData(); @@ -270,6 +347,7 @@ private static IServiceCollection CreateServices() services.AddSingleton>(new ContentResultExecutor( new Logger(NullLoggerFactory.Instance), new MemoryPoolHttpResponseStreamWriterFactory(ArrayPool.Shared, charArrayPool.Object))); + services.AddSingleton(NullLoggerFactory.Instance); return services; }