From 87ea03daf12df8f6f5e606a001af607f12e2da09 Mon Sep 17 00:00:00 2001
From: Martin Costello <martin@martincostello.com>
Date: Mon, 17 Jun 2019 17:33:45 +0100
Subject: [PATCH] Register SystemTextJsonResultExecutor (#11247)

Register SystemTextJsonResultExecutor as part of MVC core services so that JsonResult works without Newtonsoft.Json.
Addresses #11246.
---
 .../MvcCoreServiceCollectionExtensions.cs     |   1 +
 ...cs => JsonResultWithNewtonsoftJsonTest.cs} |  29 ++--
 .../JsonResultWithSystemTextJsonTest.cs       | 137 ++++++++++++++++++
 ...JsonResultWithNewtonsoftJsonController.cs} |   6 +-
 .../JsonResultWithSystemTextJsonController.cs |  45 ++++++
 .../BasicWebSite/StartupWithNewtonsoftJson.cs |  29 ++++
 .../BasicWebSite/StartupWithSystemTextJson.cs |  28 ++++
 7 files changed, 262 insertions(+), 13 deletions(-)
 rename src/Mvc/test/Mvc.FunctionalTests/{JsonResultTest.cs => JsonResultWithNewtonsoftJsonTest.cs} (77%)
 create mode 100644 src/Mvc/test/Mvc.FunctionalTests/JsonResultWithSystemTextJsonTest.cs
 rename src/Mvc/test/WebSites/BasicWebSite/Controllers/{JsonResultController.cs => JsonResultWithNewtonsoftJsonController.cs} (91%)
 create mode 100644 src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultWithSystemTextJsonController.cs
 create mode 100644 src/Mvc/test/WebSites/BasicWebSite/StartupWithNewtonsoftJson.cs
 create mode 100644 src/Mvc/test/WebSites/BasicWebSite/StartupWithSystemTextJson.cs

diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
index 380dafdc680..e34f74c2216 100644
--- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
+++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
@@ -258,6 +258,7 @@ namespace Microsoft.Extensions.DependencyInjection
             services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
             services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
             services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
+            services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
             services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
 
             //
diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonResultTest.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonResultWithNewtonsoftJsonTest.cs
similarity index 77%
rename from src/Mvc/test/Mvc.FunctionalTests/JsonResultTest.cs
rename to src/Mvc/test/Mvc.FunctionalTests/JsonResultWithNewtonsoftJsonTest.cs
index 43d442b9457..3f58fab658d 100644
--- a/src/Mvc/test/Mvc.FunctionalTests/JsonResultTest.cs
+++ b/src/Mvc/test/Mvc.FunctionalTests/JsonResultWithNewtonsoftJsonTest.cs
@@ -1,18 +1,27 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
+using System.Linq;
 using System.Net;
 using System.Net.Http;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc.FunctionalTests
 {
-    public class JsonResultTest : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting>>
+    public class JsonResultWithNewtonsoftJsonTest : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithNewtonsoftJson>>
     {
-        public JsonResultTest(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
+        private IServiceCollection _serviceCollection;
+
+        public JsonResultWithNewtonsoftJsonTest(MvcTestFixture<BasicWebSite.StartupWithNewtonsoftJson> fixture)
         {
-            Client = fixture.CreateDefaultClient();
+            var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(b => b.UseStartup<BasicWebSite.StartupWithNewtonsoftJson>());
+            factory = factory.WithWebHostBuilder(b => b.ConfigureTestServices(serviceCollection => _serviceCollection = serviceCollection));
+
+            Client = factory.CreateDefaultClient();
         }
 
         public HttpClient Client { get; }
@@ -21,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
         public async Task JsonResult_UsesDefaultContentType()
         {
             // Arrange
-            var url = "http://localhost/JsonResult/Plain";
+            var url = "http://localhost/JsonResultWithNewtonsoftJson/Plain";
             var request = new HttpRequestMessage(HttpMethod.Get, url);
 
             // Act
@@ -42,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
         public async Task JsonResult_Conneg_Fails(string mediaType)
         {
             // Arrange
-            var url = "http://localhost/JsonResult/Plain";
+            var url = "http://localhost/JsonResultWithNewtonsoftJson/Plain";
             var request = new HttpRequestMessage(HttpMethod.Get, url);
             request.Headers.TryAddWithoutValidation("Accept", mediaType);
 
@@ -61,7 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
         public async Task JsonResult_Null()
         {
             // Arrange
-            var url = "http://localhost/JsonResult/Null";
+            var url = "http://localhost/JsonResultWithNewtonsoftJson/Null";
             var request = new HttpRequestMessage(HttpMethod.Get, url);
 
             // Act
@@ -79,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
         public async Task JsonResult_String()
         {
             // Arrange
-            var url = "http://localhost/JsonResult/String";
+            var url = "http://localhost/JsonResultWithNewtonsoftJson/String";
             var request = new HttpRequestMessage(HttpMethod.Get, url);
 
             // Act
@@ -96,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
         public async Task JsonResult_Uses_CustomSerializerSettings()
         {
             // Arrange
-            var url = "http://localhost/JsonResult/CustomSerializerSettings";
+            var url = "http://localhost/JsonResultWithNewtonsoftJson/CustomSerializerSettings";
             var request = new HttpRequestMessage(HttpMethod.Get, url);
 
             // Act
@@ -112,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
         public async Task JsonResult_CustomContentType()
         {
             // Arrange
-            var url = "http://localhost/JsonResult/CustomContentType";
+            var url = "http://localhost/JsonResultWithNewtonsoftJson/CustomContentType";
             var request = new HttpRequestMessage(HttpMethod.Get, url);
 
             // Act
@@ -125,4 +134,4 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
             Assert.Equal("{\"message\":\"hello\"}", content);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonResultWithSystemTextJsonTest.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonResultWithSystemTextJsonTest.cs
new file mode 100644
index 00000000000..fa76456e027
--- /dev/null
+++ b/src/Mvc/test/Mvc.FunctionalTests/JsonResultWithSystemTextJsonTest.cs
@@ -0,0 +1,137 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.FunctionalTests
+{
+    public class JsonResultWithSystemTextJsonTest : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithSystemTextJson>>
+    {
+        private IServiceCollection _serviceCollection;
+
+        public JsonResultWithSystemTextJsonTest(MvcTestFixture<BasicWebSite.StartupWithSystemTextJson> fixture)
+        {
+            var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(b => b.UseStartup<BasicWebSite.StartupWithSystemTextJson>());
+            factory = factory.WithWebHostBuilder(b => b.ConfigureTestServices(serviceCollection => _serviceCollection = serviceCollection));
+
+            Client = factory.CreateDefaultClient();
+        }
+
+        public HttpClient Client { get; }
+
+        [Fact]
+        public async Task JsonResult_UsesDefaultContentType()
+        {
+            // Arrange
+            var url = "http://localhost/JsonResultWithSystemTextJson/Plain";
+            var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+            // Act
+            var response = await Client.SendAsync(request);
+            var content = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
+            Assert.Equal("{\"message\":\"hello\"}", content);
+        }
+
+        // Using an Accept header can't force Json to not be Json. If your accept header doesn't jive with the
+        // formatters/content-type configured on the result it will be ignored.
+        [Theory]
+        [InlineData("application/xml")]
+        [InlineData("text/xml")]
+        public async Task JsonResult_Conneg_Fails(string mediaType)
+        {
+            // Arrange
+            var url = "http://localhost/JsonResultWithSystemTextJson/Plain";
+            var request = new HttpRequestMessage(HttpMethod.Get, url);
+            request.Headers.TryAddWithoutValidation("Accept", mediaType);
+
+            // Act
+            var response = await Client.SendAsync(request);
+            var content = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
+            Assert.Equal("{\"message\":\"hello\"}", content);
+        }
+
+        // If the object is null, it will get formatted as JSON. NOT as a 204/NoContent
+        [Fact]
+        public async Task JsonResult_Null()
+        {
+            // Arrange
+            var url = "http://localhost/JsonResultWithSystemTextJson/Null";
+            var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+            // Act
+            var response = await Client.SendAsync(request);
+            var content = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
+            Assert.Equal("null", content);
+        }
+
+        // If the object is a string, it will get formatted as JSON. NOT as text/plain.
+        [Fact]
+        public async Task JsonResult_String()
+        {
+            // Arrange
+            var url = "http://localhost/JsonResultWithSystemTextJson/String";
+            var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+            // Act
+            var response = await Client.SendAsync(request);
+            var content = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
+            Assert.Equal("\"hello\"", content);
+        }
+
+        [Fact]
+        public async Task JsonResult_Uses_CustomSerializerSettings()
+        {
+            // Arrange
+            var url = "http://localhost/JsonResultWithSystemTextJson/CustomSerializerSettings";
+            var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+            // Act
+            var response = await Client.SendAsync(request);
+            var content = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("{\"Message\":\"hello\"}", content);
+        }
+
+        [Fact]
+        public async Task JsonResult_CustomContentType()
+        {
+            // Arrange
+            var url = "http://localhost/JsonResultWithSystemTextJson/CustomContentType";
+            var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+            // Act
+            var response = await Client.SendAsync(request);
+            var content = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("application/message+json", response.Content.Headers.ContentType.MediaType);
+            Assert.Equal("{\"message\":\"hello\"}", content);
+        }
+    }
+}
diff --git a/src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultController.cs b/src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultWithNewtonsoftJsonController.cs
similarity index 91%
rename from src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultController.cs
rename to src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultWithNewtonsoftJsonController.cs
index 0a077336298..47f2616b1cf 100644
--- a/src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultController.cs
+++ b/src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultWithNewtonsoftJsonController.cs
@@ -8,11 +8,11 @@ using Newtonsoft.Json.Serialization;
 
 namespace BasicWebSite.Controllers
 {
-    public class JsonResultController : Controller
+    public class JsonResultWithNewtonsoftJsonController : Controller
     {
         private static readonly JsonSerializerSettings _customSerializerSettings;
 
-        static JsonResultController()
+        static JsonResultWithNewtonsoftJsonController()
         {
             _customSerializerSettings = JsonSerializerSettingsProvider.CreateSerializerSettings();
             _customSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
@@ -45,4 +45,4 @@ namespace BasicWebSite.Controllers
             return new JsonResult("hello");
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultWithSystemTextJsonController.cs b/src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultWithSystemTextJsonController.cs
new file mode 100644
index 00000000000..28362c511d9
--- /dev/null
+++ b/src/Mvc/test/WebSites/BasicWebSite/Controllers/JsonResultWithSystemTextJsonController.cs
@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc;
+
+namespace BasicWebSite.Controllers
+{
+    public class JsonResultWithSystemTextJsonController : Controller
+    {
+        private static readonly JsonSerializerOptions _customSerializerSettings;
+
+        static JsonResultWithSystemTextJsonController()
+        {
+            _customSerializerSettings = new JsonSerializerOptions();
+        }
+
+        public JsonResult Plain()
+        {
+            return new JsonResult(new { Message = "hello" });
+        }
+
+        public JsonResult CustomContentType()
+        {
+            var result = new JsonResult(new { Message = "hello" });
+            result.ContentType = "application/message+json";
+            return result;
+        }
+
+        public JsonResult CustomSerializerSettings()
+        {
+            return new JsonResult(new { Message = "hello" }, _customSerializerSettings);
+        }
+
+        public JsonResult Null()
+        {
+            return new JsonResult(null);
+        }
+
+        public JsonResult String()
+        {
+            return new JsonResult("hello");
+        }
+    }
+}
diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithNewtonsoftJson.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithNewtonsoftJson.cs
new file mode 100644
index 00000000000..b4c63a52a71
--- /dev/null
+++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithNewtonsoftJson.cs
@@ -0,0 +1,29 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BasicWebSite
+{
+    public class StartupWithNewtonsoftJson
+    {
+        public void ConfigureServices(IServiceCollection services)
+        {
+            services
+                .AddMvc()
+                .SetCompatibilityVersion(CompatibilityVersion.Latest)
+                .AddNewtonsoftJson();
+        }
+
+        public void Configure(IApplicationBuilder app)
+        {
+            app.UseDeveloperExceptionPage();
+
+            app.UseRouting();
+
+            app.UseEndpoints((endpoints) => endpoints.MapDefaultControllerRoute());
+        }
+    }
+}
diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithSystemTextJson.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithSystemTextJson.cs
new file mode 100644
index 00000000000..5d5f9d8e91e
--- /dev/null
+++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithSystemTextJson.cs
@@ -0,0 +1,28 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BasicWebSite
+{
+    public class StartupWithSystemTextJson
+    {
+        public void ConfigureServices(IServiceCollection services)
+        {
+            services
+                .AddMvc()
+                .SetCompatibilityVersion(CompatibilityVersion.Latest);
+        }
+
+        public void Configure(IApplicationBuilder app)
+        {
+            app.UseDeveloperExceptionPage();
+
+            app.UseRouting();
+
+            app.UseEndpoints((endpoints) => endpoints.MapDefaultControllerRoute());
+        }
+    }
+}
-- 
GitLab