How to intercept a Url to dynamically change the routing - c#

I am looking to do something like:
For categories where the Controller will be CategoryController
www.mysite.com/some-category
www.mysite.com/some-category/sub-category
www.mysite.com/some-category/sub-category/another //This could go on ..
The problem is that: www.mysite.com/some-product needs to point to a ProductController. Normally this would map to the same controller.
So, how can I intercept the routing so I can check if the parameter is a Category or Product and route accordingly.
I am trying to avoid having something like www.mysite.com/category/some-category or www.mysite.com/product/some-product as I feel it will perform better on the SEO side. When I can intercept the routing, I'll forward to a product / category based on some rules that look at slugs for each etc.

You could write a custom route to serve this purpose:
public class CategoriesRoute: Route
{
public CategoriesRoute()
: base("{*categories}", new MvcRouteHandler())
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
string categories = rd.Values["categories"] as string;
if (string.IsNullOrEmpty(categories) || !categories.StartsWith("some-", StringComparison.InvariantCultureIgnoreCase))
{
// The url doesn't start with some- as per our requirement =>
// we have no match for this route
return null;
}
string[] parts = categories.Split('/');
// for each of the parts go hit your categoryService to determine whether
// this is a category slug or something else and return accordingly
if (!AreValidCategories(parts))
{
// The AreValidCategories custom method indicated that the route contained
// some parts which are not categories => we have no match for this route
return null;
}
// At this stage we know that all the parts of the url are valid categories =>
// we have a match for this route and we can pass the categories to the action
rd.Values["controller"] = "Category";
rd.Values["action"] = "Index";
rd.Values["categories"] = parts;
return rd;
}
}
that will be registered like that:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("CategoriesRoute", new CategoriesRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
and then you can have the corresponding controller:
public class CategoryController: Controller
{
public ActionResult Index(string[] categories)
{
... The categories action argument will contain a list of the provided categories
in the url
}
}

Related

Special MVC Routing not working

I'm having problems with the routings in my MVC project not working...
I want all my views in the Views > Shared folder like this:
Error.cshtml (default)
Index.cshtml (default)
Overview.cshtml (custom that I made)
Recordings.cshtml (custom that I made)
I've then created one shared controller to handle all views like this:
public class SharedController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Error()
{
return View();
}
public ActionResult Overview()
{
return View();
}
public ActionResult Recordings()
{
return View();
}
}
My RouteConfig.cs looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Map to specific pages under Shared controller:
routes.MapRoute("SharedPages", "{action}/{id}",
new { controller = "Shared", action = #"Overview|Recordings", id = UrlParameter.Optional });
// Use the default rout for all other pages:
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Shared", action = "Index", id = UrlParameter.Optional }
);
// Show the Error page for anything else (404):
routes.MapRoute("Error", "{*url}",
new { controller = "Shared", action = "Error" }
);
}
I want the routing to work like this:
://(url)/ (root - no action specified) --> Shared/Index.cshtml
://(url)/Index --> Shared/Index.cshtml
://(url)/Overview --> Shared/Overview.cshtml
://(url)/Recordings --> Shared/Recordings.cshtml
://(url)/whatever (or if an error occurs) --> Shared/Error.cshtml
But it's not working as expected. If I go to ://(url)/ (root), I get a HTTP 404 - The resource cannot be found. If I go to for example ://(url)/Overview, it's working fine.
How can I make it work like I want?
The order of how you map route is important and first matched route wins. That means that even if there is no resource there one it matches the route it will use it.
public static void RegisterRoutes(RouteCollection routes)
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Use specific rout for all other pages:
routes.MapRoute("WhateverA", "WhateverA/{action}/{id}",
new { controller = "WhateverA", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute("WhateverB", "WhateverB/{action}/{id}",
new { controller = "WhateverB", action = "Index", id = UrlParameter.Optional }
);
// Map to specific pages under Shared controller:
routes.MapRoute("RootPages", "{action}/{id}",
new { controller = "Shared", action = "Index", id = UrlParameter.Optional });
// Show the Error page for anything else (404):
routes.MapRoute("Error", "{*url}",
new { controller = "Shared", action = "Error" }
);
}
The problem with the Default and SharedPages routes is that they conflict with each other. You may need to provide specific routes for other controllers if they exist. Other wise the other option is to use Attribute Routing for the other controllers and convention-based routing for your root routes and error
public static void RegisterRoutes(RouteCollection routes)
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Attribute routing
routes.MapMvcAttributeRoutes();
// Map to specific pages under Shared controller:
routes.MapRoute("RootPages", "{action}/{id}",
new { controller = "Shared", action = "Index", id = UrlParameter.Optional });
// Show the Error page for anything else (404):
routes.MapRoute("Error", "{*url}",
new { controller = "Shared", action = "Error" }
);
}
With controllers decorated accordingly
[RoutePrefix("WhatEver")]
public class WhatEverController : Controller {
//GET whatever
[HttpGet]
[Route("")]
public ActionResult Index() { ... }
}

MVC4 Custom Routing - Validating Personalized Urls

I have a website in MVC4 that I am developing that requires some custom routing. It is a simple website with a few pages. For example:
/index
/plan
/investing
... etc.. a few others
Through an admin panel the site administrator can create "branded" sites, that basically mirror the above content, but swap out a few things like branded company name, logo etc. Once created, the URLs would look like
/{personalizedurl}/index
/{personalizedurl}/plan
/{personalizedurl}/investing
... etc... (exact same pages as the non branded pages.
I am validating the personalized urls with an action filter attribute on the controller method and returning a 404 if not found in the database.
Here is an example of one of my actions:
[ValidatePersonalizedUrl]
[ActionName("plan")]
public ActionResult Plan(string url)
{
return View("Plan", GetSite(url));
}
Easy-peasy so far and works pretty well with the following routes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Admin",
url: "Admin/{action}/{id}",
defaults: new { controller = "Admin", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{action}",
defaults: new { controller = "Default", action = "Index" }
);
routes.MapRoute(
"Branded", // Route name
"{url}/{action}", // URL with parameters
new { controller = "Default", action = "Index" } // Parameter defaults
);
/*
routes.MapRoute(
"BrandedHome", // Route name
"{url}/", // URL with parameters
new { controller = "Default", action = "Index" } // Parameter defaults
);
*/
}
The problem I currently have is with the bottom commented out route. I'd like to be able to go to /{personalizedurl}/ and have it find the correct action (Index action in default controller). right now with the bottom line commented out, I get a 404 because it thinks its an action and its not found. When I un-comment it, the index pages, work however the individual actions do not /plan for example because it thinks its a pUrl and can't find it in the database.
Anyway, sorry for the long question. Any help or suggestions on how to set this up would be greatly appreciated.
James
The problem is that MVC will use the first matching url and since the second route is:
routes.MapRoute(
name: "Default",
url: "{action}",
defaults: new { controller = "Default", action = "Index" }
);
and that matches your /{personalizedurl}/ it will route to Default/{action}.
What you want gets a bit tricky! I assume the personalizing is to be dynamic, not some static list of branded companies and you wouldn't want to recompile and deploy every time you add/remove a new one.
I think you will need to handle this in the controller, it won't work well in routing; unless it is a static list of personalized companies. You will need the ability to check if the first part is one of your actions and to check if it is a valid company, I will give you an example with simple string arrays. I believe you will be building the array by query some sort of data store for your personalized companies. I also have created a quick view model called PersonalizedViewModel that takes a string for the name.
Your routing will be simplified:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Admin",
url: "Admin/{action}/{id}",
defaults: new { controller = "Admin", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{url}/{action}",
defaults: new { controller = "Default", action = "Index", url = UrlParameter.Optional }
);
}
Here is the view model my example uses:
public class PersonalizedViewModel
{
public string Name { get; private set; }
public PersonalizedViewModel(string name)
{
Name = name;
}
}
And the Default controller:
public class DefaultController : Controller
{
private static readonly IEnumerable<string> personalizedSites = new[] { "companyA", "companyB" };
private static readonly IEnumerable<string> actions = new[] { "index", "plan", "investing", "etc" };
public ActionResult Index(string url)
{
string view;
PersonalizedViewModel viewModel;
if (string.IsNullOrWhiteSpace(url) || actions.Any(a => a.Equals(url, StringComparison.CurrentCultureIgnoreCase)))
{
view = url;
viewModel = new PersonalizedViewModel("Default");
}
else if (personalizedSites.Any(s => s.Equals(url, StringComparison.CurrentCultureIgnoreCase)))
{
view = "index";
viewModel = new PersonalizedViewModel(url);
}
else
{
return View("Error404");
}
return View(view, viewModel);
}
public ActionResult Plan(string url)
{
PersonalizedViewModel viewModel;
if (string.IsNullOrWhiteSpace(url))
{
viewModel = new PersonalizedViewModel("Default");
}
else if (personalizedSites.Any(s => s.Equals(url, StringComparison.CurrentCultureIgnoreCase)))
{
viewModel = new PersonalizedViewModel(url);
}
else
{
return View("Error404");
}
return View(viewModel);
}
}

Dynamic Routes from database for ASP.NET MVC CMS

Basically I have a CMS backend I built using ASP.NET MVC and now I'm moving on to the frontend site and need to be able to load pages from my CMS database, based on the route entered.
So if the user enters example.com/students/information, MVC would look in the pages table to see if a page exists that has a permalink that matches students/information, if so it would redirect to the page controller and then load the page data from the database and return it to the view for display.
So far I have tried to have a catch all route, but it only works for two URL segments, so /students/information, but not /students/information/fall. I can't find anything online on how to accomplish this, so I though I would ask here, before I find and open source ASP.NET MVC CMS and dissect the code.
Here is the route configuration I have so far, but I feel there is a better way to do this.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Default route to handle core pages
routes.MapRoute(null,"{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new { controller = "Index" }
);
// CMS route to handle routing to the PageController to check the database for the route.
var db = new MvcCMS.Models.MvcCMSContext();
//var page = db.CMSPages.Where(p => p.Permalink == )
routes.MapRoute(
null,
"{*.}",
new { controller = "Page", action = "Index" }
);
}
If anybody can point me in the right direction on how I would go about loading CMS pages from the database, with up to three URL segments, and still be able to load core pages, that have a controller and action predefined.
You can use a constraint to decide whether to override the default routing logic.
public class CmsUrlConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var db = new MvcCMS.Models.MvcCMSContext();
if (values[parameterName] != null)
{
var permalink = values[parameterName].ToString();
return db.CMSPages.Any(p => p.Permalink == permalink);
}
return false;
}
}
use it in route definition like,
routes.MapRoute(
name: "CmsRoute",
url: "{*permalink}",
defaults: new {controller = "Page", action = "Index"},
constraints: new { permalink = new CmsUrlConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Now if you have an 'Index' action in 'Page' Controller like,
public ActionResult Index(string permalink)
{
//load the content from db with permalink
//show the content with view
}
all urls will be caught by the first route and be verified by the constraint.
if the permalink exists in db the url will be handled by Index action in Page controller.
if not the constraint will fail and the url will fallback to default route(i dont know if you have any other controllers in the project and how you will decide your 404 logic).
EDIT
To avoid re querying the cms page in the Index action in Page controller, one can use the HttpContext.Items dictionary, like
in the constraint
var db = new MvcCMS.Models.MvcCMSContext();
if (values[parameterName] != null)
{
var permalink = values[parameterName].ToString();
var page = db.CMSPages.Where(p => p.Permalink == permalink).FirstOrDefault();
if(page != null)
{
HttpContext.Items["cmspage"] = page;
return true;
}
return false;
}
return false;
then in the action,
public ActionResult Index(string permalink)
{
var page = HttpContext.Items["cmspage"] as CMSPage;
//show the content with view
}
I use simpler approach that doesn't require any custom router handling.
Simply create a single/global Controller that handles a few optional parameters, then process those parameters as you like:
//Route all traffic through this controller with the base URL being the domain
[Route("")]
[ApiController]
public class ValuesController : ControllerBase
{
//GET api/values
[HttpGet("{a1?}/{a2?}/{a3?}/{a4?}/{a5?}")]
public ActionResult<IEnumerable<string>> Get(string a1 = "", string a2 = "", string a3 = "", string a4 = "", string a5 = "")
{
//Custom logic processing each of the route values
return new string[] { a1, a2, a3, a4, a5 };
}
}
Sample output at example.com/test1/test2/test3
["test1","test2","test3","",""]

MVC 3 routing for unknown urls

I want to be able to handle any url that s requested via some controller.
foo.com/a
foo.com/abcd
foo.com/x1
for foo.com/a
I want to process it with
UrlHandlerController with Process(string url) method.
How should i add a routing rule to be able to do this?
Any ideas?
Create a new custom route and use Phill Haack's Route Debugger to test your routes:
routes.MapRoute(
"customroute",
"{url}",
new { controller = "UrlHandler",
action = "Process",
url = ""
}
);
Controller:
public class UrlHandlerController : Controller
{
[HttpGet]
public ActionResult Process(string url)
{
return View();
/* or */
if(url == "something"){
return View("SomethingView");
}
else if(url == "somethingelse"){
return View("SomethingElseView");
}
}
}
Darth, see if this route helps:
routes.MapRoute(
"CustomRoute", // Route name
"{url}", //Route formation
new { controller = "UrlHandler", action = "Process" }, // Where to send it
new { keyWord = #"\S+" } // Regex to identify the argument
);
Regards.

Getting url path from ASP.net MVC route

I have a controller that looks like this:
public class PageController : Controller
{
public ActionResult Render(string url)
{
//this is just for testing!
return Content("url was " + url);
}
}
I'm trying to pass in the value of the url into the controller. For example:
http://www.site.com/products/something/else
Would pass "products/something/else" into my Render action of the PageController.
This is because we are using "products/something/else" as a unique key for a record in the database (legacy system, don't ask)
So, my resultant query would be something along the lines of this:
select * from foo where urlKey = 'products/something/else'
So far I have this in my RegisterRoutes section on Global.asax:
routes.MapRoute("pages", "{*url}", new { controller = "Page", action = "Render", url="/" });
But this isn't working as expected...
By visiting www.site.com/products/something/else, the value passed into the controller is "home/index/0"
The only route defined in RegisterRoutes is that described in the question.
The below class matches every route but you can modify as per your needs.
public class LegacyRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string url = httpContext.Request.RawUrl.Substring(1);
result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("controller", "Page");
result.Values.Add("action", "Render");
result.Values.Add("url", url);
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
In Global.asax.cs
routes.Add(new LegacyRoute());
Hope this helps, one of our routes does something similar and this is the code:
routes.MapRoute(
name: "Standard",
url: "{controller}/{action}/{id}",
defaults: new { id = UrlParameter.Optional, action = ControllersAndActions.TypicalController.IndexAction, page = 1 },
constraints: new
{
controller = ControllersAndActions.ControllerConstraintExpression
}
);

Categories

Resources