diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs index 5fc7b1257d..6adfa1738e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs @@ -469,7 +469,44 @@ namespace Microsoft.AspNetCore.Mvc string controllerName, object routeValues) { - return new RedirectToActionResult(actionName, controllerName, routeValues) + return RedirectToAction(actionName, controllerName, routeValues, fragment: null); + } + + /// + /// Redirects to the specified action using the specified , + /// , and . + /// + /// The name of the action. + /// The name of the controller. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToActionResult RedirectToAction( + string actionName, + string controllerName, + string fragment) + { + return RedirectToAction(actionName, controllerName, routeValues: null, fragment: fragment); + } + + /// + /// Redirects to the specified action using the specified , + /// , , + /// and . + /// + /// The name of the action. + /// The name of the controller. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToActionResult RedirectToAction( + string actionName, + string controllerName, + object routeValues, + string fragment) + { + return new RedirectToActionResult(actionName, controllerName, routeValues, fragment) { UrlHelper = Url, }; @@ -513,6 +550,24 @@ namespace Microsoft.AspNetCore.Mvc return RedirectToActionPermanent(actionName, controllerName, routeValues: null); } + /// + /// Redirects to the specified action with set to true + /// using the specified , + /// , and . + /// + /// The name of the action. + /// The name of the controller. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToActionResult RedirectToActionPermanent( + string actionName, + string controllerName, + string fragment) + { + return RedirectToActionPermanent(actionName, controllerName, routeValues: null, fragment: fragment); + } + /// /// Redirects to the specified action with set to true /// using the specified , , @@ -527,12 +582,33 @@ namespace Microsoft.AspNetCore.Mvc string actionName, string controllerName, object routeValues) + { + return RedirectToActionPermanent(actionName, controllerName, routeValues, fragment: null); + } + + /// + /// Redirects to the specified action with set to true + /// using the specified , , + /// , and . + /// + /// The name of the action. + /// The name of the controller. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToActionResult RedirectToActionPermanent( + string actionName, + string controllerName, + object routeValues, + string fragment) { return new RedirectToActionResult( actionName, controllerName, routeValues, - permanent: true) + permanent: true, + fragment: fragment) { UrlHelper = Url, }; @@ -570,7 +646,37 @@ namespace Microsoft.AspNetCore.Mvc [NonAction] public virtual RedirectToRouteResult RedirectToRoute(string routeName, object routeValues) { - return new RedirectToRouteResult(routeName, routeValues) + return RedirectToRoute(routeName, routeValues, fragment: null); + } + + /// + /// Redirects to the specified route using the specified + /// and . + /// + /// The name of the route. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToRouteResult RedirectToRoute(string routeName, string fragment) + { + return RedirectToRoute(routeName, routeValues: null, fragment: fragment); + } + + /// + /// Redirects to the specified route using the specified , + /// , and . + /// + /// The name of the route. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToRouteResult RedirectToRoute( + string routeName, + object routeValues, + string fragment) + { + return new RedirectToRouteResult(routeName, routeValues, fragment) { UrlHelper = Url, }; @@ -610,7 +716,38 @@ namespace Microsoft.AspNetCore.Mvc [NonAction] public virtual RedirectToRouteResult RedirectToRoutePermanent(string routeName, object routeValues) { - return new RedirectToRouteResult(routeName, routeValues, permanent: true) + return RedirectToRoutePermanent(routeName, routeValues, fragment: null); + } + + /// + /// Redirects to the specified route with set to true + /// using the specified and . + /// + /// The name of the route. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToRouteResult RedirectToRoutePermanent(string routeName, string fragment) + { + return RedirectToRoutePermanent(routeName, routeValues: null, fragment: fragment); + } + + /// + /// Redirects to the specified route with set to true + /// using the specified , , + /// and . + /// + /// The name of the route. + /// The parameters for a route. + /// The fragment to add to the URL. + /// The created for the response. + [NonAction] + public virtual RedirectToRouteResult RedirectToRoutePermanent( + string routeName, + object routeValues, + string fragment) + { + return new RedirectToRouteResult(routeName, routeValues, permanent: true, fragment: fragment) { UrlHelper = Url, }; @@ -1082,7 +1219,7 @@ namespace Microsoft.AspNetCore.Mvc /// /// Creates a object that produces an Accepted (202) response. /// - /// The name of the route to use for generating the URL. + /// The name of the route to use for generating the URL. /// The created for the response. [NonAction] public virtual AcceptedAtRouteResult AcceptedAtRoute(string routeName) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToActionResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToActionResultExecutor.cs index 81c613d443..119d54d4cd 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToActionResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToActionResultExecutor.cs @@ -33,7 +33,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal { var urlHelper = result.UrlHelper ?? _urlHelperFactory.GetUrlHelper(context); - var destinationUrl = urlHelper.Action(result.ActionName, result.ControllerName, result.RouteValues); + var destinationUrl = urlHelper.Action( + result.ActionName, + result.ControllerName, + result.RouteValues, + protocol: null, + host: null, + fragment: result.Fragment); if (string.IsNullOrEmpty(destinationUrl)) { throw new InvalidOperationException(Resources.NoRoutesMatched); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToRouteResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToRouteResultExecutor.cs index 4e49476875..758a639a95 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToRouteResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RedirectToRouteResultExecutor.cs @@ -33,7 +33,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal { var urlHelper = result.UrlHelper ?? _urlHelperFactory.GetUrlHelper(context); - var destinationUrl = urlHelper.RouteUrl(result.RouteName, result.RouteValues); + var destinationUrl = urlHelper.RouteUrl( + result.RouteName, + result.RouteValues, + protocol: null, + host: null, + fragment: result.Fragment); if (string.IsNullOrEmpty(destinationUrl)) { throw new InvalidOperationException(Resources.NoRoutesMatched); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/RedirectToActionResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/RedirectToActionResult.cs index 908dca4644..ae83e5601e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/RedirectToActionResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/RedirectToActionResult.cs @@ -9,8 +9,20 @@ using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc { + /// + /// An that returns a Found (302) + /// or Moved Permanently (301) response with a Location header. + /// Targets a controller action. + /// public class RedirectToActionResult : ActionResult, IKeepTempDataResult { + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The route data to use for generating the URL. public RedirectToActionResult( string actionName, string controllerName, @@ -19,16 +31,61 @@ namespace Microsoft.AspNetCore.Mvc { } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The route data to use for generating the URL. + /// The fragment to add to the URL. + public RedirectToActionResult( + string actionName, + string controllerName, + object routeValues, + string fragment) + : this(actionName, controllerName, routeValues, permanent: false, fragment: fragment) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The route data to use for generating the URL. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). public RedirectToActionResult( string actionName, string controllerName, object routeValues, bool permanent) + : this(actionName, controllerName, routeValues, permanent, fragment: null) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The route data to use for generating the URL. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + /// The fragment to add to the URL. + public RedirectToActionResult( + string actionName, + string controllerName, + object routeValues, + bool permanent, + string fragment) { ActionName = actionName; ControllerName = controllerName; RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); Permanent = permanent; + Fragment = fragment; } /// @@ -51,8 +108,16 @@ namespace Microsoft.AspNetCore.Mvc /// public RouteValueDictionary RouteValues { get; set; } + /// + /// Gets or sets an indication that the redirect is permanent. + /// public bool Permanent { get; set; } + /// + /// Gets or sets the fragment to add to the URL. + /// + public string Fragment { get; set; } + /// public override void ExecuteResult(ActionContext context) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/RedirectToRouteResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/RedirectToRouteResult.cs index ba0c4970c3..3241b13731 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/RedirectToRouteResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/RedirectToRouteResult.cs @@ -9,13 +9,29 @@ using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc { + /// + /// An that returns a Found (302) + /// or Moved Permanently (301) response with a Location header. + /// Targets a registered route. + /// public class RedirectToRouteResult : ActionResult, IKeepTempDataResult { + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The parameters for the route. public RedirectToRouteResult(object routeValues) : this(routeName: null, routeValues: routeValues) { } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. public RedirectToRouteResult( string routeName, object routeValues) @@ -23,14 +39,54 @@ namespace Microsoft.AspNetCore.Mvc { } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). public RedirectToRouteResult( string routeName, object routeValues, bool permanent) + : this(routeName, routeValues, permanent, fragment: null) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// The fragment to add to the URL. + public RedirectToRouteResult( + string routeName, + object routeValues, + string fragment) + : this(routeName, routeValues, permanent: false, fragment: fragment) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + /// The fragment to add to the URL. + public RedirectToRouteResult( + string routeName, + object routeValues, + bool permanent, + string fragment) { RouteName = routeName; RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); Permanent = permanent; + Fragment = fragment; } /// @@ -48,8 +104,16 @@ namespace Microsoft.AspNetCore.Mvc /// public RouteValueDictionary RouteValues { get; set; } + /// + /// Gets or sets an indication that the redirect is permanent. + /// public bool Permanent { get; set; } + /// + /// Gets or sets the fragment to add to the URL. + /// + public string Fragment { get; set; } + /// public override void ExecuteResult(ActionContext context) { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs index c2ea012596..f256c43bd5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs @@ -274,6 +274,30 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test Assert.Equal(expected, resultTemporary.RouteValues); } + [Theory] + [MemberData(nameof(RedirectTestData))] + public void RedirectToAction_WithParameterActionAndControllerAndRouteValuesAndFragment_SetsResultProperties( + object routeValues, + IEnumerable> expectedRouteValues) + { + // Arrange + var controller = new TestableController(); + var expectedAction = "Action"; + var expectedController = "Home"; + var expectedFragment = "test"; + + // Act + var result = controller.RedirectToAction("Action", "Home", routeValues, "test"); + + // Assert + Assert.IsType(result); + Assert.False(result.Permanent); + Assert.Equal(expectedAction, result.ActionName); + Assert.Equal(expectedRouteValues, result.RouteValues); + Assert.Equal(expectedController, result.ControllerName); + Assert.Equal(expectedFragment, result.Fragment); + } + [Theory] [MemberData(nameof(RedirectTestData))] public void RedirectToActionPermanent_WithParameterActionAndRouteValues_SetsResultProperties( @@ -293,6 +317,30 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test Assert.Equal(expected, resultPermanent.RouteValues); } + [Theory] + [MemberData(nameof(RedirectTestData))] + public void RedirectToActionPermanent_WithParameterActionAndControllerAndRouteValuesAndFragment_SetsResultProperties( + object routeValues, + IEnumerable> expectedRouteValues) + { + // Arrange + var controller = new TestableController(); + var expectedAction = "Action"; + var expectedController = "Home"; + var expectedFragment = "test"; + + // Act + var result = controller.RedirectToActionPermanent("Action", "Home", routeValues, "test"); + + // Assert + Assert.IsType(result); + Assert.True(result.Permanent); + Assert.Equal(expectedAction, result.ActionName); + Assert.Equal(expectedRouteValues, result.RouteValues); + Assert.Equal(expectedController, result.ControllerName); + Assert.Equal(expectedFragment, result.Fragment); + } + [Theory] [MemberData(nameof(RedirectTestData))] public void RedirectToRoute_WithParameterRouteValues_SetsResultEqualRouteValues( @@ -311,6 +359,28 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test Assert.Equal(expected, resultTemporary.RouteValues); } + [Theory] + [MemberData(nameof(RedirectTestData))] + public void RedirectToRoute_WithParameterRouteNameAndRouteValuesAndFragment_SetsResultProperties( + object routeValues, + IEnumerable> expectedRouteValues) + { + // Arrange + var controller = new TestableController(); + var expectedRoute = "TestRoute"; + var expectedFragment = "test"; + + // Act + var result = controller.RedirectToRoute("TestRoute", routeValues, "test"); + + // Assert + Assert.IsType(result); + Assert.False(result.Permanent); + Assert.Equal(expectedRoute, result.RouteName); + Assert.Equal(expectedRouteValues, result.RouteValues); + Assert.Equal(expectedFragment, result.Fragment); + } + [Theory] [MemberData(nameof(RedirectTestData))] public void RedirectToRoutePermanent_WithParameterRouteValues_SetsResultEqualRouteValuesAndPermanent( @@ -329,6 +399,28 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test Assert.Equal(expected, resultPermanent.RouteValues); } + [Theory] + [MemberData(nameof(RedirectTestData))] + public void RedirectToRoutePermanent_WithParameterRouteNameAndRouteValuesAndFragment_SetsResultProperties( + object routeValues, + IEnumerable> expectedRouteValues) + { + // Arrange + var controller = new TestableController(); + var expectedRoute = "TestRoute"; + var expectedFragment = "test"; + + // Act + var result = controller.RedirectToRoutePermanent("TestRoute", routeValues, "test"); + + // Assert + Assert.IsType(result); + Assert.True(result.Permanent); + Assert.Equal(expectedRoute, result.RouteName); + Assert.Equal(expectedRouteValues, result.RouteValues); + Assert.Equal(expectedFragment, result.Fragment); + } + [Fact] public void RedirectToRoute_WithParameterRouteName_SetsResultSameRouteName() { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToActionResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToActionResultTest.cs index a286a7e153..4652b13f73 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToActionResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToActionResultTest.cs @@ -83,6 +83,34 @@ namespace Microsoft.AspNetCore.Mvc "No route matches the supplied values."); } + [Fact] + public async Task RedirectToAction_Execute_WithFragment_PassesCorrectValuesToRedirect() + { + // Arrange + var expectedUrl = "/Home/SampleAction#test"; + var expectedStatusCode = StatusCodes.Status302Found; + + var httpContext = new DefaultHttpContext + { + RequestServices = CreateServices().BuildServiceProvider(), + }; + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var urlHelper = GetMockUrlHelper(expectedUrl); + var result = new RedirectToActionResult("SampleAction", "Home", null, false, "test") + { + UrlHelper = urlHelper, + }; + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + private static IUrlHelper GetMockUrlHelper(string returnValue) { var urlHelper = new Mock(); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToRouteResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToRouteResultTest.cs index e6a71e485b..a7b4075cfa 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToRouteResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToRouteResultTest.cs @@ -116,6 +116,31 @@ namespace Microsoft.AspNetCore.Mvc 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(); + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var urlHelper = GetMockUrlHelper(expectedUrl); + var result = new RedirectToRouteResult("Sample", null, true, "test") + { + UrlHelper = urlHelper, + }; + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + private static HttpContext GetHttpContext(IUrlHelperFactory factory = null) { var services = CreateServices(factory);