Skip to content

IResult Implementation For ContentResult #32679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion src/Mvc/Mvc.Core/src/ContentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// A <see cref="ActionResult"/> that when executed will produce a response with content.
/// </summary>
public class ContentResult : ActionResult, IStatusCodeActionResult
public class ContentResult : ActionResult, IResult, IStatusCodeActionResult
{
private const string DefaultContentType = "text/plain; charset=utf-8";

/// <summary>
/// Gets or set the content representing the body of the response.
/// </summary>
Expand All @@ -39,5 +45,55 @@ public override Task ExecuteResultAsync(ActionContext context)
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ContentResult>>();
return executor.ExecuteAsync(context, this);
}

/// <summary>
/// Writes the content to the HTTP response.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
/// <returns>A task that represents the asynchronous execute operation.</returns>
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<ILoggerFactory>();
var logger = factory.CreateLogger<ContentResult>();

logger.ContentResultExecuting(resolvedContentType);

if (Content != null)
{
response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content);

await using (var textWriter = new HttpResponseStreamWriter(response.Body, resolvedContentTypeEncoding))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks inefficient. Can we just use Response.WriteAsync?

{
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();
}
}
}
}
}
80 changes: 79 additions & 1 deletion src/Mvc/Mvc.Core/test/ContentResultTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MediaTypeHeaderValue, string, string, string, byte[]> ContentResultContentTypeData
{
get
Expand Down Expand Up @@ -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<string, string> ContentResult_WritesDataCorrectly_ForDifferentContentSizesData
{
get
Expand Down Expand Up @@ -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();
Expand All @@ -270,6 +347,7 @@ private static IServiceCollection CreateServices()
services.AddSingleton<IActionResultExecutor<ContentResult>>(new ContentResultExecutor(
new Logger<ContentResultExecutor>(NullLoggerFactory.Instance),
new MemoryPoolHttpResponseStreamWriterFactory(ArrayPool<byte>.Shared, charArrayPool.Object)));
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
return services;
}

Expand Down