JsonResult to use ObjectResult for content negotiation to pick default json formatter.

This commit is contained in:
harshgMSFT 2014-07-30 14:51:37 -07:00
parent 2920c55116
commit bb452f19a7
14 changed files with 289 additions and 78 deletions

View File

@ -1,77 +1,112 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System.Collections.Generic;
using System.IO; using System.Linq;
using System.Text; using System.Threading.Tasks;
using Newtonsoft.Json; using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
public class JsonResult : ActionResult public class JsonResult : ActionResult
{ {
private const int BufferSize = 1024; private static readonly IList<MediaTypeHeaderValue> _defaultSupportedContentTypes =
new List<MediaTypeHeaderValue>()
{
MediaTypeHeaderValue.Parse("application/json"),
MediaTypeHeaderValue.Parse("text/json"),
};
private IOutputFormatter _defaultFormatter;
private JsonSerializerSettings _jsonSerializerSettings; private ObjectResult _objectResult;
private Encoding _encoding = Encodings.UTF8EncodingWithoutBOM;
public JsonResult([NotNull] object data) public JsonResult(object data) :
this(data, null)
{ {
Data = data;
_jsonSerializerSettings = JsonOutputFormatter.CreateDefaultSettings();
}
public JsonSerializerSettings SerializerSettings
{
get { return _jsonSerializerSettings; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
_jsonSerializerSettings = value;
}
} }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to indent elements when writing data. /// Creates an instance of <see cref="JsonResult"/> class.
/// </summary> /// </summary>
public bool Indent { get; set; } /// <param name="data"></param>
/// <param name="defaultFormatter">If no matching formatter is found,
public Encoding Encoding /// 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)
{ {
get { return _encoding; } _defaultFormatter = defaultFormatter;
_objectResult = new ObjectResult(data);
}
public object Value
{
get
{
return _objectResult.Value;
}
set set
{ {
if (value == null) _objectResult.Value = value;
{
throw new ArgumentNullException("value");
}
_encoding = value;
} }
} }
public object Data { get; private set; } public IList<MediaTypeHeaderValue> ContentTypes
public override void ExecuteResult([NotNull] ActionContext context)
{ {
var response = context.HttpContext.Response; get
var writeStream = response.Body;
if (response.ContentType == null)
{ {
response.ContentType = "application/json"; return _objectResult.ContentTypes;
}
set
{
_objectResult.ContentTypes = value;
}
}
public override async Task ExecuteResultAsync([NotNull] ActionContext context)
{
// 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;
} }
using (var writer = new StreamWriter(writeStream, Encoding, BufferSize, leaveOpen: true)) var formatterContext = new OutputFormatterContext()
{ {
var formatter = new JsonOutputFormatter(SerializerSettings, Indent); DeclaredType = _objectResult.DeclaredType,
formatter.WriteObject(writer, Data); ActionContext = context,
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);
await formatter.WriteAsync(formatterContext);
}
private IOutputFormatter SelectFormatter(OutputFormatterContext formatterContext)
{
var defaultFormatters = formatterContext.ActionContext
.HttpContext
.RequestServices
.GetService<IOutputFormattersProvider>()
.OutputFormatters;
var formatter = _objectResult.SelectFormatter(formatterContext, defaultFormatters);
if (formatter == null)
{
formatter = _defaultFormatter ?? formatterContext.ActionContext
.HttpContext
.RequestServices
.GetService<JsonOutputFormatter>();
} }
return formatter;
} }
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions; using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;

View File

@ -79,16 +79,7 @@ namespace Microsoft.AspNet.Mvc
var selectedEncoding = context.SelectedEncoding; var selectedEncoding = context.SelectedEncoding;
using (var writer = new StreamWriter(response.Body, selectedEncoding, 1024, leaveOpen: true)) using (var writer = new StreamWriter(response.Body, selectedEncoding, 1024, leaveOpen: true))
{ {
using (var jsonWriter = CreateJsonWriter(writer)) WriteObject(writer, context.Object);
{
var jsonSerializer = CreateJsonSerializer();
jsonSerializer.Serialize(jsonWriter, context.Object);
// We're explicitly calling flush here to simplify the debugging experience because the
// underlying TextWriter might be long-lived. If this method ends up being called repeatedly
// for a request, we should revisit.
jsonWriter.Flush();
}
} }
return Task.FromResult(true); return Task.FromResult(true);

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc
/// with default XmlWriterSettings /// with default XmlWriterSettings
/// </summary> /// </summary>
public XmlDataContractSerializerOutputFormatter() public XmlDataContractSerializerOutputFormatter()
:this(GetDefaultXmlWriterSettings()) : this(GetDefaultXmlWriterSettings())
{ {
} }
@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc
/// </summary> /// </summary>
/// <param name="writerSettings">The settings to be used by the <see cref="DataContractSerializer"/>.</param> /// <param name="writerSettings">The settings to be used by the <see cref="DataContractSerializer"/>.</param>
public XmlDataContractSerializerOutputFormatter([NotNull] XmlWriterSettings writerSettings) public XmlDataContractSerializerOutputFormatter([NotNull] XmlWriterSettings writerSettings)
:base(writerSettings) : base(writerSettings)
{ {
} }

View File

@ -47,7 +47,8 @@ namespace Microsoft.AspNet.Mvc
/// </summary> /// </summary>
/// <param name="writeStream">The stream on which the XmlWriter should operate on.</param> /// <param name="writeStream">The stream on which the XmlWriter should operate on.</param>
/// <returns>A new instance of <see cref="XmlWriter"/></returns> /// <returns>A new instance of <see cref="XmlWriter"/></returns>
public virtual XmlWriter CreateXmlWriter([NotNull] Stream writeStream, [NotNull] XmlWriterSettings xmlWriterSettings) public virtual XmlWriter CreateXmlWriter([NotNull] Stream writeStream,
[NotNull] XmlWriterSettings xmlWriterSettings)
{ {
return XmlWriter.Create(writeStream, xmlWriterSettings); return XmlWriter.Create(writeStream, xmlWriterSettings);
} }

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc
/// with default XmlWriterSettings. /// with default XmlWriterSettings.
/// </summary> /// </summary>
public XmlSerializerOutputFormatter() public XmlSerializerOutputFormatter()
:this(GetDefaultXmlWriterSettings()) : this(GetDefaultXmlWriterSettings())
{ {
} }
@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc
/// </summary> /// </summary>
/// <param name="writerSettings">The settings to be used by the <see cref="XmlSerializer"/>.</param> /// <param name="writerSettings">The settings to be used by the <see cref="XmlSerializer"/>.</param>
public XmlSerializerOutputFormatter([NotNull] XmlWriterSettings writerSettings) public XmlSerializerOutputFormatter([NotNull] XmlWriterSettings writerSettings)
:base(writerSettings) : base(writerSettings)
{ {
} }

View File

@ -75,6 +75,9 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Scoped<ICompositeValueProviderFactory, CompositeValueProviderFactory>(); yield return describe.Scoped<ICompositeValueProviderFactory, CompositeValueProviderFactory>();
yield return describe.Transient<IOutputFormattersProvider, DefaultOutputFormattersProvider>(); yield return describe.Transient<IOutputFormattersProvider, DefaultOutputFormattersProvider>();
yield return describe.Instance<JsonOutputFormatter>(
new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false));
yield return describe.Transient<INestedProvider<FilterProviderContext>, DefaultFilterProvider>(); yield return describe.Transient<INestedProvider<FilterProviderContext>, DefaultFilterProvider>();
yield return describe.Transient<IModelValidatorProvider, DataAnnotationsModelValidatorProvider>(); yield return describe.Transient<IModelValidatorProvider, DataAnnotationsModelValidatorProvider>();

View File

@ -517,7 +517,7 @@ namespace Microsoft.AspNet.Mvc.Test
// Assert // Assert
Assert.IsType<JsonResult>(actualJsonResult); Assert.IsType<JsonResult>(actualJsonResult);
Assert.Same(data, actualJsonResult.Data); Assert.Same(data, actualJsonResult.Value);
} }
public static IEnumerable<object[]> RedirectTestData public static IEnumerable<object[]> RedirectTestData

View File

@ -1,12 +1,14 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc
{ {
private static readonly byte[] _abcdUTF8Bytes private static readonly byte[] _abcdUTF8Bytes
= new byte[] { 123, 34, 102, 111, 111, 34, 58, 34, 97, 98, 99, 100, 34, 125 }; = new byte[] { 123, 34, 102, 111, 111, 34, 58, 34, 97, 98, 99, 100, 34, 125 };
private JsonOutputFormatter _jsonformatter = new JsonOutputFormatter(
JsonOutputFormatter.CreateDefaultSettings(),
indent: false);
[Fact] [Fact]
public async Task ExecuteResult_GeneratesResultsWithoutBOMByDefault() public async Task ExecuteResult_GeneratesResultsWithoutBOMByDefault()
{ {
@ -28,10 +32,8 @@ namespace Microsoft.AspNet.Mvc
var response = new Mock<HttpResponse>(); var response = new Mock<HttpResponse>();
response.SetupGet(r => r.Body) response.SetupGet(r => r.Body)
.Returns(memoryStream); .Returns(memoryStream);
var context = new Mock<HttpContext>(); var context = GetHttpContext(response.Object);
context.SetupGet(c => c.Response) var actionContext = new ActionContext(context,
.Returns(response.Object);
var actionContext = new ActionContext(context.Object,
new RouteData(), new RouteData(),
new ActionDescriptor()); new ActionDescriptor());
var result = new JsonResult(new { foo = "abcd" }); var result = new JsonResult(new { foo = "abcd" });
@ -44,7 +46,7 @@ namespace Microsoft.AspNet.Mvc
} }
[Fact] [Fact]
public async Task ExecuteResult_UsesEncoderIfSpecified() public async Task ExecuteResult_IfNoMatchFoundUsesPassedInFormatter()
{ {
// Arrange // Arrange
var expected = Enumerable.Concat(Encoding.UTF8.GetPreamble(), _abcdUTF8Bytes); var expected = Enumerable.Concat(Encoding.UTF8.GetPreamble(), _abcdUTF8Bytes);
@ -52,16 +54,16 @@ namespace Microsoft.AspNet.Mvc
var response = new Mock<HttpResponse>(); var response = new Mock<HttpResponse>();
response.SetupGet(r => r.Body) response.SetupGet(r => r.Body)
.Returns(memoryStream); .Returns(memoryStream);
var context = new Mock<HttpContext>(); var context = GetHttpContext(response.Object, registerDefaultFormatter: false);
context.SetupGet(c => c.Response) var actionContext = new ActionContext(context,
.Returns(response.Object);
var actionContext = new ActionContext(context.Object,
new RouteData(), new RouteData(),
new ActionDescriptor()); new ActionDescriptor());
var result = new JsonResult(new { foo = "abcd" }) var testFormatter = new TestJsonFormatter()
{ {
Encoding = Encoding.UTF8 Encoding = Encoding.UTF8
}; };
var result = new JsonResult(new { foo = "abcd" }, testFormatter);
// Act // Act
await result.ExecuteResultAsync(actionContext); await result.ExecuteResultAsync(actionContext);
@ -69,5 +71,61 @@ namespace Microsoft.AspNet.Mvc
// Assert // Assert
Assert.Equal(expected, memoryStream.ToArray()); Assert.Equal(expected, memoryStream.ToArray());
} }
public async Task ExecuteResult_UsesDefaultFormatter_IfNoneIsRegistered_AndNoneIsPassed()
{
// Arrange
var expected = _abcdUTF8Bytes;
var memoryStream = new MemoryStream();
var response = new Mock<HttpResponse>();
response.SetupGet(r => r.Body)
.Returns(memoryStream);
var context = GetHttpContext(response.Object, registerDefaultFormatter: false);
var actionContext = new ActionContext(context,
new RouteData(),
new ActionDescriptor());
var result = new JsonResult(new { foo = "abcd" });
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
Assert.Equal(expected, memoryStream.ToArray());
}
private HttpContext GetHttpContext(HttpResponse response, bool registerDefaultFormatter = true)
{
var defaultFormatters = registerDefaultFormatter ? new List<IOutputFormatter>() { _jsonformatter } :
new List<IOutputFormatter>();
var httpContext = new Mock<HttpContext>();
var mockFormattersProvider = new Mock<IOutputFormattersProvider>();
mockFormattersProvider.SetupGet(o => o.OutputFormatters)
.Returns(defaultFormatters);
httpContext.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider)))
.Returns(mockFormattersProvider.Object);
httpContext.SetupGet(o => o.Request.Accept)
.Returns("");
httpContext.SetupGet(c => c.Response).Returns(response);
return httpContext.Object;
}
private class TestJsonFormatter : IOutputFormatter
{
public Encoding Encoding { get; set; }
public bool CanWriteResult(OutputFormatterContext context, MediaTypeHeaderValue contentType)
{
return true;
}
public async Task WriteAsync(OutputFormatterContext context)
{
// Override using the selected encoding.
context.SelectedEncoding = Encoding;
var jsonFormatter = new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(),
indent: false);
await jsonFormatter.WriteResponseBodyAsync(context);
}
}
} }
} }

View File

@ -1295,7 +1295,20 @@ namespace Microsoft.AspNet.Mvc
var httpContext = new Mock<HttpContext>(MockBehavior.Loose); var httpContext = new Mock<HttpContext>(MockBehavior.Loose);
var httpResponse = new Mock<HttpResponse>(MockBehavior.Loose); var httpResponse = new Mock<HttpResponse>(MockBehavior.Loose);
var mockFormattersProvider = new Mock<IOutputFormattersProvider>();
mockFormattersProvider.SetupGet(o => o.OutputFormatters)
.Returns(
new List<IOutputFormatter>()
{
new JsonOutputFormatter(
JsonOutputFormatter.CreateDefaultSettings(),
indent: false)
});
httpContext.SetupGet(o => o.Request.Accept)
.Returns("");
httpContext.SetupGet(c => c.Response).Returns(httpResponse.Object); httpContext.SetupGet(c => c.Response).Returns(httpResponse.Object);
httpContext.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider)))
.Returns(mockFormattersProvider.Object);
httpResponse.SetupGet(r => r.Body).Returns(new MemoryStream()); httpResponse.SetupGet(r => r.Body).Returns(new MemoryStream());
var actionContext = new ActionContext( var actionContext = new ActionContext(

View File

@ -184,6 +184,77 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act // Act
var result = await client.GetAsync("http://localhost/ProducesContentOnClass/ReturnClassNameContentTypeOnDerivedAction"); var result = await client.GetAsync("http://localhost/ProducesContentOnClass/ReturnClassNameContentTypeOnDerivedAction");
// Assert
Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
Assert.Equal(expectedBody, body);
}
[Fact]
public async Task ProducesContentAttribute_IsNotHonored_ForJsonResult()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.Handler;
var expectedContentType = "application/json;charset=utf-8";
var expectedBody = "{\"MethodName\":\"Produces_WithNonObjectResult\"}";
// Act
var result = await client.GetAsync("http://localhost/JsonResult/Produces_WithNonObjectResult");
// Assert
Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
Assert.Equal(expectedBody, body);
}
[Fact]
public async Task JsonResult_UsesDefaultContentTypes_IfNoneAreAddedExplicitly()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.Handler;
var expectedContentType = "application/json;charset=utf-8";
var expectedBody = "{\"MethodName\":\"ReturnJsonResult\"}";
// Act
var result = await client.GetAsync("http://localhost/JsonResult/ReturnJsonResult");
// Assert
Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
Assert.Equal(expectedBody, body);
}
[Fact]
public async Task JsonResult_UsesExplicitContentTypeAndFormatter_IfAdded()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.Handler;
var expectedContentType = "application/custom-json;charset=utf-8";
var expectedBody = "{ MethodName = ReturnJsonResult_WithCustomMediaType }";
// Act
var result = await client.GetAsync("http://localhost/JsonResult/ReturnJsonResult_WithCustomMediaType");
// Assert
Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
var body = await result.HttpContext.Response.ReadBodyAsStringAsync();
Assert.Equal(expectedBody, body);
}
[Fact]
public async Task JsonResult_UsesDefaultJsonFormatter_IfNoMatchingFormatterIsFound()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.Handler;
var expectedContentType = "application/json;charset=utf-8";
var expectedBody = "{\"MethodName\":\"ReturnJsonResult_WithCustomMediaType_NoFormatter\"}";
// Act
var result = await client.GetAsync("http://localhost/JsonResult/ReturnJsonResult_WithCustomMediaType_NoFormatter");
// Assert // Assert
Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType); Assert.Equal(expectedContentType, result.HttpContext.Response.ContentType);
var body = await result.HttpContext.Response.ReadBodyAsStringAsync(); var body = await result.HttpContext.Response.ReadBodyAsStringAsync();

View File

@ -0,0 +1,37 @@
// 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.
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace ConnegWebsite
{
public class JsonResultController : Controller
{
public IActionResult ReturnJsonResult()
{
return new JsonResult(new { MethodName = "ReturnJsonResult" });
}
public IActionResult ReturnJsonResult_WithCustomMediaType()
{
var jsonResult = new JsonResult(new { MethodName = "ReturnJsonResult_WithCustomMediaType" },
new CustomFormatter("application/custom-json"));
jsonResult.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/custom-json"));
return jsonResult;
}
public IActionResult ReturnJsonResult_WithCustomMediaType_NoFormatter()
{
var jsonResult = new JsonResult(new { MethodName = "ReturnJsonResult_WithCustomMediaType_NoFormatter" });
jsonResult.ContentTypes.Add(MediaTypeHeaderValue.Parse("application/custom-json"));
return jsonResult;
}
[Produces("application/xml")]
public IActionResult Produces_WithNonObjectResult()
{
return new JsonResult(new { MethodName = "Produces_WithNonObjectResult" });
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace ConnegWebsite namespace ConnegWebsite
{ {

View File

@ -39,7 +39,7 @@ namespace ConnegWebsite
{ {
var response = context.ActionContext.HttpContext.Response; var response = context.ActionContext.HttpContext.Response;
response.ContentType = ContentType + ";charset=utf-8"; response.ContentType = ContentType + ";charset=utf-8";
await response.WriteAsync(context.Object as string); await response.WriteAsync(context.Object.ToString());
} }
} }
} }