Browse Source

Separate IResult based results from ActionResults (#33843)

* Separate IResult based results from ActionResults

Fixes https://github.com/dotnet/aspnetcore/issues/33729
Pranav K 4 years ago
parent
commit
b9efadc59d
100 changed files with 5734 additions and 1831 deletions
  1. 33 0
      AspNetCore.sln
  2. 1 0
      eng/ProjectReferences.props
  3. 1 0
      eng/SharedFramework.Local.props
  4. 2 0
      src/Framework/test/TestData.cs
  5. 68 0
      src/Http/Http.Results/src/AcceptedAtRouteResult.cs
  6. 74 0
      src/Http/Http.Results/src/AcceptedResult.cs
  7. 13 0
      src/Http/Http.Results/src/BadRequestObjectResult.cs
  8. 12 0
      src/Http/Http.Results/src/BadRequestResult.cs
  9. 120 0
      src/Http/Http.Results/src/ChallengeResult.cs
  10. 13 0
      src/Http/Http.Results/src/ConflictObjectResult.cs
  11. 12 0
      src/Http/Http.Results/src/ConflictResult.cs
  12. 75 0
      src/Http/Http.Results/src/ContentResult.cs
  13. 68 0
      src/Http/Http.Results/src/CreatedAtRouteResult.cs
  14. 57 0
      src/Http/Http.Results/src/CreatedResult.cs
  15. 74 0
      src/Http/Http.Results/src/FileContentResult.cs
  16. 93 0
      src/Http/Http.Results/src/FileResult.cs
  17. 110 0
      src/Http/Http.Results/src/FileStreamResult.cs
  18. 125 0
      src/Http/Http.Results/src/ForbidResult.cs
  19. 78 0
      src/Http/Http.Results/src/JsonResult.cs
  20. 111 0
      src/Http/Http.Results/src/LocalRedirectResult.cs
  21. 31 0
      src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj
  22. 12 0
      src/Http/Http.Results/src/NoContentResult.cs
  23. 13 0
      src/Http/Http.Results/src/NotFoundObjectResult.cs
  24. 12 0
      src/Http/Http.Results/src/NotFoundResult.cs
  25. 62 0
      src/Http/Http.Results/src/ObjectResult.cs
  26. 13 0
      src/Http/Http.Results/src/OkObjectResult.cs
  27. 12 0
      src/Http/Http.Results/src/OkResult.cs
  28. 123 0
      src/Http/Http.Results/src/PhysicalFileResult.cs
  29. 1 0
      src/Http/Http.Results/src/PublicAPI.Shipped.txt
  30. 104 0
      src/Http/Http.Results/src/PublicAPI.Unshipped.txt
  31. 107 0
      src/Http/Http.Results/src/RedirectResult.cs
  32. 194 0
      src/Http/Http.Results/src/RedirectToRouteResult.cs
  33. 1527 0
      src/Http/Http.Results/src/Results.cs
  34. 97 0
      src/Http/Http.Results/src/SignInResult.cs
  35. 127 0
      src/Http/Http.Results/src/SignOutResult.cs
  36. 51 0
      src/Http/Http.Results/src/StatusCodeResult.cs
  37. 12 0
      src/Http/Http.Results/src/UnauthorizedResult.cs
  38. 13 0
      src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs
  39. 12 0
      src/Http/Http.Results/src/UnprocessableEntityResult.cs
  40. 129 0
      src/Http/Http.Results/src/VirtualFileResult.cs
  41. 120 0
      src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs
  42. 61 0
      src/Http/Http.Results/test/AcceptedResultTests.cs
  43. 22 0
      src/Http/Http.Results/test/BadRequestObjectResultTests.cs
  44. 20 0
      src/Http/Http.Results/test/BadRequestResultTests.cs
  45. 62 0
      src/Http/Http.Results/test/ChallengeResultTest.cs
  46. 22 0
      src/Http/Http.Results/test/ConflictObjectResultTest.cs
  47. 20 0
      src/Http/Http.Results/test/ConflictResultTest.cs
  48. 152 0
      src/Http/Http.Results/test/ContentResultTest.cs
  49. 93 0
      src/Http/Http.Results/test/CreatedAtRouteResultTests.cs
  50. 79 0
      src/Http/Http.Results/test/CreatedResultTest.cs
  51. 38 0
      src/Http/Http.Results/test/FileContentResultTest.cs
  52. 86 0
      src/Http/Http.Results/test/FileStreamResultTest.cs
  53. 129 0
      src/Http/Http.Results/test/ForbidResultTest.cs
  54. 138 0
      src/Http/Http.Results/test/LocalRedirectResultTest.cs
  55. 15 0
      src/Http/Http.Results/test/Microsoft.AspNetCore.Http.Results.Tests.csproj
  56. 67 0
      src/Http/Http.Results/test/NotFoundObjectResultTest.cs
  57. 20 0
      src/Http/Http.Results/test/NotFoundResultTests.cs
  58. 65 0
      src/Http/Http.Results/test/ObjectResultTests.cs
  59. 46 0
      src/Http/Http.Results/test/OkObjectResultTest.cs
  60. 37 0
      src/Http/Http.Results/test/OkResultTest.cs
  61. 41 0
      src/Http/Http.Results/test/PhysicalFileResultTest.cs
  62. 6 39
      src/Http/Http.Results/test/RedirectResultTest.cs
  63. 111 0
      src/Http/Http.Results/test/RedirectToRouteResultTest.cs
  64. 95 0
      src/Http/Http.Results/test/SignInResultTest.cs
  65. 95 0
      src/Http/Http.Results/test/SignOutResultTest.cs
  66. 45 0
      src/Http/Http.Results/test/StatusCodeResultTests.cs
  67. 29 0
      src/Http/Http.Results/test/TestLinkGenerator.cs
  68. 20 0
      src/Http/Http.Results/test/UnauthorizedResultTests.cs
  69. 22 0
      src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs
  70. 20 0
      src/Http/Http.Results/test/UnprocessableEntityResultTests.cs
  71. 25 0
      src/Http/Http.Results/test/VirtualFileResultTest.cs
  72. 2 0
      src/Http/HttpAbstractions.slnf
  73. 13 13
      src/Middleware/StaticFiles/src/LoggerExtensions.cs
  74. 3 13
      src/Mvc/Mvc.Core/src/ChallengeResult.cs
  75. 1 47
      src/Mvc/Mvc.Core/src/ContentResult.cs
  76. 1 42
      src/Mvc/Mvc.Core/src/FileContentResult.cs
  77. 3 13
      src/Mvc/Mvc.Core/src/ForbidResult.cs
  78. 4 1
      src/Mvc/Mvc.Core/src/Infrastructure/ContentResultExecutor.cs
  79. 10 363
      src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs
  80. 3 1
      src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs
  81. 1 12
      src/Mvc/Mvc.Core/src/JsonResult.cs
  82. 2 0
      src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj
  83. 1 1
      src/Mvc/Mvc.Core/src/MvcCoreLoggerExtensions.cs
  84. 1 51
      src/Mvc/Mvc.Core/src/PhysicalFileResult.cs
  85. 1 32
      src/Mvc/Mvc.Core/src/RedirectResult.cs
  86. 1 1
      src/Mvc/Mvc.Core/src/Resources.resx
  87. 2 14
      src/Mvc/Mvc.Core/src/SignInResult.cs
  88. 1 38
      src/Mvc/Mvc.Core/src/VirtualFileResult.cs
  89. 2 2
      src/Mvc/Mvc.Core/test/AcceptedResultTests.cs
  90. 0 37
      src/Mvc/Mvc.Core/test/ChallengeResultTest.cs
  91. 0 77
      src/Mvc/Mvc.Core/test/ContentResultTest.cs
  92. 1 2
      src/Mvc/Mvc.Core/test/ControllerBaseTest.cs
  93. 0 155
      src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs
  94. 0 101
      src/Mvc/Mvc.Core/test/FileContentResult.cs
  95. 44 0
      src/Mvc/Mvc.Core/test/FileContentResultTest.cs
  96. 8 9
      src/Mvc/Mvc.Core/test/FileResultHelperTest.cs
  97. 29 560
      src/Mvc/Mvc.Core/test/FileStreamResultTest.cs
  98. 6 3
      src/Mvc/Mvc.Core/test/Formatters/ResponseContentTypeHelperTest.cs
  99. 1 0
      src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
  100. 0 204
      src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs

+ 33 - 0
AspNetCore.sln

@@ -1626,6 +1626,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Compon
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleWebSiteWithWebApplicationBuilderException", "src\Mvc\test\WebSites\SimpleWebSiteWithWebApplicationBuilderException\SimpleWebSiteWithWebApplicationBuilderException.csproj", "{5C641396-7E92-4F5C-A5A1-B4CDF480539B}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.Results", "Http.Results", "{323C3EB6-1D15-4B3D-918D-699D7F64DED9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Results", "src\Http\Http.Results\src\Microsoft.AspNetCore.Http.Results.csproj", "{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Results.Tests", "src\Http\Http.Results\test\Microsoft.AspNetCore.Http.Results.Tests.csproj", "{F599EAA6-399F-4A91-9B1F-D311305B43D9}"
+EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logging.W3C.Sample", "src\Middleware\HttpLogging\samples\Logging.W3C.Sample\Logging.W3C.Sample.csproj", "{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}"
 EndProject
 Global
@@ -7747,6 +7753,30 @@ Global
 		{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x64.Build.0 = Release|Any CPU
 		{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x86.ActiveCfg = Release|Any CPU
 		{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x86.Build.0 = Release|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Debug|x64.Build.0 = Debug|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Debug|x86.Build.0 = Debug|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Release|Any CPU.Build.0 = Release|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Release|x64.ActiveCfg = Release|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Release|x64.Build.0 = Release|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Release|x86.ActiveCfg = Release|Any CPU
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8}.Release|x86.Build.0 = Release|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Debug|x64.Build.0 = Debug|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Debug|x86.Build.0 = Debug|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Release|x64.ActiveCfg = Release|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Release|x64.Build.0 = Release|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Release|x86.ActiveCfg = Release|Any CPU
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9}.Release|x86.Build.0 = Release|Any CPU
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -8564,6 +8594,9 @@ Global
 		{558C46DE-DE16-41D5-8DB7-D6D748E32977} = {3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56}
 		{B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD} = {44963D50-8B58-44E6-918D-788BCB406695}
 		{5C641396-7E92-4F5C-A5A1-B4CDF480539B} = {088C37A5-30D2-40FB-B031-D163CFBED006}
+		{323C3EB6-1D15-4B3D-918D-699D7F64DED9} = {627BE8B3-59E6-4F1D-8C9C-76B804D41724}
+		{092EA9F6-84D4-41EF-A618-BDA50A0E10A8} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9}
+		{F599EAA6-399F-4A91-9B1F-D311305B43D9} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9}
 		{17459B97-1AA3-4154-83D3-C6BDC9FA3F85} = {022B4B80-E813-4256-8034-11A68146F4EF}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution

+ 1 - 0
eng/ProjectReferences.props

@@ -29,6 +29,7 @@
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Abstractions" ProjectPath="$(RepoRoot)src\Http\Http.Abstractions\src\Microsoft.AspNetCore.Http.Abstractions.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Extensions" ProjectPath="$(RepoRoot)src\Http\Http.Extensions\src\Microsoft.AspNetCore.Http.Extensions.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Features" ProjectPath="$(RepoRoot)src\Http\Http.Features\src\Microsoft.AspNetCore.Http.Features.csproj" />
+    <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http.Results" ProjectPath="$(RepoRoot)src\Http\Http.Results\src\Microsoft.AspNetCore.Http.Results.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Http" ProjectPath="$(RepoRoot)src\Http\Http\src\Microsoft.AspNetCore.Http.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Metadata" ProjectPath="$(RepoRoot)src\Http\Metadata\src\Microsoft.AspNetCore.Metadata.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Owin" ProjectPath="$(RepoRoot)src\Http\Owin\src\Microsoft.AspNetCore.Owin.csproj" />

+ 1 - 0
eng/SharedFramework.Local.props

@@ -48,6 +48,7 @@
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Http.Abstractions" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Http.Extensions" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Http.Features" />
+    <AspNetCoreAppReference Include="Microsoft.AspNetCore.Http.Results" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Http" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Routing.Abstractions" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Routing" />

+ 2 - 0
src/Framework/test/TestData.cs

@@ -55,6 +55,7 @@ namespace Microsoft.AspNetCore
                 "Microsoft.AspNetCore.Http.Connections.Common",
                 "Microsoft.AspNetCore.Http.Extensions",
                 "Microsoft.AspNetCore.Http.Features",
+                "Microsoft.AspNetCore.Http.Results",
                 "Microsoft.AspNetCore.HttpLogging",
                 "Microsoft.AspNetCore.HttpOverrides",
                 "Microsoft.AspNetCore.HttpsPolicy",
@@ -188,6 +189,7 @@ namespace Microsoft.AspNetCore
                 { "Microsoft.AspNetCore.Http.Connections.Common", "6.0.0.0" },
                 { "Microsoft.AspNetCore.Http.Extensions", "6.0.0.0" },
                 { "Microsoft.AspNetCore.Http.Features", "6.0.0.0" },
+                { "Microsoft.AspNetCore.Http.Results", "6.0.0.0" },
                 { "Microsoft.AspNetCore.HttpLogging", "6.0.0.0" },
                 { "Microsoft.AspNetCore.HttpOverrides", "6.0.0.0" },
                 { "Microsoft.AspNetCore.HttpsPolicy", "6.0.0.0" },

+ 68 - 0
src/Http/Http.Results/src/AcceptedAtRouteResult.cs

@@ -0,0 +1,68 @@
+// 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;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class AcceptedAtRouteResult : ObjectResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AcceptedAtRouteResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public AcceptedAtRouteResult(object? routeValues, object? value)
+            : this(routeName: null, routeValues: routeValues, value: value)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AcceptedAtRouteResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route to use for generating the URL.</param>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public AcceptedAtRouteResult(
+            string? routeName,
+            object? routeValues,
+            object? value)
+            : base(value, StatusCodes.Status202Accepted)
+        {
+            RouteName = routeName;
+            RouteValues = new RouteValueDictionary(routeValues);
+        }
+
+        /// <summary>
+        /// Gets the name of the route to use for generating the URL.
+        /// </summary>
+        public string? RouteName { get; }
+
+        /// <summary>
+        /// Gets the route data to use for generating the URL.
+        /// </summary>
+        public RouteValueDictionary RouteValues { get; }
+
+        /// <inheritdoc />
+        protected override void OnFormatting(HttpContext context)
+        {
+            var linkGenerator = context.RequestServices.GetRequiredService<LinkGenerator>();
+            var url = linkGenerator.GetUriByAddress(
+                context,
+                RouteName,
+                RouteValues,
+                fragment: FragmentString.Empty);
+
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new InvalidOperationException("No route matches the supplied values.");
+            }
+
+            context.Response.Headers.Location = url;
+        }
+    }
+}

+ 74 - 0
src/Http/Http.Results/src/AcceptedResult.cs

@@ -0,0 +1,74 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class AcceptedResult : ObjectResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AcceptedResult"/> class with the values
+        /// provided.
+        /// </summary>
+        public AcceptedResult()
+            : base(value: null, StatusCodes.Status202Accepted)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AcceptedResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="location">The location at which the status of requested content can be monitored.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public AcceptedResult(string? location, object? value)
+            : base(value, StatusCodes.Status202Accepted)
+        {
+            Location = location;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AcceptedResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="locationUri">The location at which the status of requested content can be monitored.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public AcceptedResult(Uri locationUri, object? value)
+            : base(value, StatusCodes.Status202Accepted)
+        {
+            if (locationUri == null)
+            {
+                throw new ArgumentNullException(nameof(locationUri));
+            }
+
+            if (locationUri.IsAbsoluteUri)
+            {
+                Location = locationUri.AbsoluteUri;
+            }
+            else
+            {
+                Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the location at which the status of the requested content can be monitored.
+        /// </summary>
+        public string? Location { get; set; }
+
+        /// <inheritdoc />
+        protected override void OnFormatting(HttpContext context)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            if (!string.IsNullOrEmpty(Location))
+            {
+                context.Response.Headers.Location = Location;
+            }
+        }
+    }
+}

+ 13 - 0
src/Http/Http.Results/src/BadRequestObjectResult.cs

@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class BadRequestObjectResult : ObjectResult
+    {
+        public BadRequestObjectResult(object? error)
+            : base(error, StatusCodes.Status400BadRequest)
+        {
+        }
+    }
+}

+ 12 - 0
src/Http/Http.Results/src/BadRequestResult.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class BadRequestResult : StatusCodeResult
+    {
+        public BadRequestResult() : base(StatusCodes.Status400BadRequest)
+        {
+        }
+    }
+}

+ 120 - 0
src/Http/Http.Results/src/ChallengeResult.cs

@@ -0,0 +1,120 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// An <see cref="IResult"/> that on execution invokes <see cref="M:HttpContext.ChallengeAsync"/>.
+    /// </summary>
+    internal sealed partial class ChallengeResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of <see cref="ChallengeResult"/>.
+        /// </summary>
+        public ChallengeResult()
+            : this(Array.Empty<string>())
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ChallengeResult"/> with the
+        /// specified authentication scheme.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication scheme to challenge.</param>
+        public ChallengeResult(string authenticationScheme)
+            : this(new[] { authenticationScheme })
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ChallengeResult"/> with the
+        /// specified authentication schemes.
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication schemes to challenge.</param>
+        public ChallengeResult(IList<string> authenticationSchemes)
+            : this(authenticationSchemes, properties: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ChallengeResult"/> with the
+        /// specified <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        public ChallengeResult(AuthenticationProperties? properties)
+            : this(Array.Empty<string>(), properties)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ChallengeResult"/> with the
+        /// specified authentication scheme and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication schemes to challenge.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        public ChallengeResult(string authenticationScheme, AuthenticationProperties? properties)
+            : this(new[] { authenticationScheme }, properties)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ChallengeResult"/> with the
+        /// specified authentication schemes and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication scheme to challenge.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        public ChallengeResult(IList<string> authenticationSchemes, AuthenticationProperties? properties)
+        {
+            AuthenticationSchemes = authenticationSchemes;
+            Properties = properties;
+        }
+
+        public IList<string> AuthenticationSchemes { get; init; } = Array.Empty<string>();
+
+        public AuthenticationProperties? Properties { get; init; }
+
+        public async Task ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<ChallengeResult>>();
+
+            Log.ChallengeResultExecuting(logger, AuthenticationSchemes);
+
+            if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0)
+            {
+                foreach (var scheme in AuthenticationSchemes)
+                {
+                    await httpContext.ChallengeAsync(scheme, Properties);
+                }
+            }
+            else
+            {
+                await httpContext.ChallengeAsync(Properties);
+            }
+        }
+
+        private static partial class Log
+        {
+            public static void ChallengeResultExecuting(ILogger logger, IList<string> authenticationSchemes)
+            {
+                if (logger.IsEnabled(LogLevel.Information))
+                {
+                    ChallengeResultExecuting(logger, authenticationSchemes.ToArray());
+                }
+            }
+
+            [LoggerMessage(1, LogLevel.Information, "Executing ChallengeResult with authentication schemes ({Schemes}).", EventName = "ChallengeResultExecuting", SkipEnabledCheck = true)]
+            private static partial void ChallengeResultExecuting(ILogger logger, string[] schemes);
+        }
+    }
+}

+ 13 - 0
src/Http/Http.Results/src/ConflictObjectResult.cs

@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class ConflictObjectResult : ObjectResult
+    {
+        public ConflictObjectResult(object? error) :
+            base(error, StatusCodes.Status409Conflict)
+        {
+        }
+    }
+}

+ 12 - 0
src/Http/Http.Results/src/ConflictResult.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal class ConflictResult : StatusCodeResult
+    {
+        public ConflictResult() : base(StatusCodes.Status409Conflict)
+        {
+        }
+    }
+}

+ 75 - 0
src/Http/Http.Results/src/ContentResult.cs

@@ -0,0 +1,75 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed partial class ContentResult : IResult
+    {
+        private const string DefaultContentType = "text/plain; charset=utf-8";
+        private static readonly Encoding DefaultEncoding = Encoding.UTF8;
+
+        /// <summary>
+        /// Gets or set the content representing the body of the response.
+        /// </summary>
+        public string? Content { get; init; }
+
+        /// <summary>
+        /// Gets or sets the Content-Type header for the response.
+        /// </summary>
+        public string? ContentType { get; init; }
+
+        /// <summary>
+        /// Gets or sets the HTTP status code.
+        /// </summary>
+        public int? StatusCode { get; init; }
+
+        /// <summary>
+        /// Writes the content to the HTTP response.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
+        /// <returns>A task that represents the asynchronous execute operation.</returns>
+        public async Task ExecuteAsync(HttpContext httpContext)
+        {
+            var response = httpContext.Response;
+
+            ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
+                ContentType,
+                response.ContentType,
+                (DefaultContentType, DefaultEncoding),
+                ResponseContentTypeHelper.GetEncoding,
+                out var resolvedContentType,
+                out var resolvedContentTypeEncoding);
+
+            response.ContentType = resolvedContentType;
+
+            if (StatusCode != null)
+            {
+                response.StatusCode = StatusCode.Value;
+            }
+
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<ContentResult>>();
+
+            Log.ContentResultExecuting(logger, resolvedContentType);
+
+            if (Content != null)
+            {
+                response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content);
+                await response.WriteAsync(Content, resolvedContentTypeEncoding);
+            }
+        }
+
+        private static partial class Log
+        {
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing ContentResult with HTTP Response ContentType of {ContentType}",
+                EventName = "ContentResultExecuting")]
+            internal static partial void ContentResultExecuting(ILogger logger, string contentType);
+        }
+    }
+}

+ 68 - 0
src/Http/Http.Results/src/CreatedAtRouteResult.cs

@@ -0,0 +1,68 @@
+// 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;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class CreatedAtRouteResult : ObjectResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CreatedAtRouteResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public CreatedAtRouteResult(object? routeValues, object? value)
+            : this(routeName: null, routeValues: routeValues, value: value)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CreatedAtRouteResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route to use for generating the URL.</param>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public CreatedAtRouteResult(
+            string? routeName,
+            object? routeValues,
+            object? value)
+            : base(value, StatusCodes.Status201Created)
+        {
+            RouteName = routeName;
+            RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues);
+        }
+
+        /// <summary>
+        /// Gets or sets the name of the route to use for generating the URL.
+        /// </summary>
+        public string? RouteName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the route data to use for generating the URL.
+        /// </summary>
+        public RouteValueDictionary? RouteValues { get; set; }
+
+        /// <inheritdoc />
+        protected override void OnFormatting(HttpContext context)
+        {
+            var linkGenerator = context.RequestServices.GetRequiredService<LinkGenerator>();
+            var url = linkGenerator.GetUriByRouteValues(
+                context,
+                RouteName,
+                RouteValues,
+                fragment: FragmentString.Empty);
+
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new InvalidOperationException("No route matches the supplied values.");
+            }
+
+            context.Response.Headers.Location = url;
+        }
+    }
+}

+ 57 - 0
src/Http/Http.Results/src/CreatedResult.cs

@@ -0,0 +1,57 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class CreatedResult : ObjectResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CreatedResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="location">The location at which the content has been created.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public CreatedResult(string location, object? value)
+            : base(value, StatusCodes.Status201Created)
+        {
+            Location = location;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CreatedResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="location">The location at which the content has been created.</param>
+        /// <param name="value">The value to format in the entity body.</param>
+        public CreatedResult(Uri location, object? value)
+            : base(value, StatusCodes.Status201Created)
+        {
+            if (location == null)
+            {
+                throw new ArgumentNullException(nameof(location));
+            }
+
+            if (location.IsAbsoluteUri)
+            {
+                Location = location.AbsoluteUri;
+            }
+            else
+            {
+                Location = location.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the location at which the content has been created.
+        /// </summary>
+        public string Location { get; init; }
+
+        /// <inheritdoc />
+        protected override void OnFormatting(HttpContext context)
+        {
+            context.Response.Headers.Location = Location;
+        }
+    }
+}

+ 74 - 0
src/Http/Http.Results/src/FileContentResult.cs

@@ -0,0 +1,74 @@
+// 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.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed partial class FileContentResult : FileResult, IResult
+    {
+        /// <summary>
+        /// Creates a new <see cref="FileContentResult"/> instance with
+        /// the provided <paramref name="fileContents"/> and the
+        /// provided <paramref name="contentType"/>.
+        /// </summary>
+        /// <param name="fileContents">The bytes that represent the file contents.</param>
+        /// <param name="contentType">The Content-Type header of the response.</param>
+        public FileContentResult(byte[] fileContents, string contentType)
+            : base(contentType)
+        {
+            FileContents = fileContents;
+        }
+
+        /// <summary>
+        /// Gets or sets the file contents.
+        /// </summary>
+        public byte[] FileContents { get; init; }
+
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<FileContentResult>>();
+            Log.ExecutingFileResult(logger, this);
+
+            var fileResultInfo = new FileResultInfo
+            {
+                ContentType = ContentType,
+                EnableRangeProcessing = EnableRangeProcessing,
+                EntityTag = EntityTag,
+                FileDownloadName = FileDownloadName,
+                LastModified = LastModified,
+            };
+
+            var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog(
+                httpContext,
+                fileResultInfo,
+                FileContents.Length,
+                EnableRangeProcessing,
+                LastModified,
+                EntityTag,
+                logger);
+
+            if (!serveBody)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (range != null && rangeLength == 0)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (range != null)
+            {
+                FileResultHelper.Log.WritingRangeToBody(logger);
+            }
+
+            var fileContentStream = new MemoryStream(FileContents);
+            return FileResultHelper.WriteFileAsync(httpContext, fileContentStream, range, rangeLength);
+        }
+    }
+}

+ 93 - 0
src/Http/Http.Results/src/FileResult.cs

@@ -0,0 +1,93 @@
+// 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;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal abstract partial class FileResult
+    {
+        private string? _fileDownloadName;
+
+        /// <summary>
+        /// Creates a new <see cref="FileResult"/> instance with
+        /// the provided <paramref name="contentType"/>.
+        /// </summary>
+        /// <param name="contentType">The Content-Type header of the response.</param>
+        protected FileResult(string contentType)
+        {
+            if (contentType == null)
+            {
+                throw new ArgumentNullException(nameof(contentType));
+            }
+
+            ContentType = contentType;
+        }
+
+        /// <summary>
+        /// Gets the Content-Type header for the response.
+        /// </summary>
+        public string ContentType { get; }
+
+        /// <summary>
+        /// Gets the file name that will be used in the Content-Disposition header of the response.
+        /// </summary>
+        [AllowNull]
+        public string FileDownloadName
+        {
+            get { return _fileDownloadName ?? string.Empty; }
+            init { _fileDownloadName = value; }
+        }
+
+        /// <summary>
+        /// Gets or sets the last modified information associated with the <see cref="FileResult"/>.
+        /// </summary>
+        public DateTimeOffset? LastModified { get; init; }
+
+        /// <summary>
+        /// Gets or sets the etag associated with the <see cref="FileResult"/>.
+        /// </summary>
+        public EntityTagHeaderValue? EntityTag { get; init; }
+
+        /// <summary>
+        /// Gets or sets the value that enables range processing for the <see cref="FileResult"/>.
+        /// </summary>
+        public bool EnableRangeProcessing { get; init; }
+
+        protected static partial class Log
+        {
+            public static void ExecutingFileResult(ILogger logger, FileResult fileResult)
+            {
+                if (logger.IsEnabled(LogLevel.Information))
+                {
+                    var fileResultType = fileResult.GetType().Name;
+                    ExecutingFileResultWithNoFileName(logger, fileResultType, fileResult.FileDownloadName);
+                }
+            }
+
+            public static void ExecutingFileResult(ILogger logger, FileResult fileResult, string fileName)
+            {
+                if (logger.IsEnabled(LogLevel.Information))
+                {
+                    var fileResultType = fileResult.GetType().Name;
+                    ExecutingFileResult(logger, fileResultType, fileName, fileResult.FileDownloadName);
+                }
+            }
+
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing {FileResultType}, sending file with download name '{FileDownloadName}'.",
+                EventName = "ExecutingFileResultWithNoFileName",
+                SkipEnabledCheck = true)]
+            private static partial void ExecutingFileResultWithNoFileName(ILogger logger, string fileResultType, string fileDownloadName);
+
+            [LoggerMessage(2, LogLevel.Information,
+                "Executing {FileResultType}, sending file '{FileDownloadPath}' with download name '{FileDownloadName}'.",
+                EventName = "ExecutingFileResult",
+                SkipEnabledCheck = true)]
+            private static partial void ExecutingFileResult(ILogger logger, string fileResultType, string fileDownloadPath, string fileDownloadName);
+        }
+    }
+}

+ 110 - 0
src/Http/Http.Results/src/FileStreamResult.cs

@@ -0,0 +1,110 @@
+// 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;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// Represents an <see cref="FileResult"/> that when executed will
+    /// write a file from a stream to the response.
+    /// </summary>
+    internal sealed class FileStreamResult : FileResult, IResult
+    {
+        /// <summary>
+        /// Creates a new <see cref="FileStreamResult"/> instance with
+        /// the provided <paramref name="fileStream"/> and the
+        /// provided <paramref name="contentType"/>.
+        /// </summary>
+        /// <param name="fileStream">The stream with the file.</param>
+        /// <param name="contentType">The Content-Type header of the response.</param>
+        public FileStreamResult(Stream fileStream, string contentType)
+            : this(fileStream, MediaTypeHeaderValue.Parse(contentType))
+        {
+            if (fileStream == null)
+            {
+                throw new ArgumentNullException(nameof(fileStream));
+            }
+
+            FileStream = fileStream;
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="FileStreamResult"/> instance with
+        /// the provided <paramref name="fileStream"/> and the
+        /// provided <paramref name="contentType"/>.
+        /// </summary>
+        /// <param name="fileStream">The stream with the file.</param>
+        /// <param name="contentType">The Content-Type header of the response.</param>
+        public FileStreamResult(Stream fileStream, MediaTypeHeaderValue contentType)
+            : base(contentType.ToString())
+        {
+            if (fileStream == null)
+            {
+                throw new ArgumentNullException(nameof(fileStream));
+            }
+
+            FileStream = fileStream;
+        }
+
+        /// <summary>
+        /// Gets or sets the stream with the file that will be sent back as the response.
+        /// </summary>
+        public Stream FileStream { get; }
+
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<FileStreamResult>>();
+            using (FileStream)
+            {
+                Log.ExecutingFileResult(logger, this);
+
+                long? fileLength = null;
+                if (FileStream.CanSeek)
+                {
+                    fileLength = FileStream.Length;
+                }
+
+                var fileResultInfo = new FileResultInfo
+                {
+                    ContentType = ContentType,
+                    EnableRangeProcessing = EnableRangeProcessing,
+                    EntityTag = EntityTag,
+                    FileDownloadName = FileDownloadName,
+                };
+
+                var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog(
+                    httpContext,
+                    fileResultInfo,
+                    fileLength,
+                    EnableRangeProcessing,
+                    LastModified,
+                    EntityTag,
+                    logger);
+
+                if (!serveBody)
+                {
+                    return Task.CompletedTask;
+                }
+
+                if (range != null && rangeLength == 0)
+                {
+                    return Task.CompletedTask;
+                }
+
+                if (range != null)
+                {
+                    FileResultHelper.Log.WritingRangeToBody(logger);
+                }
+
+                return FileResultHelper.WriteFileAsync(httpContext, FileStream, range, rangeLength);
+            }
+        }
+    }
+}

+ 125 - 0
src/Http/Http.Results/src/ForbidResult.cs

@@ -0,0 +1,125 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed partial class ForbidResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of <see cref="ForbidResult"/>.
+        /// </summary>
+        public ForbidResult()
+            : this(Array.Empty<string>())
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ForbidResult"/> with the
+        /// specified authentication scheme.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication scheme to challenge.</param>
+        public ForbidResult(string authenticationScheme)
+            : this(new[] { authenticationScheme })
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ForbidResult"/> with the
+        /// specified authentication schemes.
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication schemes to challenge.</param>
+        public ForbidResult(IList<string> authenticationSchemes)
+            : this(authenticationSchemes, properties: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ForbidResult"/> with the
+        /// specified <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        public ForbidResult(AuthenticationProperties? properties)
+            : this(Array.Empty<string>(), properties)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ForbidResult"/> with the
+        /// specified authentication scheme and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication schemes to challenge.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        public ForbidResult(string authenticationScheme, AuthenticationProperties? properties)
+            : this(new[] { authenticationScheme }, properties)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="ForbidResult"/> with the
+        /// specified authentication schemes and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication scheme to challenge.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        public ForbidResult(IList<string> authenticationSchemes, AuthenticationProperties? properties)
+        {
+            AuthenticationSchemes = authenticationSchemes;
+            Properties = properties;
+        }
+
+        /// <summary>
+        /// Gets or sets the authentication schemes that are challenged.
+        /// </summary>
+        public IList<string> AuthenticationSchemes { get; init; }
+
+        /// <summary>
+        /// Gets or sets the <see cref="AuthenticationProperties"/> used to perform the authentication challenge.
+        /// </summary>
+        public AuthenticationProperties? Properties { get; init; }
+
+        /// <inheritdoc />
+        public async Task ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<ForbidResult>>();
+
+            Log.ForbidResultExecuting(logger, AuthenticationSchemes);
+
+            if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0)
+            {
+                for (var i = 0; i < AuthenticationSchemes.Count; i++)
+                {
+                    await httpContext.ForbidAsync(AuthenticationSchemes[i], Properties);
+                }
+            }
+            else
+            {
+                await httpContext.ForbidAsync(Properties);
+            }
+        }
+
+        private static partial class Log
+        {
+            public static void ForbidResultExecuting(ILogger logger, IList<string> authenticationSchemes)
+            {
+                if (logger.IsEnabled(LogLevel.Information))
+                {
+                    ForbidResultExecuting(logger, authenticationSchemes.ToArray());
+                }
+            }
+
+            [LoggerMessage(1, LogLevel.Information, "Executing ChallengeResult with authentication schemes ({Schemes}).", EventName = "ChallengeResultExecuting", SkipEnabledCheck = true)]
+            private static partial void ForbidResultExecuting(ILogger logger, string[] schemes);
+        }
+
+    }
+}

+ 78 - 0
src/Http/Http.Results/src/JsonResult.cs

@@ -0,0 +1,78 @@
+// 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 System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// An action result which formats the given object as JSON.
+    /// </summary>
+    internal sealed partial class JsonResult : IResult
+    {
+        /// <summary>
+        /// Gets or sets the <see cref="Net.Http.Headers.MediaTypeHeaderValue"/> representing the Content-Type header of the response.
+        /// </summary>
+        public string? ContentType { get; init; }
+
+        /// <summary>
+        /// Gets or sets the serializer settings.
+        /// <para>
+        /// When using <c>System.Text.Json</c>, this should be an instance of <see cref="JsonSerializerOptions" />
+        /// </para>
+        /// <para>
+        /// When using <c>Newtonsoft.Json</c>, this should be an instance of <c>JsonSerializerSettings</c>.
+        /// </para>
+        /// </summary>
+        public JsonSerializerOptions? JsonSerializerOptions { get; init; }
+
+        /// <summary>
+        /// Gets or sets the HTTP status code.
+        /// </summary>
+        public int? StatusCode { get; init; }
+
+        /// <summary>
+        /// Gets or sets the value to be formatted.
+        /// </summary>
+        public object? Value { get; init; }
+
+        /// <summary>
+        /// Write the result as JSON to the HTTP response.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
+        /// <returns>A task that represents the asynchronous execute operation.</returns>
+        Task IResult.ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<JsonResult>>();
+            Log.JsonResultExecuting(logger, Value);
+
+            if (StatusCode is int statusCode)
+            {
+                httpContext.Response.StatusCode = statusCode;
+            }
+
+            return httpContext.Response.WriteAsJsonAsync(Value, JsonSerializerOptions, ContentType);
+        }
+
+        private static partial class Log
+        {
+            public static void JsonResultExecuting(ILogger logger, object? value)
+            {
+                if (logger.IsEnabled(LogLevel.Information))
+                {
+                    var type = value == null ? "null" : value.GetType().FullName!;
+                    JsonResultExecuting(logger, type);
+                }
+            }
+
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing JsonResult, writing value of type '{Type}'.",
+                EventName = "JsonResultExecuting",
+                SkipEnabledCheck = true)]
+            private static partial void JsonResultExecuting(ILogger logger, string type);
+        }
+    }
+}

+ 111 - 0
src/Http/Http.Results/src/LocalRedirectResult.cs

@@ -0,0 +1,111 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// An <see cref="IResult"/> that returns a Found (302), Moved Permanently (301), Temporary Redirect (307),
+    /// or Permanent Redirect (308) response with a Location header to the supplied local URL.
+    /// </summary>
+    internal sealed partial class LocalRedirectResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalRedirectResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="localUrl">The local URL to redirect to.</param>
+        public LocalRedirectResult(string localUrl)
+             : this(localUrl, permanent: false)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalRedirectResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="localUrl">The local URL to redirect to.</param>
+        /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
+        public LocalRedirectResult(string localUrl, bool permanent)
+            : this(localUrl, permanent, preserveMethod: false)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LocalRedirectResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="localUrl">The local URL to redirect to.</param>
+        /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
+        /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request's method.</param>
+        public LocalRedirectResult(string localUrl, bool permanent, bool preserveMethod)
+        {
+            if (string.IsNullOrEmpty(localUrl))
+            {
+                throw new ArgumentException("Argument cannot be null or empty", nameof(localUrl));
+            }
+
+            Permanent = permanent;
+            PreserveMethod = preserveMethod;
+            Url = localUrl;
+        }
+
+        /// <summary>
+        /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false.
+        /// </summary>
+        public bool Permanent { get; }
+
+        /// <summary>
+        /// Gets or sets an indication that the redirect preserves the initial request method.
+        /// </summary>
+        public bool PreserveMethod { get; }
+
+        /// <summary>
+        /// Gets or sets the local URL to redirect to.
+        /// </summary>
+        public string Url { get; }
+
+        /// <inheritdoc />
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            if (!SharedUrlHelper.IsLocalUrl(Url))
+            {
+                throw new InvalidOperationException("The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local.");
+            }
+
+            var destinationUrl = SharedUrlHelper.Content(httpContext, Url);
+
+            // IsLocalUrl is called to handle URLs starting with '~/'.
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<LocalRedirectResult>>();
+
+            Log.LocalRedirectResultExecuting(logger, destinationUrl);
+
+            if (PreserveMethod)
+            {
+                httpContext.Response.StatusCode = Permanent
+                    ? StatusCodes.Status308PermanentRedirect
+                    : StatusCodes.Status307TemporaryRedirect;
+                httpContext.Response.Headers.Location = destinationUrl;
+            }
+            else
+            {
+                httpContext.Response.Redirect(destinationUrl, Permanent);
+            }
+
+            return Task.CompletedTask;
+        }
+
+        private static partial class Log
+        {
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing LocalRedirectResult, redirecting to {Destination}.",
+                EventName = "LocalRedirectResultExecuting")]
+            public static partial void LocalRedirectResultExecuting(ILogger logger, string destination);
+        }
+    }
+}

+ 31 - 0
src/Http/Http.Results/src/Microsoft.AspNetCore.Http.Results.csproj

@@ -0,0 +1,31 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <Description>ASP.NET Core types that implement Microsoft.AspNetCore.Http.IResult.</Description>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <IsAspNetCoreApp>true</IsAspNetCoreApp>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <PackageTags>aspnetcore</PackageTags>
+    <IsPackable>false</IsPackable>
+    <Nullable>enable</Nullable>
+    <RootNamespace>Microsoft.AspNetCore.Http.Result</RootNamespace>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" />
+    <Compile Include="$(SharedSourceRoot)ResponseContentTypeHelper.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)ResultsHelpers\*.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)RangeHelper\RangeHelper.cs" LinkBase="Shared" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Authentication.Abstractions" />
+    <Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
+    <Reference Include="Microsoft.AspNetCore.Http.Extensions" />
+    <Reference Include="Microsoft.AspNetCore.Routing" />
+    <Reference Include="Microsoft.Extensions.Logging.Abstractions" />
+
+    <InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Results.Tests" />
+  </ItemGroup>
+
+</Project>

+ 12 - 0
src/Http/Http.Results/src/NoContentResult.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal class NoContentResult : StatusCodeResult
+    {
+        public NoContentResult() : base(StatusCodes.Status204NoContent)
+        {
+        }
+    }
+}

+ 13 - 0
src/Http/Http.Results/src/NotFoundObjectResult.cs

@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class NotFoundObjectResult : ObjectResult
+    {
+        public NotFoundObjectResult(object? value)
+            : base(value, StatusCodes.Status404NotFound)
+        {
+        }
+    }
+}

+ 12 - 0
src/Http/Http.Results/src/NotFoundResult.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class NotFoundResult : StatusCodeResult
+    {
+        public NotFoundResult() : base(StatusCodes.Status404NotFound)
+        {
+        }
+    }
+}

+ 62 - 0
src/Http/Http.Results/src/ObjectResult.cs

@@ -0,0 +1,62 @@
+// 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.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal partial class ObjectResult : IResult
+    {
+        /// <summary>
+        /// Creates a new <see cref="ObjectResult"/> instance with the provided <paramref name="value"/>.
+        /// </summary>
+        public ObjectResult(object? value, int statusCode)
+        {
+            Value = value;
+            StatusCode = statusCode;
+        }
+
+        /// <summary>
+        /// The object result.
+        /// </summary>
+        public object? Value { get; }
+
+        /// <summary>
+        /// Gets or sets the HTTP status code.
+        /// </summary>
+        public int StatusCode { get; }
+
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
+            var logger = loggerFactory.CreateLogger(GetType());
+            Log.ObjectResultExecuting(logger, Value);
+
+            httpContext.Response.StatusCode = StatusCode;
+
+            OnFormatting(httpContext);
+            return httpContext.Response.WriteAsJsonAsync(Value);
+        }
+
+        protected virtual void OnFormatting(HttpContext httpContext)
+        {
+        }
+
+        private static partial class Log
+        {
+            public static void ObjectResultExecuting(ILogger logger, object? value)
+            {
+                if (logger.IsEnabled(LogLevel.Information))
+                {
+                    var valueType = value is null ? "null" : value.GetType().FullName!;
+                    ObjectResultExecuting(logger, valueType);
+                }
+            }
+
+            [LoggerMessage(1, LogLevel.Information, "Writing value of type '{Type}'.", EventName = "ObjectResultExecuting", SkipEnabledCheck = true)]
+            public static partial void ObjectResultExecuting(ILogger logger, string type);
+        }
+    }
+}

+ 13 - 0
src/Http/Http.Results/src/OkObjectResult.cs

@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class OkObjectResult : ObjectResult
+    {
+        public OkObjectResult(object? value)
+            : base(value, StatusCodes.Status200OK)
+        {
+        }
+    }
+}

+ 12 - 0
src/Http/Http.Results/src/OkResult.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal class OkResult : StatusCodeResult
+    {
+        public OkResult() : base(StatusCodes.Status200OK)
+        {
+        }
+    }
+}

+ 123 - 0
src/Http/Http.Results/src/PhysicalFileResult.cs

@@ -0,0 +1,123 @@
+// 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;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// A <see cref="PhysicalFileResult"/> on execution will write a file from disk to the response
+    /// using mechanisms provided by the host.
+    /// </summary>
+    internal sealed partial class PhysicalFileResult : FileResult, IResult
+    {
+        /// <summary>
+        /// Creates a new <see cref="PhysicalFileResult"/> instance with
+        /// the provided <paramref name="fileName"/> and the provided <paramref name="contentType"/>.
+        /// </summary>
+        /// <param name="fileName">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type header of the response.</param>
+        public PhysicalFileResult(string fileName, string contentType)
+            : base(contentType)
+        {
+            FileName = fileName;
+        }
+
+        /// <summary>
+        /// Gets or sets the path to the file that will be sent back as the response.
+        /// </summary>
+        public string FileName { get; }
+
+        // For testing
+        public Func<string, FileInfoWrapper> GetFileInfoWrapper { get; init; } =
+            static path => new FileInfoWrapper(path);
+
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var fileInfo = GetFileInfoWrapper(FileName);
+            if (!fileInfo.Exists)
+            {
+                throw new FileNotFoundException($"Could not find file: {FileName}", FileName);
+            }
+
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<PhysicalFileResult>>();
+
+            Log.ExecutingFileResult(logger, this, FileName);
+
+            var lastModified = LastModified ?? fileInfo.LastWriteTimeUtc;
+            var fileResultInfo = new FileResultInfo
+            {
+                ContentType = ContentType,
+                EnableRangeProcessing = EnableRangeProcessing,
+                EntityTag = EntityTag,
+                FileDownloadName = FileDownloadName,
+                LastModified = lastModified,
+            };
+
+            var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog(
+                httpContext,
+                fileResultInfo,
+                fileInfo.Length,
+                EnableRangeProcessing,
+                lastModified,
+                EntityTag,
+                logger);
+
+            if (!serveBody)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (range != null && rangeLength == 0)
+            {
+                return Task.CompletedTask;
+            }
+
+            var response = httpContext.Response;
+            if (!Path.IsPathRooted(FileName))
+            {
+                throw new NotSupportedException($"Path '{FileName}' was not rooted.");
+            }
+
+            if (range != null)
+            {
+                FileResultHelper.Log.WritingRangeToBody(logger);
+            }
+
+            var offset = 0L;
+            var count = (long?)null;
+            if (range != null)
+            {
+                offset = range.From ?? 0L;
+                count = rangeLength;
+            }
+
+            return response.SendFileAsync(
+                FileName,
+                offset: offset,
+                count: count);
+        }
+
+        internal readonly struct FileInfoWrapper
+        {
+            public FileInfoWrapper(string path)
+            {
+                var fileInfo = new FileInfo(path);
+                Exists = fileInfo.Exists;
+                Length = fileInfo.Length;
+                LastWriteTimeUtc = fileInfo.LastWriteTimeUtc;
+            }
+
+            public bool Exists { get; init; }
+
+            public long Length { get; init; }
+
+            public DateTimeOffset LastWriteTimeUtc { get; init; }
+        }
+    }
+}

+ 1 - 0
src/Http/Http.Results/src/PublicAPI.Shipped.txt

@@ -0,0 +1 @@
+#nullable enable

+ 104 - 0
src/Http/Http.Results/src/PublicAPI.Unshipped.txt

@@ -0,0 +1,104 @@
+#nullable enable
+Microsoft.AspNetCore.Http.Results
+static Microsoft.AspNetCore.Http.Results.Accepted() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Accepted(System.Uri! uri) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Accepted(System.Uri! uri, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Accepted(object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Accepted(string? uri) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Accepted(string? uri, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.AcceptedAtRoute(object? routeValues) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.AcceptedAtRoute(object? routeValues, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.AcceptedAtRoute(string? routeName) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.AcceptedAtRoute(string? routeName, object? routeValues) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.AcceptedAtRoute(string? routeName, object? routeValues, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.BadRequest() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.BadRequest(object? error) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Challenge() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Challenge(Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Challenge(Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties, params string![]! authenticationSchemes) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Challenge(params string![]! authenticationSchemes) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Conflict() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Conflict(object? error) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Content(string! content) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Content(string! content, Microsoft.Net.Http.Headers.MediaTypeHeaderValue? contentType) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Content(string! content, string! contentType) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Content(string! content, string! contentType, System.Text.Encoding! contentEncoding) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Created(System.Uri! uri, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Created(string! uri, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.CreatedAtRoute(object? routeValues, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.CreatedAtRoute(string? routeName, object? routeValues, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.CreatedAtRoute(string? routeName, object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType, string? fileDownloadName) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(System.IO.Stream! fileStream, string! contentType, string? fileDownloadName, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType, string? fileDownloadName) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(byte[]! fileContents, string! contentType, string? fileDownloadName, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType, string? fileDownloadName) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.File(string! virtualPath, string! contentType, string? fileDownloadName, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Forbid() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Forbid(Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Forbid(Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties, params string![]! authenticationSchemes) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Forbid(params string![]! authenticationSchemes) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Json(object? data, System.Text.Json.JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.LocalRedirect(string! localUrl) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.LocalRedirectPermanent(string! localUrl) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.LocalRedirectPermanentPreserveMethod(string! localUrl) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.LocalRedirectPreserveMethod(string! localUrl) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.NoContent() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.NotFound() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.NotFound(object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Ok() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Ok(object? value) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType, string? fileDownloadName) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType, string? fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.PhysicalFile(string! physicalPath, string! contentType, string? fileDownloadName, bool enableRangeProcessing) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Redirect(string! url) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectPermanent(string! url) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectPermanentPreserveMethod(string! url) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectPreserveMethod(string! url) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoute(object? routeValues) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoute(string? routeName) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoute(string? routeName, object? routeValues) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoute(string? routeName, object? routeValues, string? fragment) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoute(string? routeName, string? fragment) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoutePermanent(object? routeValues) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoutePermanent(string? routeName) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoutePermanent(string? routeName, object? routeValues) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoutePermanent(string? routeName, object? routeValues, string? fragment) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoutePermanent(string? routeName, string? fragment) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoutePermanentPreserveMethod(string? routeName = null, object? routeValues = null, string? fragment = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.RedirectToRoutePreserveMethod(string? routeName = null, object? routeValues = null, string? fragment = null) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignIn(System.Security.Claims.ClaimsPrincipal! principal) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignIn(System.Security.Claims.ClaimsPrincipal! principal, Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignIn(System.Security.Claims.ClaimsPrincipal! principal, Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties, string! authenticationScheme) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignIn(System.Security.Claims.ClaimsPrincipal! principal, string! authenticationScheme) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignOut() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignOut(Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignOut(Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties, params string![]! authenticationSchemes) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.SignOut(params string![]! authenticationSchemes) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.StatusCode(int statusCode) -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.Unauthorized() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.UnprocessableEntity() -> Microsoft.AspNetCore.Http.IResult!
+static Microsoft.AspNetCore.Http.Results.UnprocessableEntity(object? error) -> Microsoft.AspNetCore.Http.IResult!

+ 107 - 0
src/Http/Http.Results/src/RedirectResult.cs

@@ -0,0 +1,107 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed partial class RedirectResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="url">The local URL to redirect to.</param>
+        public RedirectResult(string url)
+            : this(url, permanent: false)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="url">The URL to redirect to.</param>
+        /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
+        public RedirectResult(string url, bool permanent)
+            : this(url, permanent, preserveMethod: false)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectResult"/> class with the values
+        /// provided.
+        /// </summary>
+        /// <param name="url">The URL to redirect to.</param>
+        /// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
+        /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
+        public RedirectResult(string url, bool permanent, bool preserveMethod)
+        {
+            if (url == null)
+            {
+                throw new ArgumentNullException(nameof(url));
+            }
+
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new ArgumentException("Argument cannot be null or empty", nameof(url));
+            }
+
+            Permanent = permanent;
+            PreserveMethod = preserveMethod;
+            Url = url;
+        }
+
+        /// <summary>
+        /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false.
+        /// </summary>
+        public bool Permanent { get; }
+
+        /// <summary>
+        /// Gets or sets an indication that the redirect preserves the initial request method.
+        /// </summary>
+        public bool PreserveMethod { get; }
+
+        /// <summary>
+        /// Gets or sets the URL to redirect to.
+        /// </summary>
+        public string Url { get; }
+
+        /// <inheritdoc />
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<RedirectResult>>();
+
+            // IsLocalUrl is called to handle URLs starting with '~/'.
+            var destinationUrl = SharedUrlHelper.IsLocalUrl(Url) ? SharedUrlHelper.Content(httpContext, Url) : Url;
+
+            Log.RedirectResultExecuting(logger, destinationUrl);
+
+            if (PreserveMethod)
+            {
+                httpContext.Response.StatusCode = Permanent
+                    ? StatusCodes.Status308PermanentRedirect
+                    : StatusCodes.Status307TemporaryRedirect;
+                httpContext.Response.Headers.Location = destinationUrl;
+            }
+            else
+            {
+                httpContext.Response.Redirect(destinationUrl, Permanent);
+            }
+
+            return Task.CompletedTask;
+        }
+
+        private static partial class Log
+        {
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing RedirectResult, redirecting to {Destination}.",
+                EventName = "RedirectResultExecuting")]
+            public static partial void RedirectResultExecuting(ILogger logger, string destination);
+        }
+    }
+}

+ 194 - 0
src/Http/Http.Results/src/RedirectToRouteResult.cs

@@ -0,0 +1,194 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// An <see cref="IResult"/> that returns a Found (302), Moved Permanently (301), Temporary Redirect (307),
+    /// or Permanent Redirect (308) response with a Location header.
+    /// Targets a registered route.
+    /// </summary>
+    internal sealed partial class RedirectToRouteResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectToRouteResult"/> with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeValues">The parameters for the route.</param>
+        public RedirectToRouteResult(object? routeValues)
+            : this(routeName: null, routeValues: routeValues)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectToRouteResult"/> with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for the route.</param>
+        public RedirectToRouteResult(
+            string? routeName,
+            object? routeValues)
+            : this(routeName, routeValues, permanent: false)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectToRouteResult"/> with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for the route.</param>
+        /// <param name="permanent">If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302).</param>
+        public RedirectToRouteResult(
+            string? routeName,
+            object? routeValues,
+            bool permanent)
+            : this(routeName, routeValues, permanent, fragment: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectToRouteResult"/> with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for the route.</param>
+        /// <param name="permanent">If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302).</param>
+        /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
+        public RedirectToRouteResult(
+            string? routeName,
+            object? routeValues,
+            bool permanent,
+            bool preserveMethod)
+            : this(routeName, routeValues, permanent, preserveMethod, fragment: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectToRouteResult"/> with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for the route.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        public RedirectToRouteResult(
+            string? routeName,
+            object? routeValues,
+            string? fragment)
+            : this(routeName, routeValues, permanent: false, fragment: fragment)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectToRouteResult"/> with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for the route.</param>
+        /// <param name="permanent">If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302).</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        public RedirectToRouteResult(
+            string? routeName,
+            object? routeValues,
+            bool permanent,
+            string? fragment)
+            : this(routeName, routeValues, permanent, preserveMethod: false, fragment: fragment)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RedirectToRouteResult"/> with the values
+        /// provided.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for the route.</param>
+        /// <param name="permanent">If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302).</param>
+        /// <param name="preserveMethod">If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        public RedirectToRouteResult(
+            string? routeName,
+            object? routeValues,
+            bool permanent,
+            bool preserveMethod,
+            string? fragment)
+        {
+            RouteName = routeName;
+            RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues);
+            PreserveMethod = preserveMethod;
+            Permanent = permanent;
+            Fragment = fragment;
+        }
+
+        /// <summary>
+        /// Gets or sets the name of the route to use for generating the URL.
+        /// </summary>
+        public string? RouteName { get; }
+
+        /// <summary>
+        /// Gets or sets the route data to use for generating the URL.
+        /// </summary>
+        public RouteValueDictionary? RouteValues { get; }
+
+        /// <summary>
+        /// Gets or sets an indication that the redirect is permanent.
+        /// </summary>
+        public bool Permanent { get; }
+
+        /// <summary>
+        /// Gets or sets an indication that the redirect preserves the initial request method.
+        /// </summary>
+        public bool PreserveMethod { get; }
+
+        /// <summary>
+        /// Gets or sets the fragment to add to the URL.
+        /// </summary>
+        public string? Fragment { get; }
+
+        /// <inheritdoc />
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var linkGenerator = httpContext.RequestServices.GetRequiredService<LinkGenerator>();
+
+            var destinationUrl = linkGenerator.GetUriByRouteValues(
+                httpContext,
+                RouteName,
+                RouteValues,
+                fragment: Fragment == null ? FragmentString.Empty : new FragmentString("#" + Fragment));
+            if (string.IsNullOrEmpty(destinationUrl))
+            {
+                throw new InvalidOperationException("No route matches the supplied values.");
+            }
+
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<RedirectToRouteResult>>();
+            Log.RedirectToRouteResultExecuting(logger, destinationUrl, RouteName);
+
+            if (PreserveMethod)
+            {
+                httpContext.Response.StatusCode = Permanent ?
+                    StatusCodes.Status308PermanentRedirect : StatusCodes.Status307TemporaryRedirect;
+                httpContext.Response.Headers.Location = destinationUrl;
+            }
+            else
+            {
+                httpContext.Response.Redirect(destinationUrl, Permanent);
+            }
+
+            return Task.CompletedTask;
+        }
+
+        private static partial class Log
+        {
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing RedirectToRouteResult, redirecting to {Destination} from route {RouteName}.",
+                EventName = "RedirectToRouteResultExecuting")]
+            public static partial void RedirectToRouteResultExecuting(ILogger logger, string destination, string? routeName);
+        }
+    }
+}

+ 1527 - 0
src/Http/Http.Results/src/Results.cs

@@ -0,0 +1,1527 @@
+// 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;
+using System.IO;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Json;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http.Result;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http
+{
+    /// <summary>
+    /// A factory for <see cref="IResult"/>.
+    /// </summary>
+    public static class Results
+    {
+        #region SignIn / SignOut / Challenge
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext)"/>.
+        /// <para>
+        /// The behavior of this method depends on the <see cref="IAuthenticationService"/> in use.
+        /// <see cref="StatusCodes.Status401Unauthorized"/> and <see cref="StatusCodes.Status403Forbidden"/>
+        /// are among likely status results.
+        /// </para>
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Challenge()
+            => new ChallengeResult();
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext, string?)"/>.
+        /// <para>
+        /// The behavior of this method depends on the <see cref="IAuthenticationService"/> in use.
+        /// <see cref="StatusCodes.Status401Unauthorized"/> and <see cref="StatusCodes.Status403Forbidden"/>
+        /// are among likely status results.
+        /// </para>
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication schemes to challenge.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Challenge(params string[] authenticationSchemes)
+            => new ChallengeResult { AuthenticationSchemes = authenticationSchemes };
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext, AuthenticationProperties?)" />.
+        /// <para>
+        /// The behavior of this method depends on the <see cref="IAuthenticationService"/> in use.
+        /// <see cref="StatusCodes.Status401Unauthorized"/> and <see cref="StatusCodes.Status403Forbidden"/>
+        /// are among likely status results.
+        /// </para>
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Challenge(AuthenticationProperties properties)
+            => new ChallengeResult { Properties = properties };
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext, string?, AuthenticationProperties?)" />.
+        /// <para>
+        /// The behavior of this method depends on the <see cref="IAuthenticationService"/> in use.
+        /// <see cref="StatusCodes.Status401Unauthorized"/> and <see cref="StatusCodes.Status403Forbidden"/>
+        /// are among likely status results.
+        /// </para>
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        /// <param name="authenticationSchemes">The authentication schemes to challenge.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Challenge(
+            AuthenticationProperties properties,
+            params string[] authenticationSchemes)
+            => new ChallengeResult { AuthenticationSchemes = authenticationSchemes, Properties = properties };
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignInAsync(HttpContext, ClaimsPrincipal)"/>.
+        /// </summary>
+        /// <param name="principal">The <see cref="ClaimsPrincipal"/> containing the user claims.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignIn(ClaimsPrincipal principal)
+            => new SignInResult(principal);
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignInAsync(HttpContext, string?, ClaimsPrincipal)" />.
+        /// </summary>
+        /// <param name="principal">The <see cref="ClaimsPrincipal"/> containing the user claims.</param>
+        /// <param name="authenticationScheme">The authentication scheme to use for the sign-in operation.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignIn(ClaimsPrincipal principal, string authenticationScheme)
+            => new SignInResult(authenticationScheme, principal);
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignInAsync(HttpContext, ClaimsPrincipal, AuthenticationProperties?)" />.
+        /// </summary>
+        /// <param name="principal">The <see cref="ClaimsPrincipal"/> containing the user claims.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-in operation.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignIn(
+            ClaimsPrincipal principal,
+            AuthenticationProperties properties)
+            => new SignInResult(principal, properties);
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignInAsync(HttpContext, string?, ClaimsPrincipal, AuthenticationProperties?)" />.
+        /// </summary>
+        /// <param name="principal">The <see cref="ClaimsPrincipal"/> containing the user claims.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-in operation.</param>
+        /// <param name="authenticationScheme">The authentication scheme to use for the sign-in operation.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignIn(
+            ClaimsPrincipal principal,
+            AuthenticationProperties properties,
+            string authenticationScheme)
+            => new SignInResult(authenticationScheme, principal, properties);
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignOutAsync(HttpContext)" />.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignOut()
+            => new SignOutResult();
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignOutAsync(HttpContext, AuthenticationProperties?)" />.
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-out operation.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignOut(AuthenticationProperties properties)
+            => new SignOutResult(properties);
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignOutAsync(HttpContext, string?)" />.
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication schemes to use for the sign-out operation.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignOut(params string[] authenticationSchemes)
+            => new SignOutResult(authenticationSchemes);
+
+        /// <summary>
+        /// Creates an <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.SignOutAsync(HttpContext, string?, AuthenticationProperties?)" />.
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-out operation.</param>
+        /// <param name="authenticationSchemes">The authentication scheme to use for the sign-out operation.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult SignOut(AuthenticationProperties properties, params string[] authenticationSchemes)
+            => new SignOutResult(authenticationSchemes, properties);
+        #endregion
+
+        #region ContentResult
+        /// <summary>
+        /// Writes the <paramref name="content"/> string to the HTTP response.
+        /// </summary>
+        /// <param name="content">The content to write to the response.</param>
+        /// <returns>The created <see cref="IResult"/> object for the response.</returns>
+        public static IResult Content(string content)
+            => Content(content, (MediaTypeHeaderValue?)null);
+
+        /// <summary>
+        /// Writes the <paramref name="content"/> string to the HTTP response.
+        /// </summary>
+        /// <param name="content">The content to write to the response.</param>
+        /// <param name="contentType">The content type (MIME type).</param>
+        /// <returns>The created <see cref="IResult"/> object for the response.</returns>
+        public static IResult Content(string content, string contentType)
+            => Content(content, MediaTypeHeaderValue.Parse(contentType));
+
+        /// <summary>
+        /// Writes the <paramref name="content"/> string to the HTTP response.
+        /// </summary>
+        /// <param name="content">The content to write to the response.</param>
+        /// <param name="contentType">The content type (MIME type).</param>
+        /// <param name="contentEncoding">The content encoding.</param>
+        /// <returns>The created <see cref="IResult"/> object for the response.</returns>
+        /// <remarks>
+        /// If encoding is provided by both the 'charset' and the <paramref name="contentEncoding"/> parameters, then
+        /// the <paramref name="contentEncoding"/> parameter is chosen as the final encoding.
+        /// </remarks>
+        public static IResult Content(string content, string contentType, Encoding contentEncoding)
+        {
+            var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType);
+            mediaTypeHeaderValue.Encoding = contentEncoding ?? mediaTypeHeaderValue.Encoding;
+            return Content(content, mediaTypeHeaderValue);
+        }
+
+        /// <summary>
+        /// Writes the <paramref name="content"/> string to the HTTP response.
+        /// </summary>
+        /// <param name="content">The content to write to the response.</param>
+        /// <param name="contentType">The content type (MIME type).</param>
+        /// <returns>The created <see cref="IResult"/> object for the response.</returns>
+        public static IResult Content(string content, MediaTypeHeaderValue? contentType)
+        {
+            return new ContentResult
+            {
+                Content = content,
+                ContentType = contentType?.ToString()
+            };
+        }
+        #endregion
+
+        #region ForbidResult
+        /// <summary>
+        /// Creates a <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ForbidAsync(HttpContext)"/>.
+        /// <para>
+        /// By default, executing this result returns a <see cref="StatusCodes.Status403Forbidden"/>. Some authentication schemes, such as cookies,
+        /// will convert <see cref="StatusCodes.Status403Forbidden"/> to a redirect to show a login page.
+        /// </para>
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Forbid()
+            => new ForbidResult();
+
+        /// <summary>
+        /// Creates a <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ForbidAsync(HttpContext, string?)"/>.
+        /// <para>
+        /// By default, executing this result returns a <see cref="StatusCodes.Status403Forbidden"/>. Some authentication schemes, such as cookies,
+        /// will convert <see cref="StatusCodes.Status403Forbidden"/> to a redirect to show a login page.
+        /// </para>
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication schemes to challenge.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// Some authentication schemes, such as cookies, will convert <see cref="StatusCodes.Status403Forbidden"/> to
+        /// a redirect to show a login page.
+        /// </remarks>
+        public static IResult Forbid(params string[] authenticationSchemes)
+            => new ForbidResult { AuthenticationSchemes = authenticationSchemes };
+
+        /// <summary>
+        /// Creates a <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ForbidAsync(HttpContext, AuthenticationProperties?)"/>.
+        /// <para>
+        /// By default, executing this result returns a <see cref="StatusCodes.Status403Forbidden"/>. Some authentication schemes, such as cookies,
+        /// will convert <see cref="StatusCodes.Status403Forbidden"/> to a redirect to show a login page.
+        /// </para>
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// Some authentication schemes, such as cookies, will convert <see cref="StatusCodes.Status403Forbidden"/> to
+        /// a redirect to show a login page.
+        /// </remarks>
+        public static IResult Forbid(AuthenticationProperties properties)
+            => new ForbidResult { Properties = properties };
+
+        /// <summary>
+        /// Creates a <see cref="IResult"/> that on execution invokes <see cref="AuthenticationHttpContextExtensions.ForbidAsync(HttpContext, string?, AuthenticationProperties?)"/>.
+        /// <para>
+        /// By default, executing this result returns a <see cref="StatusCodes.Status403Forbidden"/>. Some authentication schemes, such as cookies,
+        /// will convert <see cref="StatusCodes.Status403Forbidden"/> to a redirect to show a login page.
+        /// </para>
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
+        /// challenge.</param>
+        /// <param name="authenticationSchemes">The authentication schemes to challenge.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// Some authentication schemes, such as cookies, will convert <see cref="StatusCodes.Status403Forbidden"/> to
+        /// a redirect to show a login page.
+        /// </remarks>
+        public static IResult Forbid(AuthenticationProperties properties, params string[] authenticationSchemes)
+            => new ForbidResult { Properties = properties, AuthenticationSchemes = authenticationSchemes, };
+        #endregion
+
+        /// <summary>
+        /// Creates a <see cref="IResult"/> that serializes the specified <paramref name="data"/> object to JSON.
+        /// </summary>
+        /// <param name="data">The object to write as JSON.</param>
+        /// <param name="options">The serializer options use when serializing the value.</param>
+        /// <param name="contentType">The content-type to set on the response.</param>
+        /// <param name="statusCode">The status code to set on the response.</param>
+        /// <returns>The created <see cref="JsonResult"/> that serializes the specified <paramref name="data"/>
+        /// as JSON format for the response.</returns>
+        /// <remarks>Callers should cache an instance of serializer settings to avoid
+        /// recreating cached data with each call.</remarks>
+        public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null)
+        {
+            return new JsonResult
+            {
+                Value = data,
+                JsonSerializerOptions = options,
+                ContentType = contentType,
+                StatusCode = statusCode,
+            };
+        }
+
+        #region FileContentResult
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
+        /// and the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType)
+            => File(fileContents, contentType, fileDownloadName: null);
+
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
+        /// and the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType, bool enableRangeProcessing)
+            => File(fileContents, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing);
+
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType, string? fileDownloadName)
+            => new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName };
+
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType, string? fileDownloadName, bool enableRangeProcessing)
+            => new FileContentResult(fileContents, contentType)
+            {
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
+        /// and the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new FileContentResult(fileContents, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+            };
+        }
+
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
+        /// and the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new FileContentResult(fileContents, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new FileContentResult(fileContents, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+            };
+        }
+
+        /// <summary>
+        /// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileContents">The file contents.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(byte[] fileContents, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new FileContentResult(fileContents, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+        #endregion
+
+        #region FileStreamResult
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), with the
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType)
+            => File(fileStream, contentType, fileDownloadName: null);
+
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), with the
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType, bool enableRangeProcessing)
+            => File(fileStream, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing);
+
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type and the
+        /// specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType, string? fileDownloadName)
+            => new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
+
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type and the
+        /// specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType, string? fileDownloadName, bool enableRangeProcessing)
+            => new FileStreamResult(fileStream, contentType)
+            {
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>),
+        /// and the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new FileStreamResult(fileStream, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+            };
+        }
+
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>),
+        /// and the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new FileStreamResult(fileStream, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new FileStreamResult(fileStream, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+            };
+        }
+
+        /// <summary>
+        /// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="fileStream">The <see cref="Stream"/> with the contents of the file.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        /// <remarks>
+        /// The <paramref name="fileStream" /> parameter is disposed after the response is sent.
+        /// </remarks>
+        public static IResult File(Stream fileStream, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new FileStreamResult(fileStream, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+        #endregion
+
+        #region PhysicalFileResult
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(string physicalPath, string contentType)
+            => PhysicalFile(physicalPath, contentType, fileDownloadName: null);
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(string physicalPath, string contentType, bool enableRangeProcessing)
+            => PhysicalFile(physicalPath, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing);
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type and the
+        /// specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(
+            string physicalPath,
+            string contentType,
+            string? fileDownloadName)
+            => new PhysicalFileResult(physicalPath, contentType) { FileDownloadName = fileDownloadName };
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type and the
+        /// specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(
+            string physicalPath,
+            string contentType,
+            string? fileDownloadName,
+            bool enableRangeProcessing)
+            => new PhysicalFileResult(physicalPath, contentType)
+            {
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), and
+        /// the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new PhysicalFileResult(physicalPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+            };
+        }
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), and
+        /// the specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new PhysicalFileResult(physicalPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(string physicalPath, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new PhysicalFileResult(physicalPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+            };
+        }
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="physicalPath" /> (<see cref="StatusCodes.Status200OK"/>), the
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="physicalPath">The path to the file. The path must be an absolute path.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult PhysicalFile(string physicalPath, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new PhysicalFileResult(physicalPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+        #endregion
+
+        #region VirtualFileResult
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType)
+            => File(virtualPath, contentType, fileDownloadName: null);
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType, bool enableRangeProcessing)
+            => File(virtualPath, contentType, fileDownloadName: null, enableRangeProcessing: enableRangeProcessing);
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type and the
+        /// specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType, string? fileDownloadName)
+            => new VirtualFileResult(virtualPath, contentType) { FileDownloadName = fileDownloadName };
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>) with the
+        /// specified <paramref name="contentType" /> as the Content-Type and the
+        /// specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType, string? fileDownloadName, bool enableRangeProcessing)
+            => new VirtualFileResult(virtualPath, contentType)
+            {
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), and the 
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new VirtualFileResult(virtualPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+            };
+        }
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), and the 
+        /// specified <paramref name="contentType" /> as the Content-Type.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new VirtualFileResult(virtualPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), the 
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
+        {
+            return new VirtualFileResult(virtualPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+            };
+        }
+
+        /// <summary>
+        /// Returns the file specified by <paramref name="virtualPath" /> (<see cref="StatusCodes.Status200OK"/>), the 
+        /// specified <paramref name="contentType" /> as the Content-Type, and the specified <paramref name="fileDownloadName" /> as the suggested file name.
+        /// <para>
+        /// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
+        /// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
+        /// </para>
+        /// </summary>
+        /// <param name="virtualPath">The virtual path of the file to be returned.</param>
+        /// <param name="contentType">The Content-Type of the file.</param>
+        /// <param name="fileDownloadName">The suggested file name.</param>
+        /// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the file was last modified.</param>
+        /// <param name="entityTag">The <see cref="EntityTagHeaderValue"/> associated with the file.</param>
+        /// <param name="enableRangeProcessing">Set to <c>true</c> to enable range requests processing.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult File(string virtualPath, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing)
+        {
+            return new VirtualFileResult(virtualPath, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                FileDownloadName = fileDownloadName,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+        }
+        #endregion
+
+        #region RedirectResult variants
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status302Found"/>)
+        /// to the specified <paramref name="url"/>.
+        /// </summary>
+        /// <param name="url">The URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Redirect(string url)
+        {
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(url));
+            }
+
+            return new RedirectResult(url);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status301MovedPermanently" />)
+        /// to the specified <paramref name="url"/>.
+        /// </summary>
+        /// <param name="url">The URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectPermanent(string url)
+        {
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(url));
+            }
+
+            return new RedirectResult(url, permanent: true);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status307TemporaryRedirect" />)
+        /// to the specified <paramref name="url"/> preserving the HTTP method.
+        /// </summary>
+        /// <param name="url">The URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectPreserveMethod(string url)
+        {
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(url));
+            }
+
+            return new RedirectResult(url: url, permanent: false, preserveMethod: true);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status308PermanentRedirect" />)
+        /// to the specified <paramref name="url"/> preserving the HTTP method.
+        /// </summary>
+        /// <param name="url">The URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectPermanentPreserveMethod(string url)
+        {
+            if (string.IsNullOrEmpty(url))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(url));
+            }
+
+            return new RedirectResult(url: url, permanent: true, preserveMethod: true);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status302Found"/>)
+        /// to the specified <paramref name="localUrl"/>.
+        /// </summary>
+        /// <param name="localUrl">The local URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult LocalRedirect(string localUrl)
+        {
+            if (string.IsNullOrEmpty(localUrl))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(localUrl));
+            }
+
+            return new LocalRedirectResult(localUrl);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status301MovedPermanently" />)
+        /// to the specified <paramref name="localUrl"/>.
+        /// </summary>
+        /// <param name="localUrl">The local URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult LocalRedirectPermanent(string localUrl)
+        {
+            if (string.IsNullOrEmpty(localUrl))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(localUrl));
+            }
+
+            return new LocalRedirectResult(localUrl, permanent: true);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status307TemporaryRedirect" />)
+        /// to the specified <paramref name="localUrl"/> preserving the HTTP method.
+        /// </summary>
+        /// <param name="localUrl">The local URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult LocalRedirectPreserveMethod(string localUrl)
+        {
+            if (string.IsNullOrEmpty(localUrl))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(localUrl));
+            }
+
+            return new LocalRedirectResult(localUrl: localUrl, permanent: false, preserveMethod: true);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status308PermanentRedirect" />)
+        /// to the specified <paramref name="localUrl"/> preserving the HTTP method.
+        /// </summary>
+        /// <param name="localUrl">The local URL to redirect to.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult LocalRedirectPermanentPreserveMethod(string localUrl)
+        {
+            if (string.IsNullOrEmpty(localUrl))
+            {
+                throw new ArgumentException("Argument cannot be null or empty.", nameof(localUrl));
+            }
+
+            return new LocalRedirectResult(localUrl: localUrl, permanent: true, preserveMethod: true);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified route using the specified <paramref name="routeName"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoute(string? routeName)
+            => RedirectToRoute(routeName, routeValues: null);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified route using the specified <paramref name="routeValues"/>.
+        /// </summary>
+        /// <param name="routeValues">The parameters for a route.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoute(object? routeValues)
+            => RedirectToRoute(routeName: null, routeValues: routeValues);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified route using the specified
+        /// <paramref name="routeName"/> and <paramref name="routeValues"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for a route.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoute(string? routeName, object? routeValues)
+            => RedirectToRoute(routeName, routeValues, fragment: null);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified route using the specified
+        /// <paramref name="routeName"/> and <paramref name="fragment"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoute(string? routeName, string? fragment)
+            => RedirectToRoute(routeName, routeValues: null, fragment: fragment);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status302Found"/>) to the specified route using the specified
+        /// <paramref name="routeName"/>, <paramref name="routeValues"/>, and <paramref name="fragment"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for a route.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoute(
+            string? routeName,
+            object? routeValues,
+            string? fragment)
+        {
+            return new RedirectToRouteResult(routeName, routeValues, fragment);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status307TemporaryRedirect"/>) to the specified route with 
+        /// <see cref="RedirectToRouteResult.Permanent"/> set to false and <see cref="RedirectToRouteResult.PreserveMethod"/>
+        /// set to true, using the specified <paramref name="routeName"/>, <paramref name="routeValues"/>, and <paramref name="fragment"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>       
+        public static IResult RedirectToRoutePreserveMethod(
+            string? routeName = null,
+            object? routeValues = null,
+            string? fragment = null)
+        {
+            return new RedirectToRouteResult(
+                routeName: routeName,
+                routeValues: routeValues,
+                permanent: false,
+                preserveMethod: true,
+                fragment: fragment);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified route with 
+        /// <see cref="RedirectToRouteResult.Permanent"/> set to true using the specified <paramref name="routeName"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoutePermanent(string? routeName)
+            => RedirectToRoutePermanent(routeName, routeValues: null);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified route with 
+        /// <see cref="RedirectToRouteResult.Permanent"/> set to true using the specified <paramref name="routeValues"/>.
+        /// </summary>
+        /// <param name="routeValues">The parameters for a route.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoutePermanent(object? routeValues)
+            => RedirectToRoutePermanent(routeName: null, routeValues: routeValues);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified route with
+        /// <see cref="RedirectToRouteResult.Permanent"/> set to true using the specified <paramref name="routeName"/>
+        /// and <paramref name="routeValues"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for a route.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoutePermanent(string? routeName, object? routeValues)
+            => RedirectToRoutePermanent(routeName, routeValues, fragment: null);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified route with 
+        /// <see cref="RedirectToRouteResult.Permanent"/> set to true using the specified <paramref name="routeName"/>
+        /// and <paramref name="fragment"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoutePermanent(string? routeName, string? fragment)
+            => RedirectToRoutePermanent(routeName, routeValues: null, fragment: fragment);
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status301MovedPermanently"/>) to the specified route with
+        /// <see cref="RedirectToRouteResult.Permanent"/> set to true using the specified <paramref name="routeName"/>,
+        /// <paramref name="routeValues"/>, and <paramref name="fragment"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The parameters for a route.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult RedirectToRoutePermanent(
+            string? routeName,
+            object? routeValues,
+            string? fragment)
+        {
+            return new RedirectToRouteResult(routeName, routeValues, permanent: true, fragment: fragment);
+        }
+
+        /// <summary>
+        /// Redirects (<see cref="StatusCodes.Status308PermanentRedirect"/>) to the specified route with
+        /// <see cref="RedirectToRouteResult.Permanent"/> set to true and <see cref="RedirectToRouteResult.PreserveMethod"/>
+        /// set to true, using the specified <paramref name="routeName"/>, <paramref name="routeValues"/>, and <paramref name="fragment"/>.
+        /// </summary>
+        /// <param name="routeName">The name of the route.</param>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="fragment">The fragment to add to the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>       
+        public static IResult RedirectToRoutePermanentPreserveMethod(
+            string? routeName = null,
+            object? routeValues = null,
+            string? fragment = null)
+        {
+            return new RedirectToRouteResult(
+                routeName: routeName,
+                routeValues: routeValues,
+                permanent: true,
+                preserveMethod: true,
+                fragment: fragment);
+        }
+        #endregion
+
+        /// <summary>
+        /// Creates a <see cref="StatusCodeResult"/> object by specifying a <paramref name="statusCode"/>.
+        /// </summary>
+        /// <param name="statusCode">The status code to set on the response.</param>
+        /// <returns>The created <see cref="StatusCodeResult"/> object for the response.</returns>
+        public static IResult StatusCode(int statusCode)
+            => new StatusCodeResult(statusCode);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status404NotFound"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult NotFound()
+            => new NotFoundResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status404NotFound"/> response.
+        /// </summary>
+        /// <param name="value">The value to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult NotFound(object? value)
+            => new NotFoundObjectResult(value);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status401Unauthorized"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Unauthorized()
+            => new UnauthorizedResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status400BadRequest"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult BadRequest()
+            => new BadRequestResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status400BadRequest"/> response.
+        /// </summary>
+        /// <param name="error">An error object to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult BadRequest(object? error)
+            => new BadRequestObjectResult(error);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status409Conflict"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Conflict()
+            => new ConflictResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status409Conflict"/> response.
+        /// </summary>
+        /// <param name="error">An error object to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Conflict(object? error)
+            => new ConflictObjectResult(error);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status204NoContent"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult NoContent()
+            => new NoContentResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status200OK"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Ok()
+            => new OkResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status200OK"/> response.
+        /// </summary>
+        /// <param name="value">The value to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Ok(object? value)
+            => new OkObjectResult(value);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status422UnprocessableEntity"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult UnprocessableEntity()
+            => new UnprocessableEntityResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status422UnprocessableEntity"/> response.
+        /// </summary>
+        /// <param name="error">An error object to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult UnprocessableEntity(object? error)
+            => new UnprocessableEntityObjectResult(error);
+
+        #region CreatedResult
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status201Created"/> response.
+        /// </summary>
+        /// <param name="uri">The URI at which the content has been created.</param>
+        /// <param name="value">The value to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Created(string uri, object? value)
+        {
+            if (uri == null)
+            {
+                throw new ArgumentNullException(nameof(uri));
+            }
+
+            return new CreatedResult(uri, value);
+        }
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status201Created"/> response.
+        /// </summary>
+        /// <param name="uri">The URI at which the content has been created.</param>
+        /// <param name="value">The value to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Created(Uri uri, object? value)
+        {
+            if (uri == null)
+            {
+                throw new ArgumentNullException(nameof(uri));
+            }
+
+            return new CreatedResult(uri, value);
+        }
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status201Created"/> response.
+        /// </summary>
+        /// <param name="routeName">The name of the route to use for generating the URL.</param>
+        /// <param name="value">The value to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult CreatedAtRoute(string? routeName, object? value)
+            => CreatedAtRoute(routeName, routeValues: null, value: value);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status201Created"/> response.
+        /// </summary>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The value to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult CreatedAtRoute(object? routeValues, object? value)
+            => CreatedAtRoute(routeName: null, routeValues: routeValues, value: value);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status201Created"/> response.
+        /// </summary>
+        /// <param name="routeName">The name of the route to use for generating the URL.</param>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The value to be included in the HTTP response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult CreatedAtRoute(string? routeName, object? routeValues, object? value)
+            => new CreatedAtRouteResult(routeName, routeValues, value);
+
+        #endregion
+
+        #region AcceptedResult
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Accepted()
+            => new AcceptedResult();
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="value">The optional content value to format in the response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Accepted(object? value)
+            => new AcceptedResult(location: null, value: value);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="uri">The optional URI with the location at which the status of requested content can be monitored.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Accepted(Uri uri)
+        {
+            if (uri == null)
+            {
+                throw new ArgumentNullException(nameof(uri));
+            }
+
+            return new AcceptedResult(locationUri: uri, value: null);
+        }
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="uri">The optional URI with the location at which the status of requested content can be monitored.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Accepted(string? uri)
+            => new AcceptedResult(location: uri, value: null);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="uri">The URI with the location at which the status of requested content can be monitored.</param>
+        /// <param name="value">The optional content value to format in the response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Accepted(string? uri, object? value)
+            => new AcceptedResult(uri, value);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="uri">The URI with the location at which the status of requested content can be monitored.</param>
+        /// <param name="value">The optional content value to format in the response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult Accepted(Uri uri, object? value)
+        {
+            if (uri == null)
+            {
+                throw new ArgumentNullException(nameof(uri));
+            }
+
+            return new AcceptedResult(locationUri: uri, value: value);
+        }
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult AcceptedAtRoute(object? routeValues)
+            => AcceptedAtRoute(routeName: null, routeValues: routeValues, value: null);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="routeName">The name of the route to use for generating the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult AcceptedAtRoute(string? routeName)
+            => AcceptedAtRoute(routeName, routeValues: null, value: null);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="routeName">The name of the route to use for generating the URL.</param>
+        ///<param name="routeValues">The route data to use for generating the URL.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult AcceptedAtRoute(string? routeName, object? routeValues)
+            => AcceptedAtRoute(routeName, routeValues, value: null);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The optional content value to format in the response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult AcceptedAtRoute(object? routeValues, object? value)
+            => AcceptedAtRoute(routeName: null, routeValues: routeValues, value: value);
+
+        /// <summary>
+        /// Produces a <see cref="StatusCodes.Status202Accepted"/> response.
+        /// </summary>
+        /// <param name="routeName">The name of the route to use for generating the URL.</param>
+        /// <param name="routeValues">The route data to use for generating the URL.</param>
+        /// <param name="value">The optional content value to format in the response body.</param>
+        /// <returns>The created <see cref="IResult"/> for the response.</returns>
+        public static IResult AcceptedAtRoute(string? routeName, object? routeValues, object? value)
+            => new AcceptedAtRouteResult(routeName, routeValues, value);
+        #endregion
+    }
+}

+ 97 - 0
src/Http/Http.Results/src/SignInResult.cs

@@ -0,0 +1,97 @@
+// 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;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// An <see cref="IResult"/> that on execution invokes <see cref="M:HttpContext.SignInAsync"/>.
+    /// </summary>
+    internal sealed partial class SignInResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignInResult"/> with the
+        /// default authentication scheme.
+        /// </summary>
+        /// <param name="principal">The claims principal containing the user claims.</param>
+        public SignInResult(ClaimsPrincipal principal)
+            : this(authenticationScheme: null, principal, properties: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignInResult"/> with the
+        /// specified authentication scheme.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication scheme to use when signing in the user.</param>
+        /// <param name="principal">The claims principal containing the user claims.</param>
+        public SignInResult(string? authenticationScheme, ClaimsPrincipal principal)
+            : this(authenticationScheme, principal, properties: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignInResult"/> with the
+        /// default authentication scheme and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="principal">The claims principal containing the user claims.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-in operation.</param>
+        public SignInResult(ClaimsPrincipal principal, AuthenticationProperties? properties)
+            : this(authenticationScheme: null, principal, properties)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignInResult"/> with the
+        /// specified authentication scheme and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication schemes to use when signing in the user.</param>
+        /// <param name="principal">The claims principal containing the user claims.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-in operation.</param>
+        public SignInResult(string? authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties? properties)
+        {
+            Principal = principal ?? throw new ArgumentNullException(nameof(principal));
+            AuthenticationScheme = authenticationScheme;
+            Properties = properties;
+        }
+
+        /// <summary>
+        /// Gets or sets the authentication scheme that is used to perform the sign-in operation.
+        /// </summary>
+        public string? AuthenticationScheme { get; set; }
+
+        /// <summary>
+        /// Gets or sets the <see cref="ClaimsPrincipal"/> containing the user claims.
+        /// </summary>
+        public ClaimsPrincipal Principal { get; set; }
+
+        /// <summary>
+        /// Gets or sets the <see cref="AuthenticationProperties"/> used to perform the sign-in operation.
+        /// </summary>
+        public AuthenticationProperties? Properties { get; set; }
+
+        /// <inheritdoc />
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<SignInResult>>();
+
+            Log.SignInResultExecuting(logger, AuthenticationScheme, Principal);
+
+            return httpContext.SignInAsync(AuthenticationScheme, Principal, Properties);
+        }
+
+        private static partial class Log
+        {
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing SignInResult with authentication scheme ({Scheme}) and the following principal: {Principal}.",
+                EventName = "SignInResultExecuting")]
+            public static partial void SignInResultExecuting(ILogger logger, string? scheme, ClaimsPrincipal principal);
+        }
+    }
+}

+ 127 - 0
src/Http/Http.Results/src/SignOutResult.cs

@@ -0,0 +1,127 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// An <see cref="IResult"/> that on execution invokes <see cref="M:HttpContext.SignOutAsync"/>.
+    /// </summary>
+    internal sealed partial class SignOutResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignOutResult"/> with the default sign out scheme.
+        /// </summary>
+        public SignOutResult()
+            : this(Array.Empty<string>())
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignOutResult"/> with the default sign out scheme.
+        /// specified authentication scheme and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-out operation.</param>
+        public SignOutResult(AuthenticationProperties properties)
+            : this(Array.Empty<string>(), properties)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignOutResult"/> with the
+        /// specified authentication scheme.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication scheme to use when signing out the user.</param>
+        public SignOutResult(string authenticationScheme)
+            : this(new[] { authenticationScheme })
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignOutResult"/> with the
+        /// specified authentication schemes.
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication schemes to use when signing out the user.</param>
+        public SignOutResult(IList<string> authenticationSchemes)
+            : this(authenticationSchemes, properties: null)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignOutResult"/> with the
+        /// specified authentication scheme and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="authenticationScheme">The authentication schemes to use when signing out the user.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-out operation.</param>
+        public SignOutResult(string authenticationScheme, AuthenticationProperties? properties)
+            : this(new[] { authenticationScheme }, properties)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of <see cref="SignOutResult"/> with the
+        /// specified authentication schemes and <paramref name="properties"/>.
+        /// </summary>
+        /// <param name="authenticationSchemes">The authentication scheme to use when signing out the user.</param>
+        /// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the sign-out operation.</param>
+        public SignOutResult(IList<string> authenticationSchemes, AuthenticationProperties? properties)
+        {
+            AuthenticationSchemes = authenticationSchemes ?? throw new ArgumentNullException(nameof(authenticationSchemes));
+            Properties = properties;
+        }
+
+        /// <summary>
+        /// Gets or sets the authentication schemes that are challenged.
+        /// </summary>
+        public IList<string> AuthenticationSchemes { get; init; }
+
+        /// <summary>
+        /// Gets or sets the <see cref="AuthenticationProperties"/> used to perform the sign-out operation.
+        /// </summary>
+        public AuthenticationProperties? Properties { get; init; }
+
+        /// <inheritdoc />
+        public async Task ExecuteAsync(HttpContext httpContext)
+        {
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<SignOutResult>>();
+
+            Log.SignOutResultExecuting(logger, AuthenticationSchemes);
+
+            if (AuthenticationSchemes.Count == 0)
+            {
+                await httpContext.SignOutAsync(Properties);
+            }
+            else
+            {
+                for (var i = 0; i < AuthenticationSchemes.Count; i++)
+                {
+                    await httpContext.SignOutAsync(AuthenticationSchemes[i], Properties);
+                }
+            }
+        }
+
+        private static partial class Log
+        {
+            public static void SignOutResultExecuting(ILogger logger, IList<string> authenticationSchemes)
+            {
+                if (logger.IsEnabled(LogLevel.Information))
+                {
+                    SignOutResultExecuting(logger, authenticationSchemes.ToArray());
+                }
+            }
+
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing SignOutResult with authentication schemes ({Schemes}).",
+                EventName = "SignOutResultExecuting",
+                SkipEnabledCheck = true)]
+            private static partial void SignOutResultExecuting(ILogger logger, string[] schemes);
+        }
+    }
+}

+ 51 - 0
src/Http/Http.Results/src/StatusCodeResult.cs

@@ -0,0 +1,51 @@
+// 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.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal partial class StatusCodeResult : IResult
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StatusCodeResult"/> class
+        /// with the given <paramref name="statusCode"/>.
+        /// </summary>
+        /// <param name="statusCode">The HTTP status code of the response.</param>
+        public StatusCodeResult(int statusCode)
+        {
+            StatusCode = statusCode;
+        }
+
+        /// <summary>
+        /// Gets the HTTP status code.
+        /// </summary>
+        public int StatusCode { get; }
+
+        /// <summary>
+        /// Sets the status code on the HTTP response.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
+        /// <returns>A task that represents the asynchronous execute operation.</returns>
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var factory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
+            var logger = factory.CreateLogger(GetType());
+
+            Log.StatusCodeResultExecuting(logger, StatusCode);
+
+            httpContext.Response.StatusCode = StatusCode;
+            return Task.CompletedTask;
+        }
+
+        private static partial class Log
+        {
+            [LoggerMessage(1, LogLevel.Information,
+                "Executing StatusCodeResult, setting HTTP status code {StatusCode}.",
+                EventName = "StatusCodeResultExecuting")]
+            public static partial void StatusCodeResultExecuting(ILogger logger, int statusCode);
+        }
+    }
+}

+ 12 - 0
src/Http/Http.Results/src/UnauthorizedResult.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class UnauthorizedResult : StatusCodeResult
+    {
+        public UnauthorizedResult() : base(StatusCodes.Status401Unauthorized)
+        {
+        }
+    }
+}

+ 13 - 0
src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs

@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class UnprocessableEntityObjectResult : ObjectResult
+    {
+        public UnprocessableEntityObjectResult(object? error)
+            : base(error, StatusCodes.Status422UnprocessableEntity)
+        {
+        }
+    }
+}

+ 12 - 0
src/Http/Http.Results/src/UnprocessableEntityResult.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class UnprocessableEntityResult : StatusCodeResult
+    {
+        public UnprocessableEntityResult() : base(StatusCodes.Status422UnprocessableEntity)
+        {
+        }
+    }
+}

+ 129 - 0
src/Http/Http.Results/src/VirtualFileResult.cs

@@ -0,0 +1,129 @@
+// 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;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    /// <summary>
+    /// A <see cref="FileResult" /> that on execution writes the file specified using a virtual path to the response
+    /// using mechanisms provided by the host.
+    /// </summary>
+    internal sealed class VirtualFileResult : FileResult, IResult
+    {
+        private string _fileName;
+
+        /// <summary>
+        /// Creates a new <see cref="VirtualFileResult"/> instance with the provided <paramref name="fileName"/>
+        /// and the provided <paramref name="contentType"/>.
+        /// </summary>
+        /// <param name="fileName">The path to the file. The path must be relative/virtual.</param>
+        /// <param name="contentType">The Content-Type header of the response.</param>
+        public VirtualFileResult(string fileName, string contentType)
+            : this(fileName, MediaTypeHeaderValue.Parse(contentType))
+        {
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="VirtualFileResult"/> instance with
+        /// the provided <paramref name="fileName"/> and the
+        /// provided <paramref name="contentType"/>.
+        /// </summary>
+        /// <param name="fileName">The path to the file. The path must be relative/virtual.</param>
+        /// <param name="contentType">The Content-Type header of the response.</param>
+        public VirtualFileResult(string fileName, MediaTypeHeaderValue contentType)
+            : base(contentType.ToString())
+        {
+            FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
+        }
+
+        /// <summary>
+        /// Gets or sets the path to the file that will be sent back as the response.
+        /// </summary>
+        public string FileName
+        {
+            get => _fileName;
+            [MemberNotNull(nameof(_fileName))]
+            set => _fileName = value ?? throw new ArgumentNullException(nameof(value));
+        }
+
+        /// <inheritdoc/>
+        public Task ExecuteAsync(HttpContext httpContext)
+        {
+            var hostingEnvironment = httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>();
+            var logger = httpContext.RequestServices.GetRequiredService<ILogger<VirtualFileResult>>();
+
+            var fileInfo = GetFileInformation(hostingEnvironment.WebRootFileProvider);
+            if (!fileInfo.Exists)
+            {
+                throw new FileNotFoundException($"Could not find file: {FileName}.", FileName);
+            }
+
+            Log.ExecutingFileResult(logger, this);
+
+            var lastModified = LastModified ?? fileInfo.LastModified;
+            var fileResultInfo = new FileResultInfo
+            {
+                ContentType = ContentType,
+                FileDownloadName = FileDownloadName,
+                EnableRangeProcessing = EnableRangeProcessing,
+                EntityTag = EntityTag,
+                LastModified = lastModified,
+            };
+
+            var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog(
+                httpContext,
+                fileResultInfo,
+                fileInfo.Length,
+                EnableRangeProcessing,
+                lastModified,
+                EntityTag,
+                logger);
+
+            if (!serveBody)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (range != null)
+            {
+                FileResultHelper.Log.WritingRangeToBody(logger);
+            }
+
+            var response = httpContext.Response;
+            var offset = 0L;
+            var count = (long?)null;
+            if (range != null)
+            {
+                offset = range.From ?? 0L;
+                count = rangeLength;
+            }
+
+            return response.SendFileAsync(
+                fileInfo,
+                offset,
+                count);
+        }
+
+        internal IFileInfo GetFileInformation(IFileProvider fileProvider)
+        {
+            var normalizedPath = FileName;
+            if (normalizedPath.StartsWith("~", StringComparison.Ordinal))
+            {
+                normalizedPath = normalizedPath.Substring(1);
+            }
+
+            var fileInfo = fileProvider.GetFileInfo(normalizedPath);
+            return fileInfo;
+        }
+    }
+}

+ 120 - 0
src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs

@@ -0,0 +1,120 @@
+// 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;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class AcceptedAtRouteResultTests
+    {
+        [Fact]
+        public async Task ExecuteResultAsync_FormatsData()
+        {
+            // Arrange
+            var url = "testAction";
+            var linkGenerator = new TestLinkGenerator { Url = url };
+            var httpContext = GetHttpContext(linkGenerator);
+            var stream = new MemoryStream();
+            httpContext.Response.Body = stream;
+
+            var routeValues = new RouteValueDictionary(new Dictionary<string, string>()
+            {
+                { "test", "case" },
+                { "sample", "route" }
+            });
+
+            // Act
+            var result = new AcceptedAtRouteResult(
+                routeName: "sample",
+                routeValues: routeValues,
+                value: "Hello world");
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            var response = Encoding.UTF8.GetString(stream.ToArray());
+            Assert.Equal("\"Hello world\"", response);
+        }
+
+        public static TheoryData<object> AcceptedAtRouteData
+        {
+            get
+            {
+                return new TheoryData<object>
+                {
+                    null,
+                    new Dictionary<string, string>()
+                    {
+                        { "hello", "world" }
+                    },
+                    new RouteValueDictionary(
+                        new Dictionary<string, string>()
+                        {
+                            { "test", "case" },
+                            { "sample", "route" }
+                        }),
+                    };
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(AcceptedAtRouteData))]
+        public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader(object values)
+        {
+            // Arrange
+            var expectedUrl = "testAction";
+            var linkGenerator = new TestLinkGenerator { Url = expectedUrl };
+            var httpContext = GetHttpContext(linkGenerator);
+
+            // Act
+            var result = new AcceptedAtRouteResult(routeValues: values, value: null);
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode);
+            Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]);
+        }
+
+        [Fact]
+        public async Task ExecuteResultAsync_ThrowsIfRouteUrlIsNull()
+        {
+            // Arrange
+            var linkGenerator = new TestLinkGenerator();
+            var httpContext = GetHttpContext(linkGenerator);
+
+            // Act
+            var result = new AcceptedAtRouteResult(
+                routeName: null,
+                routeValues: new Dictionary<string, object>(),
+                value: null);
+
+            // Assert
+            await ExceptionAssert.ThrowsAsync<InvalidOperationException>(() =>
+                result.ExecuteAsync(httpContext),
+                "No route matches the supplied values.");
+        }
+
+        private static HttpContext GetHttpContext(LinkGenerator linkGenerator)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices(linkGenerator);
+            return httpContext;
+        }
+
+        private static IServiceProvider CreateServices(LinkGenerator linkGenerator)
+        {
+            var services = new ServiceCollection();
+            services.AddLogging();
+            services.AddSingleton(linkGenerator);
+            return services.BuildServiceProvider();
+        }
+    }
+}

+ 61 - 0
src/Http/Http.Results/test/AcceptedResultTests.cs

@@ -0,0 +1,61 @@
+// 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;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class AcceptedResultTests
+    {
+        [Fact]
+        public async Task ExecuteResultAsync_FormatsData()
+        {
+            // Arrange
+            var httpContext = GetHttpContext();
+            var stream = new MemoryStream();
+            httpContext.Response.Body = stream;
+            // Act
+            var result = new AcceptedResult("my-location", value: "Hello world");
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            var response = Encoding.UTF8.GetString(stream.ToArray());
+            Assert.Equal("\"Hello world\"", response);
+        }
+
+        [Fact]
+        public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader()
+        {
+            // Arrange
+            var expectedUrl = "testAction";
+            var httpContext = GetHttpContext();
+
+            // Act
+            var result = new AcceptedResult(expectedUrl, value: "some-value");
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode);
+            Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]);
+        }
+
+        private static HttpContext GetHttpContext()
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices();
+            return httpContext;
+        }
+
+        private static IServiceProvider CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddLogging();
+            return services.BuildServiceProvider();
+        }
+    }
+}

+ 22 - 0
src/Http/Http.Results/test/BadRequestObjectResultTests.cs

@@ -0,0 +1,22 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class BadRequestObjectResultTests
+    {
+        [Fact]
+        public void BadRequestObjectResult_SetsStatusCodeAndValue()
+        {
+            // Arrange & Act
+            var obj = new object();
+            var badRequestObjectResult = new BadRequestObjectResult(obj);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status400BadRequest, badRequestObjectResult.StatusCode);
+            Assert.Equal(obj, badRequestObjectResult.Value);
+        }
+    }
+}

+ 20 - 0
src/Http/Http.Results/test/BadRequestResultTests.cs

@@ -0,0 +1,20 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class BadRequestResultTests
+    {
+        [Fact]
+        public void BadRequestResult_InitializesStatusCode()
+        {
+            // Arrange & act
+            var badRequest = new BadRequestResult();
+
+            // Assert
+            Assert.Equal(StatusCodes.Status400BadRequest, badRequest.StatusCode);
+        }
+    }
+}

+ 62 - 0
src/Http/Http.Results/test/ChallengeResultTest.cs

@@ -0,0 +1,62 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class ChallengeResultTest
+    {
+        [Fact]
+        public async Task ChallengeResult_ExecuteAsync()
+        {
+            // Arrange
+            var result = new ChallengeResult("", null);
+            var auth = new Mock<IAuthenticationService>();
+            var httpContext = GetHttpContext(auth);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify(c => c.ChallengeAsync(httpContext, "", null), Times.Exactly(1));
+        }
+
+        [Fact]
+        public async Task ChallengeResult_ExecuteAsync_NoSchemes()
+        {
+            // Arrange
+            var result = new ChallengeResult(new string[] { }, null);
+            var auth = new Mock<IAuthenticationService>();
+            var httpContext = GetHttpContext(auth);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify(c => c.ChallengeAsync(httpContext, null, null), Times.Exactly(1));
+        }
+        
+        private static DefaultHttpContext GetHttpContext(Mock<IAuthenticationService> auth)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices()
+                .AddSingleton(auth.Object)
+                .BuildServiceProvider();
+            return httpContext;
+        }
+
+        private static IServiceCollection CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
+            return services;
+        }
+    }
+}

+ 22 - 0
src/Http/Http.Results/test/ConflictObjectResultTest.cs

@@ -0,0 +1,22 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class ConflictObjectResultTest
+    {
+        [Fact]
+        public void ConflictObjectResult_SetsStatusCodeAndValue()
+        {
+            // Arrange & Act
+            var obj = new object();
+            var conflictObjectResult = new ConflictObjectResult(obj);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode);
+            Assert.Equal(obj, conflictObjectResult.Value);
+        }
+    }
+}

+ 20 - 0
src/Http/Http.Results/test/ConflictResultTest.cs

@@ -0,0 +1,20 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class ConflictResultTest
+    {
+        [Fact]
+        public void ConflictResult_InitializesStatusCode()
+        {
+            // Arrange & act
+            var conflictResult = new ConflictResult();
+
+            // Assert
+            Assert.Equal(StatusCodes.Status409Conflict, conflictResult.StatusCode);
+        }
+    }
+}

+ 152 - 0
src/Http/Http.Results/test/ContentResultTest.cs

@@ -0,0 +1,152 @@
+// 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.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class ContentResultTest
+    {
+        [Fact]
+        public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding()
+        {
+            // Arrange
+            var contentResult = new ContentResult
+            {
+                Content = null,
+                ContentType = new MediaTypeHeaderValue("text/plain")
+                {
+                    Encoding = Encoding.Unicode
+                }.ToString()
+            };
+            var httpContext = GetHttpContext();
+
+            // Act
+            await contentResult.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType);
+        }
+
+        public static TheoryData<MediaTypeHeaderValue, string, string, string, byte[]> ContentResultContentTypeData
+        {
+            get
+            {
+                // contentType, content, responseContentType, expectedContentType, expectedData
+                return new TheoryData<MediaTypeHeaderValue, string, string, string, byte[]>
+                {
+                    {
+                        null,
+                        "κόσμε",
+                        null,
+                        "text/plain; charset=utf-8",
+                        new byte[] { 206, 186, 225, 189, 185, 207, 131, 206, 188, 206, 181 } //utf-8 without BOM
+                    },
+                    {
+                        new MediaTypeHeaderValue("text/foo"),
+                        "κόσμε",
+                        null,
+                        "text/foo",
+                        new byte[] { 206, 186, 225, 189, 185, 207, 131, 206, 188, 206, 181 } //utf-8 without BOM
+                    },
+                    {
+                        MediaTypeHeaderValue.Parse("text/foo;p1=p1-value"),
+                        "κόσμε",
+                        null,
+                        "text/foo; p1=p1-value",
+                        new byte[] { 206, 186, 225, 189, 185, 207, 131, 206, 188, 206, 181 } //utf-8 without BOM
+                    },
+                    {
+                        new MediaTypeHeaderValue("text/foo") { Encoding = Encoding.ASCII },
+                        "abcd",
+                        null,
+                        "text/foo; charset=us-ascii",
+                        new byte[] { 97, 98, 99, 100 }
+                    },
+                    {
+                        null,
+                        "abcd",
+                        "text/bar",
+                        "text/bar",
+                        new byte[] { 97, 98, 99, 100 }
+                    },
+                    {
+                        null,
+                        "abcd",
+                        "application/xml; charset=us-ascii",
+                        "application/xml; charset=us-ascii",
+                        new byte[] { 97, 98, 99, 100 }
+                    },
+                    {
+                        null,
+                        "abcd",
+                        "Invalid content type",
+                        "Invalid content type",
+                        new byte[] { 97, 98, 99, 100 }
+                    },
+                    {
+                        new MediaTypeHeaderValue("text/foo") { Charset = "us-ascii" },
+                        "abcd",
+                        "text/bar",
+                        "text/foo; charset=us-ascii",
+                        new byte[] { 97, 98, 99, 100 }
+                    },
+                };
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(ContentResultContentTypeData))]
+        public async Task ContentResult_ExecuteAsync_SetContentTypeAndEncoding_OnResponse(
+            MediaTypeHeaderValue contentType,
+            string content,
+            string responseContentType,
+            string expectedContentType,
+            byte[] expectedContentData)
+        {
+            // Arrange
+            var contentResult = new ContentResult
+            {
+                Content = content,
+                ContentType = contentType?.ToString()
+            };
+            var httpContext = GetHttpContext();
+            var memoryStream = new MemoryStream();
+            httpContext.Response.Body = memoryStream;
+            httpContext.Response.ContentType = responseContentType;
+
+            // Act
+            await contentResult.ExecuteAsync(httpContext);
+
+            // Assert
+            var finalResponseContentType = httpContext.Response.ContentType;
+            Assert.Equal(expectedContentType, finalResponseContentType);
+            Assert.Equal(expectedContentData, memoryStream.ToArray());
+            Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength);
+        }
+
+        private static IServiceCollection CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
+            return services;
+        }
+
+        private static HttpContext GetHttpContext()
+        {
+            var services = CreateServices();
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = services.BuildServiceProvider();
+
+            return httpContext;
+        }
+    }
+}

+ 93 - 0
src/Http/Http.Results/test/CreatedAtRouteResultTests.cs

@@ -0,0 +1,93 @@
+// 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;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public partial class CreatedAtRouteResultTests
+    {
+        public static IEnumerable<object[]> CreatedAtRouteData
+        {
+            get
+            {
+                yield return new object[] { null };
+                yield return
+                    new object[] {
+                        new Dictionary<string, string>() { { "hello", "world" } }
+                    };
+                yield return
+                    new object[] {
+                        new RouteValueDictionary(new Dictionary<string, string>() {
+                            { "test", "case" },
+                            { "sample", "route" }
+                        })
+                    };
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(CreatedAtRouteData))]
+        public async Task CreatedAtRouteResult_ReturnsStatusCode_SetsLocationHeader(object values)
+        {
+            // Arrange
+            var expectedUrl = "testAction";
+            var httpContext = GetHttpContext(expectedUrl);
+
+            // Act
+            var result = new CreatedAtRouteResult(routeName: null, routeValues: values, value: null);
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode);
+            Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]);
+        }
+
+        [Fact]
+        public async Task CreatedAtRouteResult_ThrowsOnNullUrl()
+        {
+            // Arrange
+            var httpContext = GetHttpContext(expectedUrl: null);
+
+            var result = new CreatedAtRouteResult(
+                routeName: null,
+                routeValues: new Dictionary<string, object>(),
+                value: null);
+
+            // Act & Assert
+            await ExceptionAssert.ThrowsAsync<InvalidOperationException>(
+                async () => await result.ExecuteAsync(httpContext),
+            "No route matches the supplied values.");
+        }
+
+        private static HttpContext GetHttpContext(string expectedUrl)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.PathBase = new PathString("");
+            httpContext.Response.Body = new MemoryStream();
+            httpContext.RequestServices = CreateServices(expectedUrl);
+            return httpContext;
+        }
+
+        private static IServiceProvider CreateServices(string expectedUrl)
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
+            services.AddSingleton<LinkGenerator>(new TestLinkGenerator
+            {
+                Url = expectedUrl
+            });
+
+            return services.BuildServiceProvider();
+        }
+    }
+}

+ 79 - 0
src/Http/Http.Results/test/CreatedResultTest.cs

@@ -0,0 +1,79 @@
+// 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;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class CreatedResultTests
+    {
+        [Fact]
+        public void CreatedResult_SetsLocation()
+        {
+            // Arrange
+            var location = "http://test/location";
+
+            // Act
+            var result = new CreatedResult(location, "testInput");
+
+            // Assert
+            Assert.Same(location, result.Location);
+        }
+
+        [Fact]
+        public async Task CreatedResult_ReturnsStatusCode_SetsLocationHeader()
+        {
+            // Arrange
+            var location = "/test/";
+            var httpContext = GetHttpContext();
+            var result = new CreatedResult(location, "testInput");
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode);
+            Assert.Equal(location, httpContext.Response.Headers["Location"]);
+        }
+
+        [Fact]
+        public async Task CreatedResult_OverwritesLocationHeader()
+        {
+            // Arrange
+            var location = "/test/";
+            var httpContext = GetHttpContext();
+            httpContext.Response.Headers["Location"] = "/different/location/";
+            var result = new CreatedResult(location, "testInput");
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode);
+            Assert.Equal(location, httpContext.Response.Headers["Location"]);
+        }
+
+        private static HttpContext GetHttpContext()
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.PathBase = new PathString("");
+            httpContext.Response.Body = new MemoryStream();
+            httpContext.RequestServices = CreateServices();
+            return httpContext;
+        }
+
+        private static IServiceProvider CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
+
+            return services.BuildServiceProvider();
+        }
+    }
+}

+ 38 - 0
src/Http/Http.Results/test/FileContentResultTest.cs

@@ -0,0 +1,38 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class FileContentResultTest : FileContentResultTestBase
+    {
+        protected override Task ExecuteAsync(
+            HttpContext httpContext,
+            byte[] buffer,
+            string contentType,
+            DateTimeOffset? lastModified = null,
+            EntityTagHeaderValue entityTag = null,
+            bool enableRangeProcessing = false)
+        {
+            var result = new FileContentResult(buffer, contentType)
+            {
+                EntityTag = entityTag,
+                LastModified = lastModified,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+
+            httpContext.RequestServices = new ServiceCollection()
+                .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>))
+                .BuildServiceProvider();
+
+            return result.ExecuteAsync(httpContext);
+        }
+    }
+}

+ 86 - 0
src/Http/Http.Results/test/FileStreamResultTest.cs

@@ -0,0 +1,86 @@
+// 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;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class FileStreamResultTest : FileStreamResultTestBase
+    {
+        protected override Task ExecuteAsync(
+            HttpContext httpContext,
+            Stream stream,
+            string contentType,
+            DateTimeOffset? lastModified = null,
+            EntityTagHeaderValue entityTag = null,
+            bool enableRangeProcessing = false)
+        {
+            var fileStreamResult = new FileStreamResult(stream, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing
+            };
+
+            return fileStreamResult.ExecuteAsync(httpContext);
+        }
+
+        [Fact]
+        public void Constructor_SetsFileName()
+        {
+            // Arrange
+            var stream = Stream.Null;
+
+            // Act
+            var result = new FileStreamResult(stream, "text/plain");
+
+            // Assert
+            Assert.Equal(stream, result.FileStream);
+        }
+
+        [Fact]
+        public void Constructor_SetsContentTypeAndParameters()
+        {
+            // Arrange
+            var stream = Stream.Null;
+            var contentType = "text/plain; charset=us-ascii; p1=p1-value";
+            var expectedMediaType = contentType;
+
+            // Act
+            var result = new FileStreamResult(stream, contentType);
+
+            // Assert
+            Assert.Equal(stream, result.FileStream);
+            Assert.Equal(expectedMediaType, result.ContentType);
+        }
+
+        [Fact]
+        public void Constructor_SetsLastModifiedAndEtag()
+        {
+            // Arrange
+            var stream = Stream.Null;
+            var contentType = "text/plain";
+            var expectedMediaType = contentType;
+            var lastModified = new DateTimeOffset();
+            var entityTag = new EntityTagHeaderValue("\"Etag\"");
+
+            // Act
+            var result = new FileStreamResult(stream, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+            };
+
+            // Assert
+            Assert.Equal(lastModified, result.LastModified);
+            Assert.Equal(entityTag, result.EntityTag);
+            Assert.Equal(expectedMediaType, result.ContentType);
+        }
+
+    }
+}

+ 129 - 0
src/Http/Http.Results/test/ForbidResultTest.cs

@@ -0,0 +1,129 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class ForbidResultTest
+    {
+        [Fact]
+        public async Task ExecuteResultAsync_InvokesForbidAsyncOnAuthenticationService()
+        {
+            // Arrange
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.ForbidAsync(It.IsAny<HttpContext>(), "", null))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new ForbidResult("", null);
+            
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        [Fact]
+        public async Task ExecuteResultAsync_InvokesForbidAsyncOnAllConfiguredSchemes()
+        {
+            // Arrange
+            var authProperties = new AuthenticationProperties();
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.ForbidAsync(It.IsAny<HttpContext>(), "Scheme1", authProperties))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            auth
+                .Setup(c => c.ForbidAsync(It.IsAny<HttpContext>(), "Scheme2", authProperties))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new ForbidResult(new[] { "Scheme1", "Scheme2" }, authProperties);
+            var routeData = new RouteData();
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        public static TheoryData ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData =>
+            new TheoryData<AuthenticationProperties>
+            {
+                null,
+                new AuthenticationProperties()
+            };
+
+        [Theory]
+        [MemberData(nameof(ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData))]
+        public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties(AuthenticationProperties expected)
+        {
+            // Arrange
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.ForbidAsync(It.IsAny<HttpContext>(), null, expected))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var result = new ForbidResult(expected);
+            var httpContext = GetHttpContext(auth.Object);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        [Theory]
+        [MemberData(nameof(ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData))]
+        public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties_WhenAuthenticationSchemesIsEmpty(
+            AuthenticationProperties expected)
+        {
+            // Arrange
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.ForbidAsync(It.IsAny<HttpContext>(), null, expected))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new ForbidResult(expected)
+            {
+                AuthenticationSchemes = new string[0]
+            };
+            var routeData = new RouteData();
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        private static DefaultHttpContext GetHttpContext(IAuthenticationService auth)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices()
+                .AddSingleton(auth)
+                .BuildServiceProvider();
+            return httpContext;
+        }
+
+        private static IServiceCollection CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
+            return services;
+        }
+    }
+}

+ 138 - 0
src/Http/Http.Results/test/LocalRedirectResultTest.cs

@@ -0,0 +1,138 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class LocalRedirectResultTest
+    {
+        [Fact]
+        public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod()
+        {
+            // Arrange
+            var url = "/test/url";
+
+            // Act
+            var result = new LocalRedirectResult(url);
+
+            // Assert
+            Assert.False(result.PreserveMethod);
+            Assert.False(result.Permanent);
+            Assert.Same(url, result.Url);
+        }
+
+        [Fact]
+        public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod()
+        {
+            // Arrange
+            var url = "/test/url";
+
+            // Act
+            var result = new LocalRedirectResult(url, permanent: true);
+
+            // Assert
+            Assert.False(result.PreserveMethod);
+            Assert.True(result.Permanent);
+            Assert.Same(url, result.Url);
+        }
+
+        [Fact]
+        public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlPermanentAndPreserveMethod()
+        {
+            // Arrange
+            var url = "/test/url";
+
+            // Act
+            var result = new LocalRedirectResult(url, permanent: true, preserveMethod: true);
+
+            // Assert
+            Assert.True(result.PreserveMethod);
+            Assert.True(result.Permanent);
+            Assert.Same(url, result.Url);
+        }
+
+        [Fact]
+        public async Task Execute_ReturnsExpectedValues()
+        {
+            // Arrange
+            var appRoot = "/";
+            var contentPath = "~/Home/About";
+            var expectedPath = "/Home/About";
+
+            var httpContext = GetHttpContext(appRoot);
+            var result = new LocalRedirectResult(contentPath);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString());
+            Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode);
+        }
+
+        [Theory]
+        [InlineData("", "//")]
+        [InlineData("", "/\\")]
+        [InlineData("", "//foo")]
+        [InlineData("", "/\\foo")]
+        [InlineData("", "Home/About")]
+        [InlineData("/myapproot", "http://www.example.com")]
+        public async Task Execute_Throws_ForNonLocalUrl(
+            string appRoot,
+            string contentPath)
+        {
+            // Arrange
+            var httpContext = GetHttpContext(appRoot);
+            var result = new LocalRedirectResult(contentPath);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => result.ExecuteAsync(httpContext));
+            Assert.Equal(
+                "The supplied URL is not local. A URL with an absolute path is considered local if it does not " +
+                "have a host/authority part. URLs using virtual paths ('~/') are also local.",
+                exception.Message);
+        }
+
+        [Theory]
+        [InlineData("", "~//")]
+        [InlineData("", "~/\\")]
+        [InlineData("", "~//foo")]
+        [InlineData("", "~/\\foo")]
+        public async Task Execute_Throws_ForNonLocalUrlTilde(
+            string appRoot,
+            string contentPath)
+        {
+            // Arrange
+            var httpContext = GetHttpContext(appRoot);
+            var result = new LocalRedirectResult(contentPath);
+
+            // Act & Assert
+            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => result.ExecuteAsync(httpContext));
+            Assert.Equal(
+                "The supplied URL is not local. A URL with an absolute path is considered local if it does not " +
+                "have a host/authority part. URLs using virtual paths ('~/') are also local.",
+                exception.Message);
+        }
+
+        private static IServiceProvider GetServiceProvider()
+        {
+            var serviceCollection = new ServiceCollection();
+            serviceCollection.AddTransient(typeof(ILogger<>), typeof(NullLogger<>));
+            return serviceCollection.BuildServiceProvider();
+        }
+
+        private static HttpContext GetHttpContext(string appRoot)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = GetServiceProvider();
+            httpContext.Request.PathBase = new PathString(appRoot);
+            return httpContext;
+        }
+    }
+}

+ 15 - 0
src/Http/Http.Results/test/Microsoft.AspNetCore.Http.Results.Tests.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Http" />
+    <Reference Include="Microsoft.AspNetCore.Http.Results" />
+    <Reference Include="Microsoft.Extensions.DependencyInjection" />
+
+    <Compile Include="$(SharedSourceRoot)ResultsTests\*.cs" LinkBase="Shared" />
+  </ItemGroup>
+
+</Project>

+ 67 - 0
src/Http/Http.Results/test/NotFoundObjectResultTest.cs

@@ -0,0 +1,67 @@
+// 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;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class NotFoundObjectResultTest
+    {
+        [Fact]
+        public void NotFoundObjectResult_InitializesStatusCode()
+        {
+            // Arrange & act
+            var notFound = new NotFoundObjectResult(null);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode);
+        }
+
+        [Fact]
+        public void NotFoundObjectResult_InitializesStatusCodeAndResponseContent()
+        {
+            // Arrange & act
+            var notFound = new NotFoundObjectResult("Test Content");
+
+            // Assert
+            Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode);
+            Assert.Equal("Test Content", notFound.Value);
+        }
+
+        [Fact]
+        public async Task NotFoundObjectResult_ExecuteSuccessful()
+        {
+            // Arrange
+            var httpContext = GetHttpContext();
+            var result = new NotFoundObjectResult("Test Content");
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode);
+        }
+
+        private static HttpContext GetHttpContext()
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.PathBase = new PathString("");
+            httpContext.Response.Body = new MemoryStream();
+            httpContext.RequestServices = CreateServices();
+            return httpContext;
+        }
+
+        private static IServiceProvider CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
+            return services.BuildServiceProvider();
+        }
+    }
+}

+ 20 - 0
src/Http/Http.Results/test/NotFoundResultTests.cs

@@ -0,0 +1,20 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class NotFoundResultTests
+    {
+        [Fact]
+        public void NotFoundResult_InitializesStatusCode()
+        {
+            // Arrange & act
+            var notFound = new NotFoundResult();
+
+            // Assert
+            Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode);
+        }
+    }
+}

+ 65 - 0
src/Http/Http.Results/test/ObjectResultTests.cs

@@ -0,0 +1,65 @@
+// 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;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class ObjectResultTests
+    {
+        [Fact]
+        public async Task ObjectResult_ExecuteAsync_SetsStatusCode()
+        {
+            // Arrange
+            var result = new ObjectResult("Hello", 407);
+
+            var httpContext = new DefaultHttpContext()
+            {
+                RequestServices = CreateServices(),
+            };
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(407, httpContext.Response.StatusCode);
+        }
+
+        [Fact]
+        public async Task ObjectResult_ExecuteAsync_JsonSerializesBody()
+        {
+            // Arrange
+            var result = new ObjectResult("Hello", 407);
+            var stream = new MemoryStream();
+            var httpContext = new DefaultHttpContext()
+            {
+                RequestServices = CreateServices(),
+                Response =
+                {
+                    Body = stream,
+                },
+            };
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray()));
+        }
+
+        private static IServiceProvider CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
+
+            return services.BuildServiceProvider();
+        }
+    }
+}

+ 46 - 0
src/Http/Http.Results/test/OkObjectResultTest.cs

@@ -0,0 +1,46 @@
+// 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;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class OkObjectResultTest
+    {
+        [Fact]
+        public async Task OkObjectResult_SetsStatusCodeAndValue()
+        {
+            // Arrange
+            var result = new OkObjectResult("Hello world");
+            var httpContext = GetHttpContext();
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
+        }
+
+        private static HttpContext GetHttpContext()
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.PathBase = new PathString("");
+            httpContext.Response.Body = new MemoryStream();
+            httpContext.RequestServices = CreateServices();
+            return httpContext;
+        }
+
+        private static IServiceProvider CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
+            return services.BuildServiceProvider();
+        }
+    }
+}

+ 37 - 0
src/Http/Http.Results/test/OkResultTest.cs

@@ -0,0 +1,37 @@
+// 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.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class OkResultTest
+    {
+        [Fact]
+        public async Task HttpOkResult_SetsStatusCode()
+        {
+            // Arrange
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices().BuildServiceProvider();
+
+            var result = new OkResult();
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode);
+        }
+
+        private static IServiceCollection CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
+            return services;
+        }
+    }
+}

+ 41 - 0
src/Http/Http.Results/test/PhysicalFileResultTest.cs

@@ -0,0 +1,41 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class PhysicalFileResultTest : PhysicalFileResultTestBase
+    {
+        protected override Task ExecuteAsync(
+            HttpContext httpContext,
+            string path,
+            string contentType,
+            DateTimeOffset? lastModified = null,
+            EntityTagHeaderValue entityTag = null,
+            bool enableRangeProcessing = false)
+        {
+            var fileResult = new PhysicalFileResult(path, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing,
+                GetFileInfoWrapper = (path) =>
+                {
+                    var lastModified = DateTimeOffset.MinValue.AddDays(1);
+                    return new()
+                    {
+                        Exists = true,
+                        Length = 34,
+                        LastWriteTimeUtc = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0))
+                    };
+                }
+            };
+
+            return fileResult.ExecuteAsync(httpContext);
+        }
+    }
+}

+ 6 - 39
src/Mvc/Mvc.Core/test/RedirectActionResultTest.cs → src/Http/Http.Results/test/RedirectResultTest.cs

@@ -1,13 +1,13 @@
 // 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;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
 using Xunit;
 
-namespace Microsoft.AspNetCore.Mvc
+namespace Microsoft.AspNetCore.Http.Result
 {
-    public class RedirectActionResultTest
+    public class RedirectResultTest : RedirectResultTestBase
     {
         [Fact]
         public void RedirectResult_Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod()
@@ -54,43 +54,10 @@ namespace Microsoft.AspNetCore.Mvc
             Assert.Same(url, result.Url);
         }
 
-        [Theory]
-        [InlineData("", "/Home/About", "/Home/About")]
-        [InlineData("/myapproot", "/test", "/test")]
-        public async Task Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde(
-            string appRoot,
-            string contentPath,
-            string expectedPath)
+        protected override Task ExecuteAsync(HttpContext httpContext, string contentPath)
         {
-            var action
-                = new Func<RedirectResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseRedirectResultTest.Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde(
-                appRoot,
-                contentPath,
-                expectedPath,
-                action);
-        }
-
-        [Theory]
-        [InlineData(null, "~/Home/About", "/Home/About")]
-        [InlineData("/", "~/Home/About", "/Home/About")]
-        [InlineData("/", "~/", "/")]
-        [InlineData("", "~/Home/About", "/Home/About")]
-        [InlineData("/myapproot", "~/", "/myapproot/")]
-        public async Task Execute_ReturnsAppRelativePath_WhenItStartsWithTilde(
-            string appRoot,
-            string contentPath,
-            string expectedPath)
-        {
-            var action =
-                new Func<RedirectResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseRedirectResultTest.Execute_ReturnsAppRelativePath_WhenItStartsWithTilde(
-                appRoot,
-                contentPath,
-                expectedPath,
-                action);
+            var redirectResult = new RedirectResult(contentPath);
+            return redirectResult.ExecuteAsync(httpContext);
         }
     }
 }

+ 111 - 0
src/Http/Http.Results/test/RedirectToRouteResultTest.cs

@@ -0,0 +1,111 @@
+// 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;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class RedirectToRouteResultTest
+    {
+        [Fact]
+        public async Task RedirectToRoute_Execute_ThrowsOnNullUrl()
+        {
+            // Arrange
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices(null).BuildServiceProvider();
+
+            var result = new RedirectToRouteResult(null, new Dictionary<string, object>());
+
+            // Act & Assert
+            await ExceptionAssert.ThrowsAsync<InvalidOperationException>(
+                async () =>
+                {
+                    await result.ExecuteAsync(httpContext);
+                },
+                "No route matches the supplied values.");
+        }
+
+        [Fact]
+        public async Task ExecuteResultAsync_UsesRouteName_ToGenerateLocationHeader()
+        {
+            // Arrange
+            var routeName = "orders_api";
+            var locationUrl = "/api/orders/10";
+
+            var httpContext = GetHttpContext(locationUrl);
+
+            var result = new RedirectToRouteResult(routeName, new { id = 10 });
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.True(httpContext.Response.Headers.ContainsKey("Location"), "Location header not found");
+            Assert.Equal(locationUrl, httpContext.Response.Headers["Location"]);
+        }
+
+        [Fact]
+        public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect()
+        {
+            // Arrange
+            var expectedUrl = "/SampleAction#test";
+            var expectedStatusCode = StatusCodes.Status301MovedPermanently;
+            var httpContext = GetHttpContext(expectedUrl);
+
+            var result = new RedirectToRouteResult("Sample", null, true, "test");
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode);
+            Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]);
+        }
+
+        [Fact]
+        public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect_WithPreserveMethod()
+        {
+            // Arrange
+            var expectedUrl = "/SampleAction#test";
+            var expectedStatusCode = StatusCodes.Status308PermanentRedirect;
+
+            var httpContext = GetHttpContext(expectedUrl);
+            var result = new RedirectToRouteResult("Sample", null, true, true, "test");
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode);
+            Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]);
+        }
+
+        private static HttpContext GetHttpContext(string path)
+        {
+            var services = CreateServices(path);
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = services.BuildServiceProvider();
+
+            return httpContext;
+        }
+
+        private static IServiceCollection CreateServices(string path)
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<LinkGenerator>(new TestLinkGenerator { Url = path });
+
+            services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
+            return services;
+        }
+    }
+}

+ 95 - 0
src/Http/Http.Results/test/SignInResultTest.cs

@@ -0,0 +1,95 @@
+// 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.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class SignInResultTest
+    {
+        [Fact]
+        public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManager()
+        {
+            // Arrange
+            var principal = new ClaimsPrincipal();
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.SignInAsync(It.IsAny<HttpContext>(), "", principal, null))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new SignInResult("", principal, null);
+        
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        [Fact]
+        public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManagerWithDefaultScheme()
+        {
+            // Arrange
+            var principal = new ClaimsPrincipal();
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.SignInAsync(It.IsAny<HttpContext>(), null, principal, null))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new SignInResult(principal);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        [Fact]
+        public async Task ExecuteAsync_InvokesSignInAsyncOnConfiguredScheme()
+        {
+            // Arrange
+            var principal = new ClaimsPrincipal();
+            var authProperties = new AuthenticationProperties();
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.SignInAsync(It.IsAny<HttpContext>(), "Scheme1", principal, authProperties))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new SignInResult("Scheme1", principal, authProperties);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        private static DefaultHttpContext GetHttpContext(IAuthenticationService auth)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices()
+                .AddSingleton(auth)
+                .BuildServiceProvider();
+            return httpContext;
+        }
+
+        private static IServiceCollection CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
+            return services;
+        }
+    }
+}

+ 95 - 0
src/Http/Http.Results/test/SignOutResultTest.cs

@@ -0,0 +1,95 @@
+// 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.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class SignOutResultTest
+    {
+        [Fact]
+        public async Task ExecuteAsync_NoArgsInvokesDefaultSignOut()
+        {
+            // Arrange
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.SignOutAsync(It.IsAny<HttpContext>(), null, null))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new SignOutResult();
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        [Fact]
+        public async Task ExecuteAsync_InvokesSignOutAsyncOnAuthenticationManager()
+        {
+            // Arrange
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.SignOutAsync(It.IsAny<HttpContext>(), "", null))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new SignOutResult("", null);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        [Fact]
+        public async Task ExecuteAsync_InvokesSignOutAsyncOnAllConfiguredSchemes()
+        {
+            // Arrange
+            var authProperties = new AuthenticationProperties();
+            var auth = new Mock<IAuthenticationService>();
+            auth
+                .Setup(c => c.SignOutAsync(It.IsAny<HttpContext>(), "Scheme1", authProperties))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            auth
+                .Setup(c => c.SignOutAsync(It.IsAny<HttpContext>(), "Scheme2", authProperties))
+                .Returns(Task.CompletedTask)
+                .Verifiable();
+            var httpContext = GetHttpContext(auth.Object);
+            var result = new SignOutResult(new[] { "Scheme1", "Scheme2" }, authProperties);
+
+            // Act
+            await result.ExecuteAsync(httpContext);
+
+            // Assert
+            auth.Verify();
+        }
+
+        private static DefaultHttpContext GetHttpContext(IAuthenticationService auth)
+        {
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = CreateServices()
+                .AddSingleton(auth)
+                .BuildServiceProvider();
+            return httpContext;
+        }
+
+        private static IServiceCollection CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
+            return services;
+        }
+    }
+}

+ 45 - 0
src/Http/Http.Results/test/StatusCodeResultTests.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 Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class StatusCodeResultTests
+    {
+        [Fact]
+        public void StatusCodeResult_ExecuteResultSetsResponseStatusCode()
+        {
+            // Arrange
+            var result = new StatusCodeResult(StatusCodes.Status404NotFound);
+
+            var httpContext = GetHttpContext();
+
+            // Act
+            result.ExecuteAsync(httpContext);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode);
+        }
+
+        private static IServiceCollection CreateServices()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
+            return services;
+        }
+
+        private static HttpContext GetHttpContext()
+        {
+            var services = CreateServices();
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.RequestServices = services.BuildServiceProvider();
+
+            return httpContext;
+        }
+    }
+}

+ 29 - 0
src/Http/Http.Results/test/TestLinkGenerator.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 System;
+using Microsoft.AspNetCore.Routing;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    internal sealed class TestLinkGenerator : LinkGenerator
+    {
+        public string Url { get; set; }
+
+        public override string GetPathByAddress<TAddress>(HttpContext httpContext, TAddress address, RouteValueDictionary values, RouteValueDictionary ambientValues = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override string GetPathByAddress<TAddress>(TAddress address, RouteValueDictionary values, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override string GetUriByAddress<TAddress>(HttpContext httpContext, TAddress address, RouteValueDictionary values, RouteValueDictionary ambientValues = null, string scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null)
+            => Url;
+
+        public override string GetUriByAddress<TAddress>(TAddress address, RouteValueDictionary values, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null)
+            => Url;
+    }
+}

+ 20 - 0
src/Http/Http.Results/test/UnauthorizedResultTests.cs

@@ -0,0 +1,20 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class UnauthorizedResultTests
+    {
+        [Fact]
+        public void UnauthorizedResult_InitializesStatusCode()
+        {
+            // Arrange & act
+            var result = new UnauthorizedResult();
+
+            // Assert
+            Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode);
+        }
+    }
+}

+ 22 - 0
src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs

@@ -0,0 +1,22 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class UnprocessableEntityObjectResultTests
+    {
+        [Fact]
+        public void UnprocessableEntityObjectResult_SetsStatusCodeAndValue()
+        {
+            // Arrange & Act
+            var obj = new object();
+            var result = new UnprocessableEntityObjectResult(obj);
+
+            // Assert
+            Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode);
+            Assert.Equal(obj, result.Value);
+        }
+    }
+}

+ 20 - 0
src/Http/Http.Results/test/UnprocessableEntityResultTests.cs

@@ -0,0 +1,20 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class UnprocessableEntityResultTests
+    {
+        [Fact]
+        public void UnprocessableEntityResult_InitializesStatusCode()
+        {
+            // Arrange & act
+            var result = new UnprocessableEntityResult();
+
+            // Assert
+            Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode);
+        }
+    }
+}

+ 25 - 0
src/Http/Http.Results/test/VirtualFileResultTest.cs

@@ -0,0 +1,25 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Http.Result
+{
+    public class VirtualFileResultTest : VirtualFileResultTestBase
+    {
+        protected override Task ExecuteAsync(HttpContext httpContext, string path, string contentType, DateTimeOffset? lastModified = null, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false)
+        {
+            var result = new VirtualFileResult(path, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+
+            return result.ExecuteAsync(httpContext);
+        }
+    }
+}

+ 2 - 0
src/Http/HttpAbstractions.slnf

@@ -19,6 +19,8 @@
       "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
       "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj",
       "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
+      "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj",
+      "src\\Http\\Http.Results\\test\\Microsoft.AspNetCore.Http.Results.Tests.csproj",
       "src\\Http\\Http\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Microbenchmarks.csproj",
       "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
       "src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj",

+ 13 - 13
src/Middleware/StaticFiles/src/LoggerExtensions.cs

@@ -12,19 +12,19 @@ namespace Microsoft.AspNetCore.StaticFiles
     /// </summary>
     internal static class LoggerExtensions
     {
-        private static Action<ILogger, string, Exception?> _methodNotSupported;
-        private static Action<ILogger, string, string, Exception?> _fileServed;
-        private static Action<ILogger, string, Exception?> _pathMismatch;
-        private static Action<ILogger, string, Exception?> _fileTypeNotSupported;
-        private static Action<ILogger, string, Exception?> _fileNotFound;
-        private static Action<ILogger, string, Exception?> _fileNotModified;
-        private static Action<ILogger, string, Exception?> _preconditionFailed;
-        private static Action<ILogger, int, string, Exception?> _handled;
-        private static Action<ILogger, string, Exception?> _rangeNotSatisfiable;
-        private static Action<ILogger, StringValues, string, Exception?> _sendingFileRange;
-        private static Action<ILogger, StringValues, string, Exception?> _copyingFileRange;
-        private static Action<ILogger, Exception?> _writeCancelled;
-        private static Action<ILogger, Exception?> _endpointMatched;
+        private static readonly Action<ILogger, string, Exception?> _methodNotSupported;
+        private static readonly Action<ILogger, string, string, Exception?> _fileServed;
+        private static readonly Action<ILogger, string, Exception?> _pathMismatch;
+        private static readonly Action<ILogger, string, Exception?> _fileTypeNotSupported;
+        private static readonly Action<ILogger, string, Exception?> _fileNotFound;
+        private static readonly Action<ILogger, string, Exception?> _fileNotModified;
+        private static readonly Action<ILogger, string, Exception?> _preconditionFailed;
+        private static readonly Action<ILogger, int, string, Exception?> _handled;
+        private static readonly Action<ILogger, string, Exception?> _rangeNotSatisfiable;
+        private static readonly Action<ILogger, StringValues, string, Exception?> _sendingFileRange;
+        private static readonly Action<ILogger, StringValues, string, Exception?> _copyingFileRange;
+        private static readonly Action<ILogger, Exception?> _writeCancelled;
+        private static readonly Action<ILogger, Exception?> _endpointMatched;
 
         static LoggerExtensions()
         {

+ 3 - 13
src/Mvc/Mvc.Core/src/ChallengeResult.cs

@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// <summary>
     /// An <see cref="ActionResult"/> that on execution invokes <see cref="M:HttpContext.ChallengeAsync"/>.
     /// </summary>
-    public class ChallengeResult : ActionResult, IResult
+    public class ChallengeResult : ActionResult
     {
         /// <summary>
         /// Initializes a new instance of <see cref="ChallengeResult"/>.
@@ -91,24 +91,14 @@ namespace Microsoft.AspNetCore.Mvc
         public AuthenticationProperties? Properties { get; set; }
 
         /// <inheritdoc />
-        public override Task ExecuteResultAsync(ActionContext context)
+        public override async Task ExecuteResultAsync(ActionContext context)
         {
             if (context == null)
             {
                 throw new ArgumentNullException(nameof(context));
             }
 
-            return ExecuteAsync(context.HttpContext);
-        }
-
-        /// <inheritdoc />
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            return ExecuteAsync(httpContext);
-        }
-
-        private async Task ExecuteAsync(HttpContext httpContext)
-        {
+            var httpContext = context.HttpContext;
             var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
             var logger = loggerFactory.CreateLogger<ChallengeResult>();
 

+ 1 - 47
src/Mvc/Mvc.Core/src/ContentResult.cs

@@ -3,22 +3,16 @@
 
 using System;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
 
 namespace Microsoft.AspNetCore.Mvc
 {
     /// <summary>
     /// A <see cref="ActionResult"/> that when executed will produce a response with content.
     /// </summary>
-    public class ContentResult : ActionResult, IResult, IStatusCodeActionResult
+    public class ContentResult : ActionResult, IStatusCodeActionResult
     {
-        private const string DefaultContentType = "text/plain; charset=utf-8";
-
         /// <summary>
         /// Gets or set the content representing the body of the response.
         /// </summary>
@@ -45,45 +39,5 @@ namespace Microsoft.AspNetCore.Mvc
             var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ContentResult>>();
             return executor.ExecuteAsync(context, this);
         }
-
-        /// <summary>
-        /// Writes the content to the HTTP response.
-        /// </summary>
-        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
-        /// <returns>A task that represents the asynchronous execute operation.</returns>
-        async Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            if (httpContext == null)
-            {
-                throw new ArgumentNullException(nameof(httpContext));
-            }
-
-            var response = httpContext.Response;
-
-            ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
-                ContentType,
-                response.ContentType,
-                DefaultContentType,
-                out var resolvedContentType,
-                out var resolvedContentTypeEncoding);
-
-            response.ContentType = resolvedContentType;
-
-            if (StatusCode != null)
-            {
-                response.StatusCode = StatusCode.Value;
-            }
-
-            var factory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
-            var logger = factory.CreateLogger<ContentResult>();
-
-            logger.ContentResultExecuting(resolvedContentType);
-
-            if (Content != null)
-            {
-                response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content);
-                await response.WriteAsync(Content, resolvedContentTypeEncoding);
-            }
-        }
     }
 }

+ 1 - 42
src/Mvc/Mvc.Core/src/FileContentResult.cs

@@ -3,12 +3,9 @@
 
 using System;
 using System.Diagnostics.CodeAnalysis;
-using System.IO;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -17,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// Represents an <see cref="ActionResult"/> that when executed will
     /// write a binary file to the response.
     /// </summary>
-    public class FileContentResult : FileResult, IResult
+    public class FileContentResult : FileResult
     {
         private byte[] _fileContents;
 
@@ -80,43 +77,5 @@ namespace Microsoft.AspNetCore.Mvc
             var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<FileContentResult>>();
             return executor.ExecuteAsync(context, this);
         }
-
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            if (httpContext == null)
-            {
-                throw new ArgumentNullException(nameof(httpContext));
-            }
-
-            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
-            var logger = loggerFactory.CreateLogger<RedirectResult>();
-
-            var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog(
-                httpContext,
-                this,
-                FileContents.Length,
-                EnableRangeProcessing,
-                LastModified,
-                EntityTag,
-                logger);
-
-            if (!serveBody)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (range != null && rangeLength == 0)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (range != null)
-            {
-                logger.WritingRangeToBody();
-            }
-
-            var fileContentStream = new MemoryStream(FileContents);
-            return FileResultExecutorBase.WriteFileAsyncInternal(httpContext, fileContentStream, range, rangeLength);
-        }
     }
 }

+ 3 - 13
src/Mvc/Mvc.Core/src/ForbidResult.cs

@@ -5,7 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
@@ -14,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// <summary>
     /// An <see cref="ActionResult"/> that on execution invokes <see cref="M:HttpContext.ForbidAsync"/>.
     /// </summary>
-    public class ForbidResult : ActionResult, IResult
+    public class ForbidResult : ActionResult
     {
         /// <summary>
         /// Initializes a new instance of <see cref="ForbidResult"/>.
@@ -91,24 +90,15 @@ namespace Microsoft.AspNetCore.Mvc
         public AuthenticationProperties? Properties { get; set; }
 
         /// <inheritdoc />
-        public override Task ExecuteResultAsync(ActionContext context)
+        public override async Task ExecuteResultAsync(ActionContext context)
         {
             if (context == null)
             {
                 throw new ArgumentNullException(nameof(context));
             }
 
-            return ExecuteAsync(context.HttpContext);
-        }
-
-        /// <inheritdoc />
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            return ExecuteAsync(httpContext);
-        }
+            var httpContext = context.HttpContext;
 
-        private async Task ExecuteAsync(HttpContext httpContext)
-        {
             var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
             var logger = loggerFactory.CreateLogger<ForbidResult>();
 

+ 4 - 1
src/Mvc/Mvc.Core/src/Infrastructure/ContentResultExecutor.cs

@@ -4,7 +4,9 @@
 #nullable enable
 
 using System;
+using System.Text;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Extensions.Logging;
 
@@ -48,7 +50,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
             ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
                 result.ContentType,
                 response.ContentType,
-                DefaultContentType,
+                (DefaultContentType, Encoding.UTF8),
+                MediaType.GetEncoding,
                 out var resolvedContentType,
                 out var resolvedContentTypeEncoding);
 

+ 10 - 363
src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs

@@ -1,15 +1,10 @@
 // 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.
 
-#nullable enable
-
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.Http.Headers;
 using Microsoft.AspNetCore.Internal;
 using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
@@ -17,12 +12,10 @@ using Microsoft.Net.Http.Headers;
 namespace Microsoft.AspNetCore.Mvc.Infrastructure
 {
     /// <summary>
-    /// Base class for a file result.
+    /// Base class for executing a file result.
     /// </summary>
     public class FileResultExecutorBase
     {
-        private const string AcceptRangeHeaderValue = "bytes";
-
         /// <summary>
         /// The buffer size: 64 * 1024.
         /// </summary>
@@ -68,330 +61,16 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
             DateTimeOffset? lastModified = null,
             EntityTagHeaderValue? etag = null)
         {
-            return SetHeadersAndLog(
-                context.HttpContext,
-                result,
-                fileLength,
-                enableRangeProcessing,
-                lastModified,
-                etag,
-                Logger);
-        }
-
-        internal static (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetHeadersAndLog(
-            HttpContext httpContext,
-            FileResult result,
-            long? fileLength,
-            bool enableRangeProcessing,
-            DateTimeOffset? lastModified,
-            EntityTagHeaderValue? etag,
-            ILogger logger)
-        {
-            if (httpContext == null)
-            {
-                throw new ArgumentNullException(nameof(httpContext));
-            }
-            if (result == null)
-            {
-                throw new ArgumentNullException(nameof(result));
-            }
-
-            var request = httpContext.Request;
-            var httpRequestHeaders = request.GetTypedHeaders();
-
-            // Since the 'Last-Modified' and other similar http date headers are rounded down to whole seconds,
-            // round down current file's last modified to whole seconds for correct comparison.
-            if (lastModified.HasValue)
-            {
-                lastModified = RoundDownToWholeSeconds(lastModified.Value);
-            }
-
-            var preconditionState = GetPreconditionState(httpRequestHeaders, lastModified, etag, logger);
-
-            var response = httpContext.Response;
-            SetLastModifiedAndEtagHeaders(response, lastModified, etag);
-
-            // Short circuit if the preconditional headers process to 304 (NotModified) or 412 (PreconditionFailed)
-            if (preconditionState == PreconditionState.NotModified)
-            {
-                response.StatusCode = StatusCodes.Status304NotModified;
-                return (range: null, rangeLength: 0, serveBody: false);
-            }
-            else if (preconditionState == PreconditionState.PreconditionFailed)
-            {
-                response.StatusCode = StatusCodes.Status412PreconditionFailed;
-                return (range: null, rangeLength: 0, serveBody: false);
-            }
-
-            SetContentType(httpContext, result);
-            SetContentDispositionHeader(httpContext, result);
-
-            if (fileLength.HasValue)
-            {
-                // Assuming the request is not a range request, and the response body is not empty, the Content-Length header is set to 
-                // the length of the entire file. 
-                // If the request is a valid range request, this header is overwritten with the length of the range as part of the 
-                // range processing (see method SetContentLength).
-
-                response.ContentLength = fileLength.Value;
-
-                // Handle range request
-                if (enableRangeProcessing)
-                {
-                    SetAcceptRangeHeader(response);
-
-                    // If the request method is HEAD or GET, PreconditionState is Unspecified or ShouldProcess, and IfRange header is valid,
-                    // range should be processed and Range headers should be set
-                    if ((HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method))
-                        && (preconditionState == PreconditionState.Unspecified || preconditionState == PreconditionState.ShouldProcess)
-                        && (IfRangeValid(httpRequestHeaders, lastModified, etag, logger)))
-                    {
-                        return SetRangeHeaders(httpContext, httpRequestHeaders, fileLength.Value, logger);
-                    }
-                }
-                else
-                {
-                    logger.NotEnabledForRangeProcessing();
-                }
-            }
-
-            return (range: null, rangeLength: 0, serveBody: !HttpMethods.IsHead(request.Method));
-        }
-
-        private static void SetContentType(HttpContext httpContext, FileResult result)
-        {
-            var response = httpContext.Response;
-            response.ContentType = result.ContentType;
-        }
-
-        private static void SetContentDispositionHeader(HttpContext httpContext, FileResult result)
-        {
-            if (!string.IsNullOrEmpty(result.FileDownloadName))
-            {
-                // From RFC 2183, Sec. 2.3:
-                // The sender may want to suggest a filename to be used if the entity is
-                // detached and stored in a separate file. If the receiving MUA writes
-                // the entity to a file, the suggested filename should be used as a
-                // basis for the actual filename, where possible.
-                var contentDisposition = new ContentDispositionHeaderValue("attachment");
-                contentDisposition.SetHttpFileName(result.FileDownloadName);
-                httpContext.Response.Headers.ContentDisposition = contentDisposition.ToString();
-            }
-        }
-
-        private static void SetLastModifiedAndEtagHeaders(HttpResponse response, DateTimeOffset? lastModified, EntityTagHeaderValue? etag)
-        {
-            var httpResponseHeaders = response.GetTypedHeaders();
-            if (lastModified.HasValue)
-            {
-                httpResponseHeaders.LastModified = lastModified;
-            }
-            if (etag != null)
-            {
-                httpResponseHeaders.ETag = etag;
-            }
-        }
-
-        private static void SetAcceptRangeHeader(HttpResponse response)
-        {
-            response.Headers.AcceptRanges = AcceptRangeHeaderValue;
-        }
-
-        internal static bool IfRangeValid(
-            RequestHeaders httpRequestHeaders,
-            DateTimeOffset? lastModified,
-            EntityTagHeaderValue? etag,
-            ILogger logger)
-        {
-            // 14.27 If-Range
-            var ifRange = httpRequestHeaders.IfRange;
-            if (ifRange != null)
-            {
-                // If the validator given in the If-Range header field matches the
-                // current validator for the selected representation of the target
-                // resource, then the server SHOULD process the Range header field as
-                // requested.  If the validator does not match, the server MUST ignore
-                // the Range header field.
-                if (ifRange.LastModified.HasValue)
-                {
-                    if (lastModified.HasValue && lastModified > ifRange.LastModified)
-                    {
-                        logger.IfRangeLastModifiedPreconditionFailed(lastModified, ifRange.LastModified);
-                        return false;
-                    }
-                }
-                else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true))
-                {
-                    logger.IfRangeETagPreconditionFailed(etag, ifRange.EntityTag);
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        // Internal for testing
-        internal static PreconditionState GetPreconditionState(
-            RequestHeaders httpRequestHeaders,
-            DateTimeOffset? lastModified,
-            EntityTagHeaderValue? etag,
-            ILogger logger)
-        {
-            var ifMatchState = PreconditionState.Unspecified;
-            var ifNoneMatchState = PreconditionState.Unspecified;
-            var ifModifiedSinceState = PreconditionState.Unspecified;
-            var ifUnmodifiedSinceState = PreconditionState.Unspecified;
-
-            // 14.24 If-Match
-            var ifMatch = httpRequestHeaders.IfMatch;
-            if (etag != null)
-            {
-                ifMatchState = GetEtagMatchState(
-                    useStrongComparison: true,
-                    etagHeader: ifMatch,
-                    etag: etag,
-                    matchFoundState: PreconditionState.ShouldProcess,
-                    matchNotFoundState: PreconditionState.PreconditionFailed);
-
-                if (ifMatchState == PreconditionState.PreconditionFailed)
-                {
-                    logger.IfMatchPreconditionFailed(etag);
-                }
-            }
-
-            // 14.26 If-None-Match
-            var ifNoneMatch = httpRequestHeaders.IfNoneMatch;
-            if (etag != null)
-            {
-                ifNoneMatchState = GetEtagMatchState(
-                    useStrongComparison: false,
-                    etagHeader: ifNoneMatch,
-                    etag: etag,
-                    matchFoundState: PreconditionState.NotModified,
-                    matchNotFoundState: PreconditionState.ShouldProcess);
-            }
-
-            var now = RoundDownToWholeSeconds(DateTimeOffset.UtcNow);
-
-            // 14.25 If-Modified-Since
-            var ifModifiedSince = httpRequestHeaders.IfModifiedSince;
-            if (lastModified.HasValue && ifModifiedSince.HasValue && ifModifiedSince <= now)
-            {
-                var modified = ifModifiedSince < lastModified;
-                ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified;
-            }
-
-            // 14.28 If-Unmodified-Since
-            var ifUnmodifiedSince = httpRequestHeaders.IfUnmodifiedSince;
-            if (lastModified.HasValue && ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now)
-            {
-                var unmodified = ifUnmodifiedSince >= lastModified;
-                ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed;
-
-                if (ifUnmodifiedSinceState == PreconditionState.PreconditionFailed)
-                {
-                    logger.IfUnmodifiedSincePreconditionFailed(lastModified, ifUnmodifiedSince);
-                }
-            }
-
-            var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState);
-            return state;
-        }
-
-        private static PreconditionState GetEtagMatchState(
-            bool useStrongComparison,
-            IList<EntityTagHeaderValue> etagHeader,
-            EntityTagHeaderValue etag,
-            PreconditionState matchFoundState,
-            PreconditionState matchNotFoundState)
-        {
-            if (etagHeader?.Count > 0)
+            var fileResultInfo = new FileResultInfo
             {
-                var state = matchNotFoundState;
-                foreach (var entityTag in etagHeader)
-                {
-                    if (entityTag.Equals(EntityTagHeaderValue.Any) || entityTag.Compare(etag, useStrongComparison))
-                    {
-                        state = matchFoundState;
-                        break;
-                    }
-                }
-
-                return state;
-            }
+                ContentType = result.ContentType,
+                EnableRangeProcessing = result.EnableRangeProcessing,
+                EntityTag = result.EntityTag,
+                FileDownloadName = result.FileDownloadName,
+                LastModified = result.LastModified,
+            };
 
-            return PreconditionState.Unspecified;
-        }
-
-        private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states)
-        {
-            var max = PreconditionState.Unspecified;
-            for (var i = 0; i < states.Length; i++)
-            {
-                if (states[i] > max)
-                {
-                    max = states[i];
-                }
-            }
-
-            return max;
-        }
-
-        private static (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetRangeHeaders(
-            HttpContext httpContext,
-            RequestHeaders httpRequestHeaders,
-            long fileLength,
-            ILogger logger)
-        {
-            var response = httpContext.Response;
-            var httpResponseHeaders = response.GetTypedHeaders();
-            var serveBody = !HttpMethods.IsHead(httpContext.Request.Method);
-
-            // Range may be null for empty range header, invalid ranges, parsing errors, multiple ranges 
-            // and when the file length is zero.
-            var (isRangeRequest, range) = RangeHelper.ParseRange(
-                httpContext,
-                httpRequestHeaders,
-                fileLength,
-                logger);
-
-            if (!isRangeRequest)
-            {
-                return (range: null, rangeLength: 0, serveBody);
-            }
-
-            // Requested range is not satisfiable
-            if (range == null)
-            {
-                // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable)
-                // SHOULD include a Content-Range field with a byte-range-resp-spec of "*". The instance-length specifies
-                // the current length of the selected resource.  e.g. */length
-                response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
-                httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(fileLength);
-                response.ContentLength = 0;
-
-                return (range: null, rangeLength: 0, serveBody: false);
-            }
-
-            response.StatusCode = StatusCodes.Status206PartialContent;
-            httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(
-                range.From!.Value,
-                range.To!.Value,
-                fileLength);
-
-            // Overwrite the Content-Length header for valid range requests with the range length.
-            var rangeLength = SetContentLength(response, range);
-
-            return (range, rangeLength, serveBody);
-        }
-
-        private static long SetContentLength(HttpResponse response, RangeItemHeaderValue range)
-        {
-            var start = range.From!.Value;
-            var end = range.To!.Value;
-            var length = end - start + 1;
-            response.ContentLength = length;
-            return length;
+            return FileResultHelper.SetHeadersAndLog(context.HttpContext, fileResultInfo, fileLength, enableRangeProcessing, lastModified, etag, Logger);
         }
 
         /// <summary>
@@ -420,39 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
         /// <returns>The async task.</returns>
         protected static async Task WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue? range, long rangeLength)
         {
-            await WriteFileAsyncInternal(context, fileStream, range, rangeLength);
-        }
-
-        internal static async Task WriteFileAsyncInternal(HttpContext context, Stream fileStream, RangeItemHeaderValue? range, long rangeLength)
-        {
-            var outputStream = context.Response.Body;
-            using (fileStream)
-            {
-                try
-                {
-                    if (range == null)
-                    {
-                        await StreamCopyOperation.CopyToAsync(fileStream, outputStream, count: null, bufferSize: BufferSize, cancel: context.RequestAborted);
-                    }
-                    else
-                    {
-                        fileStream.Seek(range.From!.Value, SeekOrigin.Begin);
-                        await StreamCopyOperation.CopyToAsync(fileStream, outputStream, rangeLength, BufferSize, context.RequestAborted);
-                    }
-                }
-                catch (OperationCanceledException)
-                {
-                    // Don't throw this exception, it's most likely caused by the client disconnecting.
-                    // However, if it was cancelled for any other reason we need to prevent empty responses.
-                    context.Abort();
-                }
-            }
-        }
-
-        private static DateTimeOffset RoundDownToWholeSeconds(DateTimeOffset dateTimeOffset)
-        {
-            var ticksToRemove = dateTimeOffset.Ticks % TimeSpan.TicksPerSecond;
-            return dateTimeOffset.Subtract(TimeSpan.FromTicks(ticksToRemove));
+            await FileResultHelper.WriteFileAsync(context, fileStream, range, rangeLength);
         }
     }
 }

+ 3 - 1
src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs

@@ -7,6 +7,7 @@ using System.Runtime.ExceptionServices;
 using System.Text;
 using System.Text.Json;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Mvc.Core;
 using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Extensions.Logging;
@@ -55,7 +56,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
             ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
                 result.ContentType,
                 response.ContentType,
-                DefaultContentType,
+                (DefaultContentType, Encoding.UTF8),
+                MediaType.GetEncoding,
                 out var resolvedContentType,
                 out var resolvedContentTypeEncoding);
 

+ 1 - 12
src/Mvc/Mvc.Core/src/JsonResult.cs

@@ -4,7 +4,6 @@
 using System;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.Extensions.DependencyInjection;
 
@@ -13,7 +12,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// <summary>
     /// An action result which formats the given object as JSON.
     /// </summary>
-    public class JsonResult : ActionResult, IResult, IStatusCodeActionResult
+    public class JsonResult : ActionResult, IStatusCodeActionResult
     {
         /// <summary>
         /// Creates a new <see cref="JsonResult"/> with the given <paramref name="value"/>.
@@ -81,15 +80,5 @@ namespace Microsoft.AspNetCore.Mvc
             var executor = services.GetRequiredService<IActionResultExecutor<JsonResult>>();
             return executor.ExecuteAsync(context, this);
         }
-
-        /// <summary>
-        /// Write the result as JSON to the HTTP response.
-        /// </summary>
-        /// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
-        /// <returns>A task that represents the asynchronous execute operation.</returns>
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            return httpContext.Response.WriteAsJsonAsync(Value);
-        }
     }
 }

+ 2 - 0
src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj

@@ -26,6 +26,8 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
     <Compile Include="$(SharedSourceRoot)PropertyHelper\**\*.cs" />
     <Compile Include="$(SharedSourceRoot)RangeHelper\**\*.cs" />
     <Compile Include="$(SharedSourceRoot)SecurityHelper\**\*.cs" />
+    <Compile Include="$(SharedSourceRoot)ResponseContentTypeHelper.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)ResultsHelpers\*.cs" LinkBase="Shared" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 1
src/Mvc/Mvc.Core/src/MvcCoreLoggerExtensions.cs

@@ -322,7 +322,7 @@ namespace Microsoft.AspNetCore.Mvc
             _httpStatusCodeResultExecuting = LoggerMessage.Define<int>(
                 LogLevel.Information,
                 new EventId(1, "HttpStatusCodeResultExecuting"),
-                "Executing HttpStatusCodeResult, setting HTTP status code {StatusCode}");
+                "Executing StatusCodeResult, setting HTTP status code {StatusCode}");
 
             _localRedirectResultExecuting = LoggerMessage.Define<string>(
                 LogLevel.Information,

+ 1 - 51
src/Mvc/Mvc.Core/src/PhysicalFileResult.cs

@@ -3,13 +3,9 @@
 
 using System;
 using System.Diagnostics.CodeAnalysis;
-using System.IO;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Core;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -18,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// A <see cref="FileResult"/> on execution will write a file from disk to the response
     /// using mechanisms provided by the host.
     /// </summary>
-    public class PhysicalFileResult : FileResult, IResult
+    public class PhysicalFileResult : FileResult
     {
         private string _fileName;
 
@@ -70,51 +66,5 @@ namespace Microsoft.AspNetCore.Mvc
             var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<PhysicalFileResult>>();
             return executor.ExecuteAsync(context, this);
         }
-
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            if (httpContext == null)
-            {
-                throw new ArgumentNullException(nameof(httpContext));
-            }
-
-            var fileInfo = new FileInfo(FileName);
-            if (!fileInfo.Exists)
-            {
-                throw new FileNotFoundException(
-                    Resources.FormatFileResult_InvalidPath(FileName), FileName);
-            }
-
-            return ExecuteAsyncInternal(httpContext, this, fileInfo.LastWriteTimeUtc, fileInfo.Length);
-        }
-
-        internal static Task ExecuteAsyncInternal(
-            HttpContext httpContext,
-            PhysicalFileResult result,
-            DateTimeOffset fileLastModified,
-            long fileLength)
-        {
-            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
-            var logger = loggerFactory.CreateLogger<RedirectResult>();
-
-            logger.ExecutingFileResult(result, result.FileName);
-
-            var lastModified = result.LastModified ?? fileLastModified;
-            var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog(
-                httpContext,
-                result,
-                fileLength,
-                result.EnableRangeProcessing,
-                lastModified,
-                result.EntityTag,
-                logger);
-
-            if (serveBody)
-            {
-                return PhysicalFileResultExecutor.WriteFileAsyncInternal(httpContext, result, range, rangeLength, logger);
-            }
-
-            return Task.CompletedTask;
-        }
     }
 }

+ 1 - 32
src/Mvc/Mvc.Core/src/RedirectResult.cs

@@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// An <see cref="ActionResult"/> that returns a Found (302), Moved Permanently (301), Temporary Redirect (307),
     /// or Permanent Redirect (308) response with a Location header to the supplied URL.
     /// </summary>
-    public class RedirectResult : ActionResult, IResult, IKeepTempDataResult
+    public class RedirectResult : ActionResult, IKeepTempDataResult
     {
         private string _url;
 
@@ -115,36 +115,5 @@ namespace Microsoft.AspNetCore.Mvc
             var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<RedirectResult>>();
             return executor.ExecuteAsync(context, this);
         }
-
-        /// <inheritdoc />
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            if (httpContext == null)
-            {
-                throw new ArgumentNullException(nameof(httpContext));
-            }
-
-            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
-            var logger = loggerFactory.CreateLogger<RedirectResult>();
-
-            // IsLocalUrl is called to handle URLs starting with '~/'.
-            var destinationUrl = UrlHelperBase.CheckIsLocalUrl(_url) ? UrlHelperBase.Content(httpContext, _url) : _url;
-
-            logger.RedirectResultExecuting(destinationUrl);
-
-            if (PreserveMethod)
-            {
-                httpContext.Response.StatusCode = Permanent
-                    ? StatusCodes.Status308PermanentRedirect
-                    : StatusCodes.Status307TemporaryRedirect;
-                httpContext.Response.Headers.Location = destinationUrl;
-            }
-            else
-            {
-                httpContext.Response.Redirect(destinationUrl, Permanent);
-            }
-
-            return Task.CompletedTask;
-        }
     }
 }

+ 1 - 1
src/Mvc/Mvc.Core/src/Resources.resx

@@ -237,7 +237,7 @@
     <comment>0 is the newline - 1 is a newline separate list of action display names</comment>
   </data>
   <data name="FileResult_InvalidPath" xml:space="preserve">
-    <value>Could not find file: {0}</value>
+    <value>Could not find file: {0}.</value>
     <comment>{0} is the value for the provided path</comment>
   </data>
   <data name="SerializableError_DefaultError" xml:space="preserve">

+ 2 - 14
src/Mvc/Mvc.Core/src/SignInResult.cs

@@ -5,8 +5,6 @@ using System;
 using System.Security.Claims;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Core;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
@@ -15,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// <summary>
     /// An <see cref="ActionResult"/> that on execution invokes <see cref="M:HttpContext.SignInAsync"/>.
     /// </summary>
-    public class SignInResult : ActionResult, IResult
+    public class SignInResult : ActionResult
     {
         /// <summary>
         /// Initializes a new instance of <see cref="SignInResult"/> with the
@@ -86,17 +84,7 @@ namespace Microsoft.AspNetCore.Mvc
                 throw new ArgumentNullException(nameof(context));
             }
 
-            return ExecuteAsync(context.HttpContext);
-        }
-
-        /// <inheritdoc />
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            return ExecuteAsync(httpContext);
-        }
-
-        private Task ExecuteAsync(HttpContext httpContext)
-        {
+            var httpContext = context.HttpContext;
             var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
             var logger = loggerFactory.CreateLogger<SignInResult>();
 

+ 1 - 38
src/Mvc/Mvc.Core/src/VirtualFileResult.cs

@@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
     /// A <see cref="FileResult" /> that on execution writes the file specified using a virtual path to the response
     /// using mechanisms provided by the host.
     /// </summary>
-    public class VirtualFileResult : FileResult, IResult
+    public class VirtualFileResult : FileResult
     {
         private string _fileName;
 
@@ -74,42 +74,5 @@ namespace Microsoft.AspNetCore.Mvc
             var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<VirtualFileResult>>();
             return executor.ExecuteAsync(context, this);
         }
-
-        Task IResult.ExecuteAsync(HttpContext httpContext)
-        {
-            if (httpContext == null)
-            {
-                throw new ArgumentNullException(nameof(httpContext));
-            }
-
-            var hostingEnvironment = httpContext.RequestServices.GetRequiredService<IWebHostEnvironment>();
-
-            var fileInfo = VirtualFileResultExecutor.GetFileInformation(this, hostingEnvironment);
-            if (!fileInfo.Exists)
-            {
-                throw new FileNotFoundException(
-                    Resources.FormatFileResult_InvalidPath(FileName), FileName);
-            }
-
-            var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
-            var logger = loggerFactory.CreateLogger<RedirectResult>();
-
-            var lastModified = LastModified ?? fileInfo.LastModified;
-            var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog(
-                httpContext,
-                this,
-                fileInfo.Length,
-                EnableRangeProcessing,
-                lastModified,
-                EntityTag,
-                logger);
-
-            if (serveBody)
-            {
-                return VirtualFileResultExecutor.WriteFileAsyncInternal(httpContext, fileInfo, range, rangeLength, logger);
-            }
-
-            return Task.CompletedTask;
-        }
     }
 }

+ 2 - 2
src/Mvc/Mvc.Core/test/AcceptedResultTests.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
@@ -14,7 +14,7 @@ using Microsoft.Extensions.Options;
 using Moq;
 using Xunit;
 
-namespace Microsoft.AspNetCore.Mvc.Core.Test
+namespace Microsoft.AspNetCore.Mvc
 {
     public class AcceptedResultTests
     {

+ 0 - 37
src/Mvc/Mvc.Core/test/ChallengeResultTest.cs

@@ -67,43 +67,6 @@ namespace Microsoft.AspNetCore.Mvc
             auth.Verify(c => c.ChallengeAsync(httpContext.Object, null, null), Times.Exactly(1));
         }
 
-        [Fact]
-        public async Task ChallengeResult_ExecuteAsync()
-        {
-            // Arrange
-            var result = new ChallengeResult("", null);
-
-            var auth = new Mock<IAuthenticationService>();
-
-            var httpContext = new Mock<HttpContext>();
-            httpContext.SetupGet(c => c.RequestServices)
-                .Returns(CreateServices().AddSingleton(auth.Object).BuildServiceProvider());
-
-            // Act
-            await ((IResult)result).ExecuteAsync(httpContext.Object);
-
-            // Assert
-            auth.Verify(c => c.ChallengeAsync(httpContext.Object, "", null), Times.Exactly(1));
-        }
-
-        [Fact]
-        public async Task ChallengeResult_ExecuteAsync_NoSchemes()
-        {
-            // Arrange
-            var result = new ChallengeResult(new string[] { }, null);
-
-            var auth = new Mock<IAuthenticationService>();
-            var httpContext = new Mock<HttpContext>();
-            httpContext.SetupGet(c => c.RequestServices)
-                .Returns(CreateServices().AddSingleton(auth.Object).BuildServiceProvider());
-
-            // Act
-            await ((IResult)result).ExecuteAsync(httpContext.Object);
-
-            // Assert
-            auth.Verify(c => c.ChallengeAsync(httpContext.Object, null, null), Times.Exactly(1));
-        }
-
         private static IServiceCollection CreateServices()
         {
             var services = new ServiceCollection();

+ 0 - 77
src/Mvc/Mvc.Core/test/ContentResultTest.cs

@@ -45,28 +45,6 @@ namespace Microsoft.AspNetCore.Mvc
             MediaTypeAssert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType);
         }
 
-        [Fact]
-        public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding()
-        {
-            // Arrange
-            var contentResult = new ContentResult
-            {
-                Content = null,
-                ContentType = new MediaTypeHeaderValue("text/plain")
-                {
-                    Encoding = Encoding.Unicode
-                }.ToString()
-            };
-            var httpContext = GetHttpContext();
-            var actionContext = GetActionContext(httpContext);
-
-            // Act
-            await ((IResult)contentResult).ExecuteAsync(httpContext);
-
-            // Assert
-            MediaTypeAssert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType);
-        }
-
         public static TheoryData<MediaTypeHeaderValue, string, string, string, byte[]> ContentResultContentTypeData
         {
             get
@@ -165,36 +143,6 @@ namespace Microsoft.AspNetCore.Mvc
             Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength);
         }
 
-        [Theory]
-        [MemberData(nameof(ContentResultContentTypeData))]
-        public async Task ContentResult_ExecuteAsync_SetContentTypeAndEncoding_OnResponse(
-           MediaTypeHeaderValue contentType,
-           string content,
-           string responseContentType,
-           string expectedContentType,
-           byte[] expectedContentData)
-        {
-            // Arrange
-            var contentResult = new ContentResult
-            {
-                Content = content,
-                ContentType = contentType?.ToString()
-            };
-            var httpContext = GetHttpContext();
-            var memoryStream = new MemoryStream();
-            httpContext.Response.Body = memoryStream;
-            httpContext.Response.ContentType = responseContentType;
-
-            // Act
-            await ((IResult)contentResult).ExecuteAsync(httpContext);
-
-            // Assert
-            var finalResponseContentType = httpContext.Response.ContentType;
-            Assert.Equal(expectedContentType, finalResponseContentType);
-            Assert.Equal(expectedContentData, memoryStream.ToArray());
-            Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength);
-        }
-
         public static TheoryData<string, string> ContentResult_WritesDataCorrectly_ForDifferentContentSizesData
         {
             get
@@ -298,31 +246,6 @@ namespace Microsoft.AspNetCore.Mvc
             Assert.Equal(content, actualContent);
         }
 
-        [Theory]
-        [MemberData(nameof(ContentResult_WritesDataCorrectly_ForDifferentContentSizesData))]
-        public async Task ContentResult_ExecuteAsync_WritesDataCorrectly_ForDifferentContentSizes(string content, string contentType)
-        {
-            // Arrange
-            var contentResult = new ContentResult
-            {
-                Content = content,
-                ContentType = contentType
-            };
-            var httpContext = GetHttpContext();
-            var memoryStream = new MemoryStream();
-            httpContext.Response.Body = memoryStream;
-            var encoding = MediaTypeHeaderValue.Parse(contentType).Encoding;
-
-            // Act
-            await ((IResult)contentResult).ExecuteAsync(httpContext);
-
-            // Assert
-            memoryStream.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(memoryStream, encoding);
-            var actualContent = await streamReader.ReadToEndAsync();
-            Assert.Equal(content, actualContent);
-        }
-
         private static ActionContext GetActionContext(HttpContext httpContext)
         {
             var routeData = new RouteData();

+ 1 - 2
src/Mvc/Mvc.Core/test/ControllerBaseTest.cs

@@ -10,7 +10,6 @@ using System.Reflection;
 using System.Text;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Abstractions;
 using Microsoft.AspNetCore.Mvc.Controllers;
 using Microsoft.AspNetCore.Mvc.DataAnnotations;
 using Microsoft.AspNetCore.Mvc.Formatters;
@@ -27,7 +26,7 @@ using Microsoft.Net.Http.Headers;
 using Moq;
 using Xunit;
 
-namespace Microsoft.AspNetCore.Mvc.Core.Test
+namespace Microsoft.AspNetCore.Mvc
 {
     public class ControllerBaseTest
     {

+ 0 - 155
src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs

@@ -1,155 +0,0 @@
-// 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;
-using System.Threading.Tasks;
-using Microsoft.Net.Http.Headers;
-using Xunit;
-
-namespace Microsoft.AspNetCore.Mvc
-{
-    public class FileContentActionResultTest
-    {
-        [Fact]
-        public void Constructor_SetsFileContents()
-        {
-            // Arrange
-            var fileContents = new byte[0];
-
-            // Act
-            var result = new FileContentResult(fileContents, "text/plain");
-
-            // Assert
-            Assert.Same(fileContents, result.FileContents);
-        }
-
-        [Fact]
-        public void Constructor_SetsContentTypeAndParameters()
-        {
-            // Arrange
-            var fileContents = new byte[0];
-            var contentType = "text/plain; charset=us-ascii; p1=p1-value";
-            var expectedMediaType = contentType;
-
-            // Act
-            var result = new FileContentResult(fileContents, contentType);
-
-            // Assert
-            Assert.Same(fileContents, result.FileContents);
-            MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
-        }
-
-        [Fact]
-        public void Constructor_SetsLastModifiedAndEtag()
-        {
-            // Arrange
-            var fileContents = new byte[0];
-            var contentType = "text/plain";
-            var expectedMediaType = contentType;
-            var lastModified = new DateTimeOffset();
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-
-            // Act
-            var result = new FileContentResult(fileContents, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag
-            };
-
-            // Assert
-            Assert.Equal(lastModified, result.LastModified);
-            Assert.Equal(entityTag, result.EntityTag);
-            MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_CopiesBuffer_ToOutputStream(action);
-        }
-
-        [Theory]
-        [InlineData(0, 4, "Hello", 5)]
-        [InlineData(6, 10, "World", 5)]
-        [InlineData(null, 5, "World", 5)]
-        [InlineData(6, null, "World", 5)]
-        public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest
-                .WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(start, end, expectedString, contentLength, action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest()
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored()
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored()
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored(action);
-        }
-
-        [Theory]
-        [InlineData("0-5")]
-        [InlineData("bytes = ")]
-        [InlineData("bytes = 1-4, 5-11")]
-        public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(rangeString, action);
-        }
-
-        [Theory]
-        [InlineData("bytes = 12-13")]
-        [InlineData("bytes = -0")]
-        public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored()
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_PreconditionFailed_RangeRequestedIgnored(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action);
-        }
-
-        [Fact]
-        public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
-        {
-            var action = new Func<FileContentResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BaseFileContentResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
-        }
-    }
-}

+ 0 - 101
src/Mvc/Mvc.Core/test/FileContentResult.cs

@@ -1,101 +0,0 @@
-// 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;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Xunit;
-
-namespace Microsoft.AspNetCore.Mvc
-{
-    public class FileContentResultTest
-    {
-        [Fact]
-        public async Task WriteFileAsync_CopiesBuffer_ToOutputStream()
-        {var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-            await BaseFileContentResultTest.WriteFileAsync_CopiesBuffer_ToOutputStream(action);
-        }
-
-        [Theory]
-        [InlineData(0, 4, "Hello", 5)]
-        [InlineData(6, 10, "World", 5)]
-        [InlineData(null, 5, "World", 5)]
-        [InlineData(6, null, "World", 5)]
-        public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest
-                .WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(start, end, expectedString, contentLength, action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest()
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored()
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored()
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored(action);
-        }
-
-        [Theory]
-        [InlineData("0-5")]
-        [InlineData("bytes = ")]
-        [InlineData("bytes = 1-4, 5-11")]
-        public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(rangeString, action);
-        }
-
-        [Theory]
-        [InlineData("bytes = 12-13")]
-        [InlineData("bytes = -0")]
-        public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored()
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_PreconditionFailed_RangeRequestedIgnored(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action);
-        }
-
-        [Fact]
-        public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
-        {
-            var action = new Func<FileContentResult, HttpContext, Task>(async (result, context) => await ((IResult)result).ExecuteAsync(context));
-
-            await BaseFileContentResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
-        }
-    }
-}

+ 44 - 0
src/Mvc/Mvc.Core/test/FileContentResultTest.cs

@@ -0,0 +1,44 @@
+// 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;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+    public class FileContentResultTest : FileContentResultTestBase
+    {
+        protected override Task ExecuteAsync(
+            HttpContext httpContext,
+            byte[] buffer,
+            string contentType,
+            DateTimeOffset? lastModified = null,
+            EntityTagHeaderValue entityTag = null,
+            bool enableRangeProcessing = false)
+        {
+            var result = new FileContentResult(buffer, contentType)
+            {
+                EntityTag = entityTag,
+                LastModified = lastModified,
+                EnableRangeProcessing = enableRangeProcessing,
+            };
+
+            httpContext.RequestServices = new ServiceCollection()
+                .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
+                .AddSingleton<IActionResultExecutor<FileContentResult>, FileContentResultExecutor>()
+                .BuildServiceProvider();
+            var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+            return result.ExecuteResultAsync(context);
+        }
+    }
+}

+ 8 - 9
src/Mvc/Mvc.Core/test/FileResultTest.cs → src/Mvc/Mvc.Core/test/FileResultHelperTest.cs

@@ -5,15 +5,14 @@ using System;
 using System.IO;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Mvc.Abstractions;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Testing;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.Net.Http.Headers;
-using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc
@@ -270,14 +269,14 @@ namespace Microsoft.AspNetCore.Mvc
             actionContext.HttpContext = httpContext;
 
             // Act
-            var state = FileResultExecutorBase.GetPreconditionState(
+            var state = FileResultHelper.GetPreconditionState(
                 httpRequestHeaders,
                 lastModified,
                 etag,
                 NullLogger.Instance);
 
             // Assert
-            Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, state);
+            Assert.Equal(FileResultHelper.PreconditionState.ShouldProcess, state);
         }
 
         [Theory]
@@ -309,14 +308,14 @@ namespace Microsoft.AspNetCore.Mvc
             actionContext.HttpContext = httpContext;
 
             // Act
-            var state = FileResultExecutorBase.GetPreconditionState(
+            var state = FileResultHelper.GetPreconditionState(
                 httpRequestHeaders,
                 lastModified,
                 etag,
                 NullLogger.Instance);
 
             // Assert
-            Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, state);
+            Assert.Equal(FileResultHelper.PreconditionState.PreconditionFailed, state);
         }
 
         [Theory]
@@ -346,14 +345,14 @@ namespace Microsoft.AspNetCore.Mvc
             actionContext.HttpContext = httpContext;
 
             // Act
-            var state = FileResultExecutorBase.GetPreconditionState(
+            var state = FileResultHelper.GetPreconditionState(
                 httpRequestHeaders,
                 lastModified,
                 etag,
                 NullLogger.Instance);
 
             // Assert
-            Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state);
+            Assert.Equal(FileResultHelper.PreconditionState.NotModified, state);
         }
 
         [Theory]
@@ -374,7 +373,7 @@ namespace Microsoft.AspNetCore.Mvc
             actionContext.HttpContext = httpContext;
 
             // Act
-            var ifRangeIsValid = FileResultExecutorBase.IfRangeValid(
+            var ifRangeIsValid = FileResultHelper.IfRangeValid(
                 httpRequestHeaders,
                 lastModified,
                 etag,

+ 29 - 560
src/Mvc/Mvc.Core/test/FileStreamResultTest.cs

@@ -3,25 +3,44 @@
 
 using System;
 using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.Net.Http.Headers;
-using Moq;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Mvc
 {
-    public class FileStreamResultTest
+    public class FileStreamResultTest : FileStreamResultTestBase
     {
+        protected override Task ExecuteAsync(
+            HttpContext httpContext,
+            Stream stream,
+            string contentType,
+            DateTimeOffset? lastModified = null,
+            EntityTagHeaderValue entityTag = null,
+            bool enableRangeProcessing = false)
+        {
+            httpContext.RequestServices = new ServiceCollection()
+                .AddSingleton<ILoggerFactory, NullLoggerFactory>()
+                .AddSingleton<IActionResultExecutor<FileStreamResult>, FileStreamResultExecutor>()
+                .BuildServiceProvider();
+
+            var actionContext = new ActionContext(httpContext, new(), new());
+            var fileStreamResult = new FileStreamResult(stream, contentType)
+            {
+                LastModified = lastModified,
+                EntityTag = entityTag,
+                EnableRangeProcessing = enableRangeProcessing
+            };
+
+            return fileStreamResult.ExecuteResultAsync(actionContext);
+        }
+
         [Fact]
         public void Constructor_SetsFileName()
         {
@@ -48,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc
 
             // Assert
             Assert.Equal(stream, result.FileStream);
-            MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
+            Assert.Equal(expectedMediaType, result.ContentType);
         }
 
         [Fact]
@@ -71,558 +90,8 @@ namespace Microsoft.AspNetCore.Mvc
             // Assert
             Assert.Equal(lastModified, result.LastModified);
             Assert.Equal(entityTag, result.EntityTag);
-            MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
-        }
-
-        [Theory]
-        [InlineData(0, 4, "Hello", 5)]
-        [InlineData(6, 10, "World", 5)]
-        [InlineData(null, 5, "World", 5)]
-        [InlineData(6, null, "World", 5)]
-        public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
-        {
-            // Arrange
-            var contentType = "text/plain";
-            var lastModified = new DateTimeOffset();
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-            readStream.SetLength(11);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            requestHeaders.Range = new RangeHeaderValue(start, end);
-            requestHeaders.IfMatch = new[]
-            {
-                new EntityTagHeaderValue("\"Etag\""),
-            };
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            start = start ?? 11 - end;
-            end = start + contentLength - 1;
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length);
-            Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified);
-            Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
-            Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
-            Assert.Equal("bytes", httpResponse.Headers.AcceptRanges);
-            Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
-            Assert.Equal(contentLength, httpResponse.ContentLength);
-            Assert.Equal(expectedString, body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
-        {
-            // Arrange
-            var contentType = "text/plain";
-            var lastModified = DateTimeOffset.MinValue;
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-            readStream.SetLength(11);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            requestHeaders.IfMatch = new[]
-            {
-                new EntityTagHeaderValue("\"Etag\""),
-            };
-            requestHeaders.Range = new RangeHeaderValue(0, 4);
-            requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified);
-            Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
-            var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length);
-            Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
-            Assert.Equal("bytes", httpResponse.Headers.AcceptRanges);
-            Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
-            Assert.Equal(5, httpResponse.ContentLength);
-            Assert.Equal("Hello", body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored()
-        {
-            // Arrange
-            var contentType = "text/plain";
-            var lastModified = DateTimeOffset.MinValue;
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-            readStream.SetLength(11);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            requestHeaders.IfMatch = new[]
-            {
-                new EntityTagHeaderValue("\"Etag\""),
-            };
-            requestHeaders.Range = new RangeHeaderValue(0, 4);
-            requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
-            Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified);
-            Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
-            Assert.Equal("Hello World", body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
-        {
-            // Arrange
-            var contentType = "text/plain";
-            var lastModified = DateTimeOffset.MinValue.AddDays(1);
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-            readStream.SetLength(11);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            requestHeaders.IfMatch = new[]
-            {
-                new EntityTagHeaderValue("\"Etag\""),
-            };
-            requestHeaders.Range = new RangeHeaderValue(0, 4);
-            requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue);
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
-            Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified);
-            Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
-            Assert.Equal("Hello World", body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Theory]
-        [InlineData("0-5")]
-        [InlineData("bytes = ")]
-        [InlineData("bytes = 1-4, 5-11")]
-        public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString)
-        {
-            // Arrange            
-            var contentType = "text/plain";
-            var lastModified = new DateTimeOffset();
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            httpContext.Request.Headers.Range = rangeString;
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            Assert.Empty(httpResponse.Headers.ContentRange);
-            Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
-            Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified);
-            Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
-            Assert.Equal("Hello World", body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Theory]
-        [InlineData("bytes = 12-13")]
-        [InlineData("bytes = -0")]
-        public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString)
-        {
-            // Arrange            
-            var contentType = "text/plain";
-            var lastModified = new DateTimeOffset();
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            httpContext.Request.Headers.Range = rangeString;
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            var contentRange = new ContentRangeHeaderValue(byteArray.Length);
-            Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified);
-            Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
-            Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
-            Assert.Equal("bytes", httpResponse.Headers.AcceptRanges);
-            Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
-            Assert.Equal(0, httpResponse.ContentLength);
-            Assert.Empty(body);
-            Assert.False(readStream.CanSeek);
+            Assert.Equal(expectedMediaType, result.ContentType);
         }
 
-        [Fact]
-        public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
-        {
-            // Arrange
-            var contentType = "text/plain";
-            var lastModified = new DateTimeOffset();
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            requestHeaders.IfMatch = new[]
-            {
-                new EntityTagHeaderValue("\"NotEtag\""),
-            };
-            httpContext.Request.Headers.Range = "bytes = 0-6";
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
-            Assert.Null(httpResponse.ContentLength);
-            Assert.Empty(httpResponse.Headers.ContentRange);
-            Assert.NotEmpty(httpResponse.Headers.LastModified);
-            Assert.Empty(body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_NotModified_RangeRequestedIgnored()
-        {
-            // Arrange       
-            var contentType = "text/plain";
-            var lastModified = new DateTimeOffset();
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("Hello World");
-            var readStream = new MemoryStream(byteArray);
-
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            requestHeaders.IfNoneMatch = new[]
-            {
-                new EntityTagHeaderValue("\"Etag\""),
-            };
-            httpContext.Request.Headers.Range = "bytes = 0-6";
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = await streamReader.ReadToEndAsync();
-            Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
-            Assert.Null(httpResponse.ContentLength);
-            Assert.Empty(httpResponse.Headers.ContentRange);            
-            Assert.False(httpResponse.Headers.ContainsKey(HeaderNames.ContentType));
-            Assert.NotEmpty(httpResponse.Headers.LastModified);
-            Assert.Empty(body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Theory]
-        [InlineData(0)]
-        [InlineData(null)]
-        public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileLength)
-        {
-            // Arrange
-            var contentType = "text/plain";
-            var lastModified = new DateTimeOffset();
-            var entityTag = new EntityTagHeaderValue("\"Etag\"");
-            var byteArray = Encoding.ASCII.GetBytes("");
-            var readStream = new MemoryStream(byteArray);
-            fileLength = fileLength ?? 0L;
-            readStream.SetLength(fileLength.Value);
-            var result = new FileStreamResult(readStream, contentType)
-            {
-                LastModified = lastModified,
-                EntityTag = entityTag,
-                EnableRangeProcessing = true,
-            };
-
-            var httpContext = GetHttpContext();
-            var requestHeaders = httpContext.Request.GetTypedHeaders();
-            requestHeaders.Range = new RangeHeaderValue(0, 5);
-            requestHeaders.IfMatch = new[]
-            {
-                new EntityTagHeaderValue("\"Etag\""),
-            };
-            httpContext.Request.Method = HttpMethods.Get;
-            httpContext.Response.Body = new MemoryStream();
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var httpResponse = actionContext.HttpContext.Response;
-            httpResponse.Body.Seek(0, SeekOrigin.Begin);
-            var streamReader = new StreamReader(httpResponse.Body);
-            var body = streamReader.ReadToEndAsync().Result;
-            Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified);
-            Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag);
-            var contentRange = new ContentRangeHeaderValue(byteArray.Length);
-            Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode);
-            Assert.Equal("bytes", httpResponse.Headers.AcceptRanges);
-            Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange);
-            Assert.Equal(0, httpResponse.ContentLength);
-            Assert.Empty(body);
-            Assert.False(readStream.CanSeek);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes()
-        {
-            // Arrange
-            var mockReadStream = new Mock<Stream>();
-            mockReadStream.SetupSequence(s => s.ReadAsync(It.IsAny<byte[]>(), 0, 0x1000, CancellationToken.None))
-                .Returns(Task.FromResult(0x1000))
-                .Returns(Task.FromResult(0x500))
-                .Returns(Task.FromResult(0));
-
-            var mockBodyStream = new Mock<Stream>();
-            mockBodyStream
-                .Setup(s => s.WriteAsync(It.IsAny<byte[]>(), 0, 0x1000, CancellationToken.None))
-                .Returns(Task.FromResult(0));
-
-            mockBodyStream
-                .Setup(s => s.WriteAsync(It.IsAny<byte[]>(), 0, 0x500, CancellationToken.None))
-                .Returns(Task.FromResult(0));
-
-            var result = new FileStreamResult(mockReadStream.Object, "text/plain");
-
-            var httpContext = GetHttpContext();
-            httpContext.Response.Body = mockBodyStream.Object;
-
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            mockReadStream.Verify();
-            mockBodyStream.Verify();
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_CopiesProvidedStream_ToOutputStream()
-        {
-            // Arrange
-            // Generate an array of bytes with a predictable pattern
-            // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13
-            var originalBytes = Enumerable.Range(0, 0x1234)
-                .Select(b => (byte)(b % 20)).ToArray();
-
-            var originalStream = new MemoryStream(originalBytes);
-
-            var httpContext = GetHttpContext();
-            var outStream = new MemoryStream();
-            httpContext.Response.Body = outStream;
-
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            var result = new FileStreamResult(originalStream, "text/plain");
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var outBytes = outStream.ToArray();
-            Assert.True(originalBytes.SequenceEqual(outBytes));
-            Assert.False(originalStream.CanSeek);
-        }
-
-        [Fact]
-        public async Task SetsSuppliedContentTypeAndEncoding()
-        {
-            // Arrange
-            var expectedContentType = "text/foo; charset=us-ascii";
-            // Generate an array of bytes with a predictable pattern
-            // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13
-            var originalBytes = Enumerable.Range(0, 0x1234)
-                .Select(b => (byte)(b % 20)).ToArray();
-
-            var originalStream = new MemoryStream(originalBytes);
-
-            var httpContext = GetHttpContext();
-            var outStream = new MemoryStream();
-            httpContext.Response.Body = outStream;
-
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            var result = new FileStreamResult(originalStream, expectedContentType);
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            var outBytes = outStream.ToArray();
-            Assert.True(originalBytes.SequenceEqual(outBytes));
-            Assert.Equal(expectedContentType, httpContext.Response.ContentType);
-            Assert.False(originalStream.CanSeek);
-        }
-
-        [Fact]
-        public async Task HeadRequest_DoesNotWriteToBody_AndClosesReadStream()
-        {
-            // Arrange
-            var readStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello, World!"));
-
-            var httpContext = GetHttpContext();
-            httpContext.Request.Method = "HEAD";
-            var outStream = new MemoryStream();
-            httpContext.Response.Body = outStream;
-
-            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
-
-            var result = new FileStreamResult(readStream, "text/plain");
-
-            // Act
-            await result.ExecuteResultAsync(actionContext);
-
-            // Assert
-            Assert.False(readStream.CanSeek);
-            Assert.Equal(200, httpContext.Response.StatusCode);
-            Assert.Equal(0, httpContext.Response.Body.Length);
-        }
-
-        private static IServiceCollection CreateServices()
-        {
-            var services = new ServiceCollection();
-            services.AddSingleton<IActionResultExecutor<FileStreamResult>, FileStreamResultExecutor>();
-            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
-            return services;
-        }
-
-        private static HttpContext GetHttpContext()
-        {
-            var services = CreateServices();
-
-            var httpContext = new DefaultHttpContext();
-            httpContext.RequestServices = services.BuildServiceProvider();
-            return httpContext;
-        }
     }
-}
+}

+ 6 - 3
src/Mvc/Mvc.Core/test/Formatters/ResponseContentTypeHelperTest.cs

@@ -2,10 +2,11 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System.Text;
+using Microsoft.AspNetCore.Mvc.Formatters;
 using Microsoft.Net.Http.Headers;
 using Xunit;
 
-namespace Microsoft.AspNetCore.Mvc.Formatters
+namespace Microsoft.AspNetCore.Internal
 {
     public class ResponseContentTypeHelperTest
     {
@@ -108,7 +109,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
             ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
                 contentType?.ToString(),
                 responseContentType,
-                defaultContentType,
+                (defaultContentType, Encoding.UTF8),
+                MediaType.GetEncoding,
                 out var resolvedContentType,
                 out var resolvedContentTypeEncoding);
 
@@ -127,7 +129,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
             ResponseContentTypeHelper.ResolveContentTypeAndEncoding(
                 null,
                 expectedContentType,
-                defaultContentType,
+                (defaultContentType, Encoding.UTF8),
+                MediaType.GetEncoding,
                 out var resolvedContentType,
                 out var resolvedContentTypeEncoding);
 

+ 1 - 0
src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj

@@ -15,5 +15,6 @@
     <ProjectReference Include="..\..\shared\Mvc.TestDiagnosticListener\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj" />
 
     <Reference Include="Microsoft.AspNetCore.ResponseCaching" />
+    <Compile Include="$(SharedSourceRoot)ResultsTests\*.cs" LinkBase="Shared" />
   </ItemGroup>
 </Project>

+ 0 - 204
src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs

@@ -1,204 +0,0 @@
-// 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;
-using System.IO;
-using System.Threading.Tasks;
-using Xunit;
-
-namespace Microsoft.AspNetCore.Mvc
-{
-    public class PhysicalFileActionResultTest
-    {
-        [Fact]
-        public void Constructor_SetsFileName()
-        {
-            // Arrange
-            var path = Path.GetFullPath("helllo.txt");
-
-            // Act
-            var result = new PhysicalFileResult(path, "text/plain");
-
-            // Assert
-            Assert.Equal(path, result.FileName);
-        }
-
-        [Fact]
-        public void Constructor_SetsContentTypeAndParameters()
-        {
-            // Arrange
-            var path = Path.GetFullPath("helllo.txt");
-            var contentType = "text/plain; charset=us-ascii; p1=p1-value";
-            var expectedMediaType = contentType;
-
-            // Act
-            var result = new PhysicalFileResult(path, contentType);
-
-            // Assert
-            Assert.Equal(path, result.FileName);
-            MediaTypeAssert.Equal(expectedMediaType, result.ContentType);
-        }
-
-        [Theory]
-        [InlineData(0, 3, "File", 4)]
-        [InlineData(8, 13, "Result", 6)]
-        [InlineData(null, 5, "ts�", 5)]
-        [InlineData(8, null, "ResultTestFile contents�", 26)]
-        public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength)
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_WritesRangeRequested(
-                start,
-                end,
-                expectedString,
-                contentLength,
-                action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action);
-        }
-
-        [Theory]
-        [InlineData("0-5")]
-        [InlineData("bytes = ")]
-        [InlineData("bytes = 1-4, 5-11")]
-        public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString)
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action);
-        }
-
-        [Theory]
-        [InlineData("bytes = 35-36")]
-        [InlineData("bytes = -0")]
-        public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString)
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_RangeRequested_PreconditionFailed()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action);
-        }
-
-        [Fact]
-        public async Task WriteFileAsync_RangeRequested_NotModified()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_NotModified(action);
-        }
-
-        [Fact]
-        public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action);
-        }
-
-        [Theory]
-        [InlineData(0, 3, 4)]
-        [InlineData(8, 13, 6)]
-        [InlineData(null, 3, 3)]
-        [InlineData(8, null, 26)]
-        public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength)
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(start, end, contentLength, action);
-        }
-
-        [Fact]
-        public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action);
-        }
-
-        [Fact]
-        public async Task ExecuteResultAsync_WorksWithAbsolutePaths()
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.ExecuteResultAsync_WorksWithAbsolutePaths(action);
-        }
-
-        [Theory]
-        [InlineData("FilePathResultTestFile.txt")]
-        [InlineData("./FilePathResultTestFile.txt")]
-        [InlineData(".\\FilePathResultTestFile.txt")]
-        [InlineData("~/FilePathResultTestFile.txt")]
-        [InlineData("..\\TestFiles/FilePathResultTestFile.txt")]
-        [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")]
-        [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")]
-        [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")]
-        [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")]
-        [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")]
-        [InlineData("~/SubFolder/SubFolderTestFile.txt")]
-        [InlineData("~/SubFolder\\SubFolderTestFile.txt")]
-        public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path)
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            await BasePhysicalFileResultTest.ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(path, action);
-        }
-
-        [Theory]
-        [InlineData("/SubFolder/SubFolderTestFile.txt")]
-        [InlineData("\\SubFolder\\SubFolderTestFile.txt")]
-        [InlineData("/SubFolder\\SubFolderTestFile.txt")]
-        [InlineData("\\SubFolder/SubFolderTestFile.txt")]
-        [InlineData("./SubFolder/SubFolderTestFile.txt")]
-        [InlineData(".\\SubFolder\\SubFolderTestFile.txt")]
-        [InlineData("./SubFolder\\SubFolderTestFile.txt")]
-        [InlineData(".\\SubFolder/SubFolderTestFile.txt")]
-        public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path)
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            BasePhysicalFileResultTest
-                .ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(path, action);
-        }
-
-        [Theory]
-        [InlineData("/FilePathResultTestFile.txt")]
-        [InlineData("\\FilePathResultTestFile.txt")]
-        public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path)
-        {
-            var action = new Func<PhysicalFileResult, ActionContext, Task>(async (result, context) => await result.ExecuteResultAsync(context));
-
-            BasePhysicalFileResultTest
-                .ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(path, action);
-        }
-    }
-}

Some files were not shown because too many files changed in this diff