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.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
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 Encoding _encoding = Encodings.UTF8EncodingWithoutBOM;
private ObjectResult _objectResult;
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>
/// Gets or sets a value indicating whether to indent elements when writing data.
/// Creates an instance of <see cref="JsonResult"/> class.
/// </summary>
public bool Indent { get; set; }
public Encoding Encoding
/// <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)
{
get { return _encoding; }
_defaultFormatter = defaultFormatter;
_objectResult = new ObjectResult(data);
}
public object Value
{
get
{
return _objectResult.Value;
}
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
_encoding = value;
_objectResult.Value = value;
}
}
public object Data { get; private set; }
public override void ExecuteResult([NotNull] ActionContext context)
public IList<MediaTypeHeaderValue> ContentTypes
{
var response = context.HttpContext.Response;
var writeStream = response.Body;
if (response.ContentType == null)
get
{
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);
formatter.WriteObject(writer, Data);
DeclaredType = _objectResult.DeclaredType,
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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;

View File

@ -79,16 +79,7 @@ namespace Microsoft.AspNet.Mvc
var selectedEncoding = context.SelectedEncoding;
using (var writer = new StreamWriter(response.Body, selectedEncoding, 1024, leaveOpen: true))
{
using (var jsonWriter = CreateJsonWriter(writer))
{
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();
}
WriteObject(writer, context.Object);
}
return Task.FromResult(true);

View File

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

View File

@ -47,7 +47,8 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
/// <param name="writeStream">The stream on which the XmlWriter should operate on.</param>
/// <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);
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc
/// with default XmlWriterSettings.
/// </summary>
public XmlSerializerOutputFormatter()
:this(GetDefaultXmlWriterSettings())
: this(GetDefaultXmlWriterSettings())
{
}
@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
/// <param name="writerSettings">The settings to be used by the <see cref="XmlSerializer"/>.</param>
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.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<IModelValidatorProvider, DataAnnotationsModelValidatorProvider>();

View File

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

View File

@ -1,12 +1,14 @@
// 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 System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc
{
private static readonly byte[] _abcdUTF8Bytes
= 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]
public async Task ExecuteResult_GeneratesResultsWithoutBOMByDefault()
{
@ -28,10 +32,8 @@ namespace Microsoft.AspNet.Mvc
var response = new Mock<HttpResponse>();
response.SetupGet(r => r.Body)
.Returns(memoryStream);
var context = new Mock<HttpContext>();
context.SetupGet(c => c.Response)
.Returns(response.Object);
var actionContext = new ActionContext(context.Object,
var context = GetHttpContext(response.Object);
var actionContext = new ActionContext(context,
new RouteData(),
new ActionDescriptor());
var result = new JsonResult(new { foo = "abcd" });
@ -44,7 +46,7 @@ namespace Microsoft.AspNet.Mvc
}
[Fact]
public async Task ExecuteResult_UsesEncoderIfSpecified()
public async Task ExecuteResult_IfNoMatchFoundUsesPassedInFormatter()
{
// Arrange
var expected = Enumerable.Concat(Encoding.UTF8.GetPreamble(), _abcdUTF8Bytes);
@ -52,16 +54,16 @@ namespace Microsoft.AspNet.Mvc
var response = new Mock<HttpResponse>();
response.SetupGet(r => r.Body)
.Returns(memoryStream);
var context = new Mock<HttpContext>();
context.SetupGet(c => c.Response)
.Returns(response.Object);
var actionContext = new ActionContext(context.Object,
var context = GetHttpContext(response.Object, registerDefaultFormatter: false);
var actionContext = new ActionContext(context,
new RouteData(),
new ActionDescriptor());
var result = new JsonResult(new { foo = "abcd" })
{
Encoding = Encoding.UTF8
};
var testFormatter = new TestJsonFormatter()
{
Encoding = Encoding.UTF8
};
var result = new JsonResult(new { foo = "abcd" }, testFormatter);
// Act
await result.ExecuteResultAsync(actionContext);
@ -69,5 +71,61 @@ namespace Microsoft.AspNet.Mvc
// Assert
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 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.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider)))
.Returns(mockFormattersProvider.Object);
httpResponse.SetupGet(r => r.Body).Returns(new MemoryStream());
var actionContext = new ActionContext(

View File

@ -184,6 +184,77 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Act
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.Equal(expectedContentType, result.HttpContext.Response.ContentType);
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.
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace ConnegWebsite
{

View File

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