diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/JsonResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/JsonResult.cs index 06afd12f8f..1529af0b85 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/JsonResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/JsonResult.cs @@ -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 _defaultSupportedContentTypes = + new List() + { + 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; - } } /// - /// Gets or sets a value indicating whether to indent elements when writing data. + /// Creates an instance of class. /// - public bool Indent { get; set; } - - public Encoding Encoding + /// + /// If no matching formatter is found, + /// the response is written to using defaultFormatter. + /// + /// The default formatter must be able to handle either application/json + /// or text/json. + /// + 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 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() + .OutputFormatters; + + var formatter = _objectResult.SelectFormatter(formatterContext, defaultFormatters); + if (formatter == null) + { + formatter = _defaultFormatter ?? formatterContext.ActionContext + .HttpContext + .RequestServices + .GetService(); } + + return formatter; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs index 0d54fb1fce..88573bf49d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ObjectResult.cs @@ -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; diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs index df69d866dc..edd413d9ce 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonOutputFormatter.cs @@ -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); diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlDataContractSerializerOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlDataContractSerializerOutputFormatter.cs index efc07b7c2c..a2b53da271 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlDataContractSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlDataContractSerializerOutputFormatter.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc /// with default XmlWriterSettings /// public XmlDataContractSerializerOutputFormatter() - :this(GetDefaultXmlWriterSettings()) + : this(GetDefaultXmlWriterSettings()) { } @@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc /// /// The settings to be used by the . public XmlDataContractSerializerOutputFormatter([NotNull] XmlWriterSettings writerSettings) - :base(writerSettings) + : base(writerSettings) { } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlOutputFormatter.cs index 53dc95bfe5..cf9d1ccb35 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlOutputFormatter.cs @@ -47,7 +47,8 @@ namespace Microsoft.AspNet.Mvc /// /// The stream on which the XmlWriter should operate on. /// A new instance of - 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); } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlSerializerOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlSerializerOutputFormatter.cs index e74dc5db2d..ea1223f658 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/XmlSerializerOutputFormatter.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc /// with default XmlWriterSettings. /// public XmlSerializerOutputFormatter() - :this(GetDefaultXmlWriterSettings()) + : this(GetDefaultXmlWriterSettings()) { } @@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc /// /// The settings to be used by the . public XmlSerializerOutputFormatter([NotNull] XmlWriterSettings writerSettings) - :base(writerSettings) + : base(writerSettings) { } diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index a0b9485a79..3fe7dfae89 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -75,6 +75,9 @@ namespace Microsoft.AspNet.Mvc yield return describe.Scoped(); yield return describe.Transient(); + yield return describe.Instance( + new JsonOutputFormatter(JsonOutputFormatter.CreateDefaultSettings(), indent: false)); + yield return describe.Transient, DefaultFilterProvider>(); yield return describe.Transient(); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs index 3d9b797d29..bd39dfead3 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs @@ -517,7 +517,7 @@ namespace Microsoft.AspNet.Mvc.Test // Assert Assert.IsType(actualJsonResult); - Assert.Same(data, actualJsonResult.Data); + Assert.Same(data, actualJsonResult.Value); } public static IEnumerable RedirectTestData diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs index 0a5f754008..982b816a94 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/JsonResultTest.cs @@ -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(); response.SetupGet(r => r.Body) .Returns(memoryStream); - var context = new Mock(); - 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(); response.SetupGet(r => r.Body) .Returns(memoryStream); - var context = new Mock(); - 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(); + 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() { _jsonformatter } : + new List(); + var httpContext = new Mock(); + var mockFormattersProvider = new Mock(); + 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); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs index 4850b3fad1..441a8ff422 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs @@ -1295,7 +1295,20 @@ namespace Microsoft.AspNet.Mvc var httpContext = new Mock(MockBehavior.Loose); var httpResponse = new Mock(MockBehavior.Loose); + var mockFormattersProvider = new Mock(); + mockFormattersProvider.SetupGet(o => o.OutputFormatters) + .Returns( + new List() + { + 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( diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs index 14799760ef..0f70303954 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ConnegTests.cs @@ -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(); diff --git a/test/WebSites/ConnegWebSite/Controllers/JsonResultController.cs b/test/WebSites/ConnegWebSite/Controllers/JsonResultController.cs new file mode 100644 index 0000000000..bef9e38a78 --- /dev/null +++ b/test/WebSites/ConnegWebSite/Controllers/JsonResultController.cs @@ -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" }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ConnegWebSite/Controllers/NormalController.cs b/test/WebSites/ConnegWebSite/Controllers/NormalController.cs index 04e6b589dd..f8c689281d 100644 --- a/test/WebSites/ConnegWebSite/Controllers/NormalController.cs +++ b/test/WebSites/ConnegWebSite/Controllers/NormalController.cs @@ -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 { diff --git a/test/WebSites/ConnegWebSite/ContentType.cs b/test/WebSites/ConnegWebSite/CustomFormatter.cs similarity index 95% rename from test/WebSites/ConnegWebSite/ContentType.cs rename to test/WebSites/ConnegWebSite/CustomFormatter.cs index d152773bed..03e1593b5c 100644 --- a/test/WebSites/ConnegWebSite/ContentType.cs +++ b/test/WebSites/ConnegWebSite/CustomFormatter.cs @@ -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()); } } } \ No newline at end of file