-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Fix #1370 - Always use the provided formatter in JsonResult #1480
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,104 +9,124 @@ | |
|
||
namespace Microsoft.AspNet.Mvc | ||
{ | ||
/// <summary> | ||
/// An action result which formats the given object as JSON. | ||
/// </summary> | ||
public class JsonResult : ActionResult | ||
{ | ||
private static readonly IList<MediaTypeHeaderValue> _defaultSupportedContentTypes = | ||
new List<MediaTypeHeaderValue>() | ||
{ | ||
MediaTypeHeaderValue.Parse("application/json"), | ||
MediaTypeHeaderValue.Parse("text/json"), | ||
}; | ||
private IOutputFormatter _defaultFormatter; | ||
|
||
private ObjectResult _objectResult; | ||
|
||
public JsonResult(object data) : | ||
this(data, null) | ||
/// <summary> | ||
/// The list of content-types used for formatting when <see cref="ContentTypes"/> is null or empty. | ||
/// </summary> | ||
public static readonly IReadOnlyList<MediaTypeHeaderValue> DefaultContentTypes = new MediaTypeHeaderValue[] | ||
{ | ||
} | ||
MediaTypeHeaderValue.Parse("application/json"), | ||
MediaTypeHeaderValue.Parse("text/json"), | ||
}; | ||
|
||
/// <summary> | ||
/// Creates an instance of <see cref="JsonResult"/> class. | ||
/// Creates a new <see cref="JsonResult"/> with the given <paramref name="data"/>. | ||
/// </summary> | ||
/// <param name="data"></param> | ||
/// <param name="defaultFormatter">If no matching formatter is found, | ||
/// the response is written to using defaultFormatter.</param> | ||
/// <remarks> | ||
/// The default formatter must be able to handle either application/json | ||
/// or text/json. | ||
/// </remarks> | ||
public JsonResult(object data, IOutputFormatter defaultFormatter) | ||
/// <param name="value">The value to format as JSON.</param> | ||
public JsonResult(object value) | ||
: this(value, formatter: null) | ||
{ | ||
_defaultFormatter = defaultFormatter; | ||
_objectResult = new ObjectResult(data); | ||
} | ||
|
||
public object Value | ||
/// <summary> | ||
/// Creates a new <see cref="JsonResult"/> with the given <paramref name="data"/>. | ||
/// </summary> | ||
/// <param name="value">The value to format as JSON.</param> | ||
/// <param name="formatter">The formatter to use, or <c>null</c> to choose a formatter dynamically.</param> | ||
public JsonResult(object value, IOutputFormatter formatter) | ||
{ | ||
get | ||
{ | ||
return _objectResult.Value; | ||
} | ||
set | ||
{ | ||
_objectResult.Value = value; | ||
} | ||
} | ||
Value = value; | ||
Formatter = formatter; | ||
|
||
public IList<MediaTypeHeaderValue> ContentTypes | ||
{ | ||
get | ||
{ | ||
return _objectResult.ContentTypes; | ||
} | ||
set | ||
{ | ||
_objectResult.ContentTypes = value; | ||
} | ||
ContentTypes = new List<MediaTypeHeaderValue>(); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the list of supported Content-Types. | ||
/// </summary> | ||
public IList<MediaTypeHeaderValue> ContentTypes { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the formatter. | ||
/// </summary> | ||
public IOutputFormatter Formatter { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the value to be formatted. | ||
/// </summary> | ||
public object Value { get; set; } | ||
|
||
/// <inheritdoc /> | ||
public override async Task ExecuteResultAsync([NotNull] ActionContext context) | ||
{ | ||
var objectResult = new ObjectResult(Value); | ||
|
||
// Set the content type explicitly to application/json and text/json. | ||
// if the user has not already set it. | ||
if (ContentTypes == null || ContentTypes.Count == 0) | ||
{ | ||
ContentTypes = _defaultSupportedContentTypes; | ||
foreach (var contentType in DefaultContentTypes) | ||
{ | ||
objectResult.ContentTypes.Add(contentType); | ||
} | ||
} | ||
else | ||
{ | ||
objectResult.ContentTypes = ContentTypes; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we always honor the default Declared type which is present in objectResult and pass that around? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the suggestion here? In all possible cases There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is fragile because it depends on this value explicitly being null. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense 👍 I'll put it back |
||
|
||
var formatterContext = new OutputFormatterContext() | ||
{ | ||
DeclaredType = _objectResult.DeclaredType, | ||
ActionContext = context, | ||
DeclaredType = objectResult.DeclaredType, | ||
Object = Value, | ||
}; | ||
|
||
// Need to call this instead of directly calling _objectResult.ExecuteResultAsync | ||
// as that sets the status to 406 if a formatter is not found. | ||
// this can be cleaned up after https://github.com/aspnet/Mvc/issues/941 gets resolved. | ||
var formatter = SelectFormatter(formatterContext); | ||
var formatter = SelectFormatter(objectResult, formatterContext); | ||
await formatter.WriteAsync(formatterContext); | ||
} | ||
|
||
private IOutputFormatter SelectFormatter(OutputFormatterContext formatterContext) | ||
private IOutputFormatter SelectFormatter(ObjectResult objectResult, OutputFormatterContext formatterContext) | ||
{ | ||
var defaultFormatters = formatterContext.ActionContext | ||
.HttpContext | ||
.RequestServices | ||
.GetRequiredService<IOutputFormattersProvider>() | ||
.OutputFormatters; | ||
|
||
var formatter = _objectResult.SelectFormatter(formatterContext, defaultFormatters); | ||
if (formatter == null) | ||
if (Formatter == null) | ||
{ | ||
formatter = _defaultFormatter ?? formatterContext.ActionContext | ||
.HttpContext | ||
.RequestServices | ||
.GetRequiredService<JsonOutputFormatter>(); | ||
} | ||
// If no formatter was provided, then run Conneg with the formatters configured in options. | ||
var formatters = formatterContext | ||
.ActionContext | ||
.HttpContext | ||
.RequestServices | ||
.GetRequiredService<IOutputFormattersProvider>() | ||
.OutputFormatters | ||
.OfType<IJsonOutputFormatter>() | ||
.ToArray(); | ||
|
||
var formatter = objectResult.SelectFormatter(formatterContext, formatters); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wondering what the behavior should be when someone sets the value to be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
NoContentFormatter actually does pick this up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cool..thanks!...could you add couple of tests for this scenario? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I'll add a functional test that does null. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, coming back to this after some discussion... It turns out NoContentFormatter does pick this up and write a 204. So I went back to old-MVC, which just does nothing if the value is null. So in that spirit 204 is actually right. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably we should add a functional test which turns on the |
||
if (formatter == null) | ||
{ | ||
// If the available user-configured formatters can't write this type, then fall back to the | ||
// 'global' one. | ||
formatter = formatterContext | ||
.ActionContext | ||
.HttpContext | ||
.RequestServices | ||
.GetRequiredService<JsonOutputFormatter>(); | ||
|
||
return formatter; | ||
// Run SelectFormatter again to try to choose a content type that this formatter can do. | ||
objectResult.SelectFormatter(formatterContext, new[] { formatter }); | ||
} | ||
|
||
return formatter; | ||
} | ||
else | ||
{ | ||
// Run SelectFormatter to try to choose a content type that this formatter can do. | ||
objectResult.SelectFormatter(formatterContext, new[] { Formatter }); | ||
return Formatter; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
namespace Microsoft.AspNet.Mvc | ||
{ | ||
/// <summary> | ||
/// An output formatter that specializes in writing JSON content. | ||
/// </summary> | ||
/// <remarks> | ||
/// The <see cref="JsonResult"/> class filter the collection of | ||
/// <see cref="IOutputFormattersProvider.OutputFormatters"/> and use only those which implement | ||
/// <see cref="IJsonOutputFormatter"/>. | ||
/// | ||
/// To create a custom formatter that can be used by <see cref="JsonResult"/>, derive from | ||
/// <see cref="JsonOutputFormatter"/> or implement <see cref="IJsonOutputFormatter"/>. | ||
/// </remarks> | ||
public interface IJsonOutputFormatter : IOutputFormatter | ||
{ | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not make the property a getter only? Saves the trouble of having to do null checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plus this was a FxCop design violation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is consistent with ObjectResult, leaving it alone.