Skip to content

Instantly share code, notes, and snippets.

@mvalipour
Last active January 13, 2018 12:09
Show Gist options
  • Save mvalipour/fd5f4f97a4b3f6c3e6cc to your computer and use it in GitHub Desktop.
Save mvalipour/fd5f4f97a4b3f6c3e6cc to your computer and use it in GitHub Desktop.
ASP.NET MVC client-side `Redirect` methods and `ActionResult`

ASP.NET MVC client-side Redirect methods and ActionResult

A little background

When in need of redirecting the user on client-side (e.g. when in ajax actions), Most of the developers choose the easy way and just return a JavascriptResult('window.location = ...') and passing to it Url.Action(...).

But what is wrong with this?

  1. Testability: There are several ways that the above approach makes unit-testing difficult (if not impossible).

    • By using Url.Action(...) the code heavily depend on the routing and request url configurations that requires a lot of mocking during unit-testing.
    • You would have to assert against a literal string (i.e. window.location = ...) instead of a state of action result (i.e. what action you wish to redirect to).
  2. Breaking the MVC pattern: One of the building block of MVC is to seperate presentation (view) and controller; Altough they don't look like to be, both "building url" and "building script", are presentation concerns that in the solution mentioned above are included in the controller (wrong place).

How does this gist solve the problem?

Exactly in the same as ASP.NET MVC's built-in normal redirections works!

Instead of returning the javascript itself, we send out a specific type of ActionResult that renders the same javascript as before but now at the right time.

So...

  1. It is easily testable: because it is strongly typed and has state to assert againist (i.e. URL or Route Data)
  2. It perfectly matches the principles of MVC pattern

OK, help me understand the files...

/// <summary>
/// Contains extension methods for asp.net mvc controller class.
/// </summary>
public static class ControllerExtensions
{
#region Redirect to action
/// <summary>
/// Creates an action result that redirects to the given url on client side.
/// </summary>
/// <param name="control">
/// The control.
/// </param>
/// <param name="url">
/// The url.
/// </param>
/// <returns>
/// The <see cref="RedirectOnClientResult"/>.
/// </returns>
public static RedirectOnClientResult RedirectOnClient(this Controller control, string url)
{
return new RedirectOnClientResult(url);
}
#endregion
#region Redirect to action on client
/// <summary>
/// Creates an action result that redirects to the given route on client side.
/// </summary>
/// <param name="controller">
/// The controller.
/// </param>
/// <param name="actionName">
/// The action name.
/// </param>
/// <param name="controllerName">
/// The controller name.
/// </param>
/// <returns>
/// The <see cref="RedirectToRouteOnClientResult"/>.
/// </returns>
public static RedirectToRouteOnClientResult RedirectToActionOnClient(this Controller controller, string actionName, string controllerName = null)
{
return controller.RedirectToActionOnClient(actionName, controllerName, (object)null);
}
/// <summary>
/// Creates an action result that redirects to the given route on client side.
/// </summary>
/// <param name="controller">
/// The controller.
/// </param>
/// <param name="actionName">
/// The action name.
/// </param>
/// <param name="controllerName">
/// The controller name.
/// </param>
/// <param name="routeValues">
/// The route values.
/// </param>
/// <returns>
/// The <see cref="RedirectToRouteOnClientResult"/>.
/// </returns>
public static RedirectToRouteOnClientResult RedirectToActionOnClient(this Controller controller, string actionName, string controllerName, object routeValues)
{
return controller.RedirectToActionOnClient(actionName, controllerName, new RouteValueDictionary(routeValues));
}
/// <summary>
/// Creates an action result that redirects to the given route on client side.
/// </summary>
/// <param name="controller">
/// The controller.
/// </param>
/// <param name="actionName">
/// The action name.
/// </param>
/// <param name="controllerName">
/// The controller name.
/// </param>
/// <param name="routeValues">
/// The route values.
/// </param>
/// <returns>
/// The <see cref="RedirectToRouteOnClientResult"/>.
/// </returns>
public static RedirectToRouteOnClientResult RedirectToActionOnClient(this Controller controller, string actionName, string controllerName, RouteValueDictionary routeValues)
{
RouteValueDictionary routeValuesDic;
if (controller.RouteData == null)
{
const bool IncludeImplicitMvcValues = true;
routeValuesDic = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, null, routeValues, IncludeImplicitMvcValues);
}
else
{
const bool IncludeImplicitMvcValues = true;
routeValuesDic = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, controller.RouteData.Values, routeValues, IncludeImplicitMvcValues);
}
return new RedirectToRouteOnClientResult(routeValuesDic);
}
#endregion
}
/// <summary>
/// The redirect on client action result.
/// </summary>
public class RedirectOnClientResult : RedirectOnClientResultBase
{
#region Constructors
/// <summary>
/// Initialises a new instance of the <see cref="RedirectOnClientResult"/> class.
/// </summary>
/// <param name="url">
/// The url.
/// </param>
public RedirectOnClientResult(string url)
{
this.Url = url;
}
#endregion
#region Properties
/// <summary>
/// Gets the url.
/// </summary>
public string Url { get; private set; }
#endregion
/// <summary>
/// Gets the url that the action redirects to.
/// </summary>
/// <param name="context">
/// The context.
/// </param>
/// <returns>
/// The <see cref="string"/>.
/// </returns>
protected override string GetUrl(ControllerContext context)
{
return this.Url;
}
}
/// <summary>
/// The redirect on client action result base class.
/// </summary>
public abstract class RedirectOnClientResultBase : JavaScriptResult
{
/// <summary>
/// Enables processing of the result of an action method by a custom type that
/// inherits from the System.Web.Mvc.ActionResult class.
/// </summary>
/// <param name="context">
/// The context within which the result is executed.
/// </param>
public override void ExecuteResult(ControllerContext context)
{
this.Script = string.Format("window.location = '{0}';", this.GetUrl(context));
base.ExecuteResult(context);
}
/// <summary>
/// Gets the url that the action redirects to.
/// </summary>
/// <param name="context">
/// The context.
/// </param>
/// <returns>
/// The <see cref="string"/>.
/// </returns>
protected abstract string GetUrl(ControllerContext context);
}
/// <summary>
/// The redirect to action on client action result.
/// </summary>
public class RedirectToRouteOnClientResult : RedirectOnClientResultBase
{
#region Constructors
/// <summary>
/// Initialises a new instance of the <see cref="RedirectToRouteOnClientResult"/> class.
/// </summary>
/// <param name="routeValues">
/// The route values.
/// </param>
public RedirectToRouteOnClientResult(RouteValueDictionary routeValues)
{
this.RouteValues = routeValues;
}
/// <summary>
/// Initialises a new instance of the <see cref="RedirectToRouteOnClientResult"/> class.
/// </summary>
/// <param name="routeName">
/// The route name.
/// </param>
/// <param name="routeValues">
/// The route values.
/// </param>
public RedirectToRouteOnClientResult(string routeName, RouteValueDictionary routeValues)
{
this.RouteName = routeName;
this.RouteValues = routeValues;
}
#endregion
#region Properties
/// <summary>
/// Gets the route name.
/// </summary>
public string RouteName { get; private set; }
/// <summary>
/// Gets the route values.
/// </summary>
public RouteValueDictionary RouteValues { get; private set; }
#endregion
/// <summary>
/// Gets the url that the action redirects to.
/// </summary>
/// <param name="context">
/// The context.
/// </param>
/// <returns>
/// The <see cref="string"/>.
/// </returns>
protected override string GetUrl(ControllerContext context)
{
var urlHelper = new UrlHelper(context.RequestContext);
var url = string.IsNullOrEmpty(this.RouteName)
? urlHelper.RouteUrl(this.RouteValues)
: urlHelper.RouteUrl(this.RouteName, this.RouteValues);
return url;
}
}
/* ****************************************************************************
*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* This software is subject to the Microsoft Public License (Ms-PL).
* A copy of the license can be found in the license.htm file included
* in this distribution.
*
* You must not remove this notice, or any other, from this software.
*
* ***************************************************************************/
/// <summary>
/// The route values helper methods.
/// </summary>
internal static class RouteValuesHelpers
{
/// <summary>
/// Gets the route values for the given route values.
/// </summary>
/// <param name="routeValues">
/// The route values.
/// </param>
/// <returns>
/// The <see cref="RouteValueDictionary"/>.
/// </returns>
public static RouteValueDictionary GetRouteValues(RouteValueDictionary routeValues)
{
return (routeValues != null) ? new RouteValueDictionary(routeValues) : new RouteValueDictionary();
}
/// <summary>
/// Merges the given route data with the route values collection.
/// </summary>
/// <param name="actionName">
/// The action name.
/// </param>
/// <param name="controllerName">
/// The controller name.
/// </param>
/// <param name="implicitRouteValues">
/// The implicit route values.
/// </param>
/// <param name="routeValues">
/// The route values.
/// </param>
/// <param name="includeImplicitMvcValues">
/// The include implicit mvc values.
/// </param>
/// <returns>
/// The <see cref="RouteValueDictionary"/>.
/// </returns>
public static RouteValueDictionary MergeRouteValues(string actionName, string controllerName, RouteValueDictionary implicitRouteValues, RouteValueDictionary routeValues, bool includeImplicitMvcValues)
{
// Create a new dictionary containing implicit and auto-generated values
var mergedRouteValues = new RouteValueDictionary();
if (includeImplicitMvcValues)
{
// We only include MVC-specific values like 'controller' and 'action' if we are generating an action link.
// If we are generating a route link [as to MapRoute("Foo", "any/url", new { controller = ... })], including
// the current controller name will cause the route match to fail if the current controller is not the same
// as the destination controller.
object implicitValue;
if (implicitRouteValues != null && implicitRouteValues.TryGetValue("action", out implicitValue))
{
mergedRouteValues["action"] = implicitValue;
}
if (implicitRouteValues != null && implicitRouteValues.TryGetValue("controller", out implicitValue))
{
mergedRouteValues["controller"] = implicitValue;
}
}
// Merge values from the user's dictionary/object
if (routeValues != null)
{
foreach (var routeElement in GetRouteValues(routeValues))
{
mergedRouteValues[routeElement.Key] = routeElement.Value;
}
}
// Merge explicit parameters when not null
if (actionName != null)
{
mergedRouteValues["action"] = actionName;
}
if (controllerName != null)
{
mergedRouteValues["controller"] = controllerName;
}
return mergedRouteValues;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment