Search code examples
c#asp.net-mvccustom-action-filter

MVC ActionFilter : Rewrite request / redirect to Action


Imagine a site that has many domains it binds to. f.e. product1.com, product2.com, ...

When product23 gets out of order the idea is to have a 'landingpagecontroller' serve all the request to product23.com.

The idea is to write a ActionFilter to accomplish this :

   public class Landingpage : IActionFilter
{

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var DnsSafeHost = filterContext.HttpContext.Request.Url.DnsSafeHost;
        if (DnsSafeHost.NeedsLandingpage())
        {
            //Do actual redirecting
         }
    }
}

NeedsLandingpage() return a boolean. True if servicing by landingpage controller is needed. Intelligence will be found in a DB.

I already added a route to the landingpage controller.

            routes.MapRoute(
            name: "Ladingpage",
            url: "Landingpage/{id}/{*dummy}",
            defaults: new { controller = "LandingPage", action = "Index" }
        );

Any tips on how to change the route settings from the actionfilter so that the above route is triggered or a better solution to accomplish the goal.

********** UPDATED ************

Given the input from you all I came up with the following working solution, but I'm not 100% happy with it. So a better solution is welcome.

I created the following IActionFilter and registered it to be global.

public class Landingpage : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var DnsSafeHost = filterContext.HttpContext.Request.Url.DnsSafeHost;
            var _LandingPage = LandingPage(DnsSafeHost);
            if (_LandingPage != null)
            {
                if ((String)filterContext.RouteData.Values["controller"] != "Landingpage")
                {
                    filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Landingpage", action = "Index", id = _LandingPage }));
                    filterContext.Result.ExecuteResult(filterContext.Controller.ControllerContext);
                }
            }
        }
        private String LandingPage(String DnsSafeHost)
        {
            if (DnsSafeHost.Contains("<domain to look for>".ToLower())) return "<viewname>";
            if (DnsSafeHost.Contains("<domain to look for>".ToLower())) return "<viewname>";
            if (DnsSafeHost.Contains("<domain to look for>".ToLower())) return "<viewname>";
            return null;
        }
    }

What I do not like about the solution is the check of (String)filterContext.RouteData.Values["controller"]

which is needed or you get stuck in a loop because.

Browser -> Routing -> Filter -> Redirect -> Routing -> Filter ...

A possible solution I thought would be to do something in Application_BeginRequest (global.asax). But I did not find a way to 'rewrite' the request. Because if that would be possible the flow would be : Browser -> BeginRequest -> RewriteRequest -> Routing -> Controller.

A down-site seems to be that in vNext this would not work anymore if you follow the best-practices, I guess.

Filterconfig.cs

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new Landingpage());

    }
}

All the 'landingpage' calls are redirected to the LandingPageController. The call to ViewEngine is there to check if the viewname passed from the actionfilter does exists, if not a default one is used.

   public class LandingpageController : Controller
    {
        public ActionResult Index(string id)
        {
            if (id == null) return View();
            if (ViewEngines.Engines.FindView(ControllerContext, id, null).View == null) return View();
            ViewBag.Title = id;
            return View(id);
        }
    }

A sample of a view.

@{
    Layout = "_Index_layout.cshtml";
}

<div class="domainname">Landingpage</div>

To give it a 'special' lay-out different from the normal pages (f.e. no menu) I added a custom layout for this page.

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <style>
--- add some styling here ---
     </style>
</head>

<body>
    @RenderBody()
</body>

</html>

Solution

  • As mentioned in my EDIT I had a solution but was not 100% happy with due to the redirect and the dirty way to get out of the redirect loop.

    If you take a look at the request processing pipeline.

    http://saulius.sunauskas.com/wp-content/uploads/asp_net__mvc_request_pipeline_4.png

    The place to alter the request is the 'IRouteHandler' on the flow-chart.

    Found a nice sample of creating a custom one here :

    http://www.brainthud.com/cards/5218/24821/give-an-example-of-how-you-would-create-a-custom-route-handler-to-reroute-requests-with-a-certain-value-in-the-accept-http-he

    The proto-type of the final solution is:

    1. Create a custom IRouteHandler direving from MVCRouteHandler :

      public class LandingPageRouteHandler : MvcRouteHandler
      {
          protected override IHttpHandler GetHttpHandler(RequestContext Context)
          {
              if (  Context.HttpContext.Request.Url.DnsSafeHost.ToLower().Contains("onecityguide"))
              {
                  Context.RouteData.Values["controller"] = "LandingPage";
                  Context.RouteData.Values["action"] = "Index"; 
                  Context.RouteData.Values["id"] = "onecityguide";
              }
              return base.GetHttpHandler(Context);
          }
      }
      
    2. Hookup the custom RouteHandler to the default route.

      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
      ).RouteHandler = new LandingPageRouteHandler();