Skip to content

Commit 1448cd9

Browse files
authored
IResult Implementation For ContentResult (#32679)
1 parent 6345d5a commit 1448cd9

File tree

2 files changed

+136
-2
lines changed

2 files changed

+136
-2
lines changed

src/Mvc/Mvc.Core/src/ContentResult.cs

+57-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33

44
using System;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Mvc.Formatters;
68
using Microsoft.AspNetCore.Mvc.Infrastructure;
9+
using Microsoft.AspNetCore.WebUtilities;
710
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
812

913
namespace Microsoft.AspNetCore.Mvc
1014
{
1115
/// <summary>
1216
/// A <see cref="ActionResult"/> that when executed will produce a response with content.
1317
/// </summary>
14-
public class ContentResult : ActionResult, IStatusCodeActionResult
18+
public class ContentResult : ActionResult, IResult, IStatusCodeActionResult
1519
{
20+
private const string DefaultContentType = "text/plain; charset=utf-8";
21+
1622
/// <summary>
1723
/// Gets or set the content representing the body of the response.
1824
/// </summary>
@@ -39,5 +45,55 @@ public override Task ExecuteResultAsync(ActionContext context)
3945
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ContentResult>>();
4046
return executor.ExecuteAsync(context, this);
4147
}
48+
49+
/// <summary>
50+
/// Writes the content to the HTTP response.
51+
/// </summary>
52+
/// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
53+
/// <returns>A task that represents the asynchronous execute operation.</returns>
54+
async Task IResult.ExecuteAsync(HttpContext httpContext)
55+
{
56+
if (httpContext == null)
57+
{
58+
throw new ArgumentNullException(nameof(httpContext));
59+
}
60+
61+
var response = httpContext.Response;
62+
63+
ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
64+
ContentType,
65+
response.ContentType,
66+
DefaultContentType,
67+
out var resolvedContentType,
68+
out var resolvedContentTypeEncoding);
69+
70+
response.ContentType = resolvedContentType;
71+
72+
if (StatusCode != null)
73+
{
74+
response.StatusCode = StatusCode.Value;
75+
}
76+
77+
var factory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
78+
var logger = factory.CreateLogger<ContentResult>();
79+
80+
logger.ContentResultExecuting(resolvedContentType);
81+
82+
if (Content != null)
83+
{
84+
response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content);
85+
86+
await using (var textWriter = new HttpResponseStreamWriter(response.Body, resolvedContentTypeEncoding))
87+
{
88+
await textWriter.WriteAsync(Content);
89+
90+
// Flushing the HttpResponseStreamWriter does not flush the underlying stream. This just flushes
91+
// the buffered text in the writer.
92+
// We do this rather than letting dispose handle it because dispose would call Write and we want
93+
// to call WriteAsync.
94+
await textWriter.FlushAsync();
95+
}
96+
}
97+
}
4298
}
4399
}

src/Mvc/Mvc.Core/test/ContentResultTest.cs

+79-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class ContentResultTest
2424
MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize;
2525

2626
[Fact]
27-
public async Task ContentResult_Response_NullContent_SetsContentTypeAndEncoding()
27+
public async Task ContentResult_ExecuteResultAsync_Response_NullContent_SetsContentTypeAndEncoding()
2828
{
2929
// Arrange
3030
var contentResult = new ContentResult
@@ -45,6 +45,28 @@ public async Task ContentResult_Response_NullContent_SetsContentTypeAndEncoding(
4545
MediaTypeAssert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType);
4646
}
4747

48+
[Fact]
49+
public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding()
50+
{
51+
// Arrange
52+
var contentResult = new ContentResult
53+
{
54+
Content = null,
55+
ContentType = new MediaTypeHeaderValue("text/plain")
56+
{
57+
Encoding = Encoding.Unicode
58+
}.ToString()
59+
};
60+
var httpContext = GetHttpContext();
61+
var actionContext = GetActionContext(httpContext);
62+
63+
// Act
64+
await ((IResult)contentResult).ExecuteAsync(httpContext);
65+
66+
// Assert
67+
MediaTypeAssert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType);
68+
}
69+
4870
public static TheoryData<MediaTypeHeaderValue, string, string, string, byte[]> ContentResultContentTypeData
4971
{
5072
get
@@ -143,6 +165,36 @@ public async Task ContentResult_ExecuteResultAsync_SetContentTypeAndEncoding_OnR
143165
Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength);
144166
}
145167

168+
[Theory]
169+
[MemberData(nameof(ContentResultContentTypeData))]
170+
public async Task ContentResult_ExecuteAsync_SetContentTypeAndEncoding_OnResponse(
171+
MediaTypeHeaderValue contentType,
172+
string content,
173+
string responseContentType,
174+
string expectedContentType,
175+
byte[] expectedContentData)
176+
{
177+
// Arrange
178+
var contentResult = new ContentResult
179+
{
180+
Content = content,
181+
ContentType = contentType?.ToString()
182+
};
183+
var httpContext = GetHttpContext();
184+
var memoryStream = new MemoryStream();
185+
httpContext.Response.Body = memoryStream;
186+
httpContext.Response.ContentType = responseContentType;
187+
188+
// Act
189+
await ((IResult)contentResult).ExecuteAsync(httpContext);
190+
191+
// Assert
192+
var finalResponseContentType = httpContext.Response.ContentType;
193+
Assert.Equal(expectedContentType, finalResponseContentType);
194+
Assert.Equal(expectedContentData, memoryStream.ToArray());
195+
Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength);
196+
}
197+
146198
public static TheoryData<string, string> ContentResult_WritesDataCorrectly_ForDifferentContentSizesData
147199
{
148200
get
@@ -246,6 +298,31 @@ public async Task ContentResult_WritesDataCorrectly_ForDifferentContentSizes(str
246298
Assert.Equal(content, actualContent);
247299
}
248300

301+
[Theory]
302+
[MemberData(nameof(ContentResult_WritesDataCorrectly_ForDifferentContentSizesData))]
303+
public async Task ContentResult_ExecuteAsync_WritesDataCorrectly_ForDifferentContentSizes(string content, string contentType)
304+
{
305+
// Arrange
306+
var contentResult = new ContentResult
307+
{
308+
Content = content,
309+
ContentType = contentType
310+
};
311+
var httpContext = GetHttpContext();
312+
var memoryStream = new MemoryStream();
313+
httpContext.Response.Body = memoryStream;
314+
var encoding = MediaTypeHeaderValue.Parse(contentType).Encoding;
315+
316+
// Act
317+
await ((IResult)contentResult).ExecuteAsync(httpContext);
318+
319+
// Assert
320+
memoryStream.Seek(0, SeekOrigin.Begin);
321+
var streamReader = new StreamReader(memoryStream, encoding);
322+
var actualContent = await streamReader.ReadToEndAsync();
323+
Assert.Equal(content, actualContent);
324+
}
325+
249326
private static ActionContext GetActionContext(HttpContext httpContext)
250327
{
251328
var routeData = new RouteData();
@@ -270,6 +347,7 @@ private static IServiceCollection CreateServices()
270347
services.AddSingleton<IActionResultExecutor<ContentResult>>(new ContentResultExecutor(
271348
new Logger<ContentResultExecutor>(NullLoggerFactory.Instance),
272349
new MemoryPoolHttpResponseStreamWriterFactory(ArrayPool<byte>.Shared, charArrayPool.Object)));
350+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
273351
return services;
274352
}
275353

0 commit comments

Comments
 (0)