Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 258f314

Browse files
committed
Use strongly typed MediaTypeHeaderValue for content type in action results.
1 parent 955b45f commit 258f314

23 files changed

+479
-163
lines changed

samples/MvcSample.Web/Filters/ErrorMessagesAttribute.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using Microsoft.AspNet.Mvc;
5+
using Microsoft.Net.Http.Headers;
56

67
namespace MvcSample.Web
78
{
@@ -15,7 +16,7 @@ public override void OnActionExecuted(ActionExecutedContext context)
1516

1617
context.Result = new ContentResult
1718
{
18-
ContentType = "text/plain",
19+
ContentType = new MediaTypeHeaderValue("text/plain"),
1920
Content = "Boom " + context.Exception.Message
2021
};
2122
}

src/Microsoft.AspNet.Mvc.Core/ActionResults/ContentResult.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ namespace Microsoft.AspNet.Mvc
1111
{
1212
public class ContentResult : ActionResult
1313
{
14-
public string Content { get; set; }
14+
private readonly MediaTypeHeaderValue DefaultContentType = new MediaTypeHeaderValue("text/plain")
15+
{
16+
Encoding = Encodings.UTF8EncodingWithoutBOM
17+
};
1518

16-
public Encoding ContentEncoding { get; set; }
19+
public string Content { get; set; }
1720

18-
public string ContentType { get; set; }
21+
public MediaTypeHeaderValue ContentType { get; set; }
1922

2023
/// <summary>
2124
/// Gets or sets the HTTP status code.
@@ -26,17 +29,34 @@ public override async Task ExecuteResultAsync([NotNull] ActionContext context)
2629
{
2730
var response = context.HttpContext.Response;
2831

32+
// Encoding property on MediaTypeHeaderValue does not return the exact encoding instance that
33+
// is set, so any settings(for example: BOM) on it will be lost when retrieving the value.
34+
// In the scenario where the user does not supply encoding, we want to use UTF8 without BOM and
35+
// for this reason do not rely on Encoding property.
2936
MediaTypeHeaderValue contentTypeHeader;
30-
if (string.IsNullOrEmpty(ContentType))
37+
Encoding encoding;
38+
if (ContentType == null)
3139
{
32-
contentTypeHeader = new MediaTypeHeaderValue("text/plain");
40+
encoding = Encodings.UTF8EncodingWithoutBOM;
41+
contentTypeHeader = DefaultContentType;
3342
}
3443
else
3544
{
36-
contentTypeHeader = new MediaTypeHeaderValue(ContentType);
45+
if (ContentType.Encoding == null)
46+
{
47+
encoding = Encodings.UTF8EncodingWithoutBOM;
48+
// 1. Do not modify the user supplied content type
49+
// 2. Parse here to handle parameters apart from charset
50+
contentTypeHeader = MediaTypeHeaderValue.Parse(ContentType.ToString());
51+
contentTypeHeader.Encoding = encoding;
52+
}
53+
else
54+
{
55+
encoding = ContentType.Encoding;
56+
contentTypeHeader = ContentType;
57+
}
3758
}
3859

39-
contentTypeHeader.Encoding = ContentEncoding ?? Encodings.UTF8EncodingWithoutBOM;
4060
response.ContentType = contentTypeHeader.ToString();
4161

4262
if (StatusCode != null)
@@ -46,7 +66,7 @@ public override async Task ExecuteResultAsync([NotNull] ActionContext context)
4666

4767
if (Content != null)
4868
{
49-
await response.WriteAsync(Content, contentTypeHeader.Encoding);
69+
await response.WriteAsync(Content, encoding);
5070
}
5171
}
5272
}

src/Microsoft.AspNet.Mvc.Core/ActionResults/FileContentResult.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading.Tasks;
77
using Microsoft.AspNet.Http;
88
using Microsoft.Framework.Internal;
9+
using Microsoft.Net.Http.Headers;
910

1011
namespace Microsoft.AspNet.Mvc
1112
{
@@ -19,13 +20,14 @@ public class FileContentResult : FileResult
1920

2021
/// <summary>
2122
/// Creates a new <see cref="FileContentResult"/> instance with
22-
/// the provided <paramref name="fileContents"/>.
23+
/// the provided <paramref name="fileContents"/> and the
24+
/// provided <paramref name="contentType"/>.
2325
/// </summary>
2426
/// <param name="fileContents">The bytes that represent the file contents.</param>
25-
public FileContentResult([NotNull] byte[] fileContents)
26-
: base(contentType: null)
27+
/// <param name="contentType">The Content-Type header of the response.</param>
28+
public FileContentResult([NotNull] byte[] fileContents, string contentType)
29+
: this(fileContents, new MediaTypeHeaderValue(contentType))
2730
{
28-
FileContents = fileContents;
2931
}
3032

3133
/// <summary>
@@ -35,7 +37,7 @@ public FileContentResult([NotNull] byte[] fileContents)
3537
/// </summary>
3638
/// <param name="fileContents">The bytes that represent the file contents.</param>
3739
/// <param name="contentType">The Content-Type header of the response.</param>
38-
public FileContentResult([NotNull] byte[] fileContents, string contentType)
40+
public FileContentResult([NotNull] byte[] fileContents, MediaTypeHeaderValue contentType)
3941
: base(contentType)
4042
{
4143
FileContents = fileContents;

src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.AspNet.Mvc.Core;
1212
using Microsoft.Framework.DependencyInjection;
1313
using Microsoft.Framework.Internal;
14+
using Microsoft.Net.Http.Headers;
1415

1516
namespace Microsoft.AspNet.Mvc
1617
{
@@ -27,15 +28,15 @@ public class FilePathResult : FileResult
2728

2829
/// <summary>
2930
/// Creates a new <see cref="FilePathResult"/> instance with
30-
/// the provided <paramref name="fileName"/>
31+
/// the provided <paramref name="fileName"/> and the
32+
/// provided <paramref name="contentType"/>.
3133
/// </summary>
3234
/// <param name="fileName">The path to the file. The path must be an absolute
3335
/// path. Relative and virtual paths are not supported.</param>
3436
/// <param name="contentType">The Content-Type header of the response.</param>
35-
public FilePathResult([NotNull] string fileName)
36-
: base(contentType: null)
37+
public FilePathResult([NotNull] string fileName, string contentType)
38+
: this(fileName, new MediaTypeHeaderValue(contentType))
3739
{
38-
FileName = fileName;
3940
}
4041

4142
/// <summary>
@@ -46,7 +47,7 @@ public FilePathResult([NotNull] string fileName)
4647
/// <param name="fileName">The path to the file. The path must be an absolute
4748
/// path. Relative and virtual paths are not supported.</param>
4849
/// <param name="contentType">The Content-Type header of the response.</param>
49-
public FilePathResult([NotNull] string fileName, string contentType)
50+
public FilePathResult([NotNull] string fileName, MediaTypeHeaderValue contentType)
5051
: base(contentType)
5152
{
5253
FileName = fileName;

src/Microsoft.AspNet.Mvc.Core/ActionResults/FileResult.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,24 @@ public abstract class FileResult : ActionResult
2525
/// </summary>
2626
/// <param name="contentType">The Content-Type header of the response.</param>
2727
protected FileResult(string contentType)
28+
: this(new MediaTypeHeaderValue(contentType))
29+
{
30+
}
31+
32+
/// <summary>
33+
/// Creates a new <see cref="FileResult"/> instance with
34+
/// the provided <paramref name="Microsoft.Net.Http.Headers.MediaTypeHeaderValue"/>.
35+
/// </summary>
36+
/// <param name="contentType">The Content-Type header of the response.</param>
37+
protected FileResult([NotNull] MediaTypeHeaderValue contentType)
2838
{
2939
ContentType = contentType;
3040
}
3141

3242
/// <summary>
3343
/// Gets or sets the Content-Type header value that will be written to the response.
3444
/// </summary>
35-
public string ContentType { get; set; }
45+
public MediaTypeHeaderValue ContentType { get; set; }
3646

3747
/// <summary>
3848
/// Gets the file name that will be used in the Content-Disposition header of the response.
@@ -47,11 +57,7 @@ public string FileDownloadName
4757
public override Task ExecuteResultAsync([NotNull] ActionContext context)
4858
{
4959
var response = context.HttpContext.Response;
50-
51-
if (ContentType != null)
52-
{
53-
response.ContentType = ContentType;
54-
}
60+
response.ContentType = ContentType.ToString();
5561

5662
if (!string.IsNullOrEmpty(FileDownloadName))
5763
{

src/Microsoft.AspNet.Mvc.Core/ActionResults/FileStreamResult.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading.Tasks;
88
using Microsoft.AspNet.Http;
99
using Microsoft.Framework.Internal;
10+
using Microsoft.Net.Http.Headers;
1011

1112
namespace Microsoft.AspNet.Mvc
1213
{
@@ -23,13 +24,14 @@ public class FileStreamResult : FileResult
2324

2425
/// <summary>
2526
/// Creates a new <see cref="FileStreamResult"/> instance with
26-
/// the provided <paramref name="fileStream"/>.
27+
/// the provided <paramref name="fileStream"/> and the
28+
/// provided <paramref name="contentType"/>.
2729
/// </summary>
2830
/// <param name="fileStream">The stream with the file.</param>
29-
public FileStreamResult([NotNull] Stream fileStream)
30-
: base(contentType: null)
31+
/// <param name="contentType">The Content-Type header of the response.</param>
32+
public FileStreamResult([NotNull] Stream fileStream, string contentType)
33+
: this(fileStream, new MediaTypeHeaderValue(contentType))
3134
{
32-
FileStream = fileStream;
3335
}
3436

3537
/// <summary>
@@ -39,7 +41,7 @@ public FileStreamResult([NotNull] Stream fileStream)
3941
/// </summary>
4042
/// <param name="fileStream">The stream with the file.</param>
4143
/// <param name="contentType">The Content-Type header of the response.</param>
42-
public FileStreamResult([NotNull] Stream fileStream, string contentType)
44+
public FileStreamResult([NotNull] Stream fileStream, MediaTypeHeaderValue contentType)
4345
: base(contentType)
4446
{
4547
FileStream = fileStream;

src/Microsoft.AspNet.Mvc.Core/ActionResults/PartialViewResult.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Framework.DependencyInjection;
88
using Microsoft.Framework.Internal;
99
using Microsoft.Framework.Logging;
10+
using Microsoft.Net.Http.Headers;
1011

1112
namespace Microsoft.AspNet.Mvc
1213
{
@@ -45,6 +46,11 @@ public class PartialViewResult : ActionResult
4546
/// <c>ActionContext.HttpContext.RequestServices</c> is used.</remarks>
4647
public IViewEngine ViewEngine { get; set; }
4748

49+
/// <summary>
50+
/// Gets or sets the <see cref="MediaTypeHeaderValue"/> representing the Content-Type header of the response.
51+
/// </summary>
52+
public MediaTypeHeaderValue ContentType { get; set; }
53+
4854
/// <inheritdoc />
4955
public override async Task ExecuteResultAsync([NotNull] ActionContext context)
5056
{
@@ -74,7 +80,7 @@ public override async Task ExecuteResultAsync([NotNull] ActionContext context)
7480

7581
using (view as IDisposable)
7682
{
77-
await ViewExecutor.ExecuteAsync(view, context, ViewData, TempData, contentType: null);
83+
await ViewExecutor.ExecuteAsync(view, context, ViewData, TempData, ContentType);
7884
}
7985
}
8086
}

src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewExecutor.cs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.IO;
5+
using System.Text;
56
using System.Threading.Tasks;
67
using Microsoft.AspNet.Mvc.Rendering;
78
using Microsoft.Framework.Internal;
9+
using Microsoft.Net.Http.Headers;
810

911
namespace Microsoft.AspNet.Mvc
1012
{
@@ -14,7 +16,10 @@ namespace Microsoft.AspNet.Mvc
1416
public static class ViewExecutor
1517
{
1618
private const int BufferSize = 1024;
17-
private const string ContentType = "text/html; charset=utf-8";
19+
private static readonly MediaTypeHeaderValue DefaultContentType = new MediaTypeHeaderValue("text/html")
20+
{
21+
Encoding = Encodings.UTF8EncodingWithoutBOM
22+
};
1823

1924
/// <summary>
2025
/// Asynchronously renders the specified <paramref name="view"/> to the response body.
@@ -23,22 +28,52 @@ public static class ViewExecutor
2328
/// <param name="actionContext">The <see cref="ActionContext"/> for the current executing action.</param>
2429
/// <param name="viewData">The <see cref="ViewDataDictionary"/> for the view being rendered.</param>
2530
/// <param name="tempData">The <see cref="ITempDataDictionary"/> for the view being rendered.</param>
26-
/// <returns>A <see cref="Task"/> that represents the asychronous rendering.</returns>
31+
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering.</returns>
2732
public static async Task ExecuteAsync([NotNull] IView view,
2833
[NotNull] ActionContext actionContext,
2934
[NotNull] ViewDataDictionary viewData,
3035
[NotNull] ITempDataDictionary tempData,
31-
string contentType)
36+
MediaTypeHeaderValue contentType)
3237
{
33-
if (string.IsNullOrEmpty(contentType))
38+
var response = actionContext.HttpContext.Response;
39+
40+
// Encoding property on MediaTypeHeaderValue does not return the exact encoding instance that
41+
// is set, so any settings(for example: BOM) on it will be lost when retrieving the value.
42+
// In the scenario where the user does not supply encoding, we want to use UTF8 without BOM and
43+
// for this reason do not rely on Encoding property.
44+
MediaTypeHeaderValue contentTypeHeader;
45+
Encoding encoding;
46+
if (contentType == null)
3447
{
35-
contentType = ContentType;
48+
encoding = Encodings.UTF8EncodingWithoutBOM;
49+
contentTypeHeader = DefaultContentType;
3650
}
51+
else
52+
{
53+
if (contentType.Encoding == null)
54+
{
55+
encoding = Encodings.UTF8EncodingWithoutBOM;
56+
// 1. Do not modify the user supplied content type
57+
// 2. Parse here to handle parameters apart from charset
58+
contentTypeHeader = MediaTypeHeaderValue.Parse(contentType.ToString());
59+
contentTypeHeader.Encoding = encoding;
60+
}
61+
else
62+
{
63+
encoding = contentType.Encoding;
64+
contentTypeHeader = contentType;
65+
}
66+
}
67+
68+
response.ContentType = contentTypeHeader.ToString();
69+
70+
var wrappedStream = new StreamWrapper(response.Body);
3771

38-
actionContext.HttpContext.Response.ContentType = contentType;
39-
var wrappedStream = new StreamWrapper(actionContext.HttpContext.Response.Body);
40-
var encoding = Encodings.UTF8EncodingWithoutBOM;
41-
using (var writer = new StreamWriter(wrappedStream, encoding, BufferSize, leaveOpen: true))
72+
using (var writer = new StreamWriter(
73+
wrappedStream,
74+
encoding,
75+
BufferSize,
76+
leaveOpen: true))
4277
{
4378
try
4479
{

src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Framework.DependencyInjection;
88
using Microsoft.Framework.Internal;
99
using Microsoft.Framework.Logging;
10+
using Microsoft.Net.Http.Headers;
1011

1112
namespace Microsoft.AspNet.Mvc
1213
{
@@ -45,6 +46,11 @@ public class ViewResult : ActionResult
4546
/// <c>ActionContext.HttpContext.RequestServices</c> is used.</remarks>
4647
public IViewEngine ViewEngine { get; set; }
4748

49+
/// <summary>
50+
/// Gets or sets the <see cref="MediaTypeHeaderValue"/> representing the Content-Type header of the response.
51+
/// </summary>
52+
public MediaTypeHeaderValue ContentType { get; set; }
53+
4854
/// <inheritdoc />
4955
public override async Task ExecuteResultAsync([NotNull] ActionContext context)
5056
{
@@ -74,7 +80,7 @@ public override async Task ExecuteResultAsync([NotNull] ActionContext context)
7480

7581
using (view as IDisposable)
7682
{
77-
await ViewExecutor.ExecuteAsync(view, context, ViewData, TempData, contentType: null);
83+
await ViewExecutor.ExecuteAsync(view, context, ViewData, TempData, ContentType);
7884
}
7985
}
8086
}

0 commit comments

Comments
 (0)