ASP.NET MVC. Routing manager. Resolve routes issues with similar link values - c#

I need advice about a problem with which I recently encountered. The essence of the problem: in my web site I have a Controller. Let it be ProductController. And, of course, I have some Actions in it:
public class ProductController : Controller
{
public virtual ActionResult ProductDetails(string slug)
public virtual ActionResult ProductList (string slug, string category, string filter)
}
Filter parameter can be empty. My routes in RouteConfig:
routes.MapRoute("ProudctsList", "products/{slug}/{category}/{selectedFilters}", MVC.Product.ProductList()
.AddRouteValue("selectedFilters", ""));
routes.MapRoute("Products", "products/{slug}", MVC.Product.ProductDetails());
routes.MapRoute("CustomPage", "custom/{slug}", MVC.CustomPage.Index());
......
routes.MapRoute("BrandCollectionDetails", "brand/{slug}/{collectionId}", MVC.Brand.BrandCollectionDetails());
routes.MapRoute("BrandDetails", "brand/{slug}", MVC.Brand.BrandDetails());
routes.MapRoute("HomePage", "", MVC.Home.Index());
routes.MapRoute("Sales.Index", "sales", MVC.Sales.Index());
According to requirements I need to remove products from the Url. I mean that if Url has such scheme product/cars/audi/black - it should be just cars/audio/black. And also I need to remove custom from the Url. As you could see, I'll get Urls with the same scheme. I need to say that I have I List of custom Urls:
List<string> customPages = new List<string> {/Ferrari, ...}
I mean that all Urls with such scheme: /{something} should be checked using this List.
I just need to remove products/ segment from this Urls products/{slug}/{category}/{filter} and custom from this custom/{slug} and direct user to the necessary Controller Action. I have no ideas how to build custom route manager that should resolve this issues and need experts help. Please, tell me if the issue is clear.

There are several ways to solve this problem. It looks like your specific problem can be solved by creating an implementation of the IRouteConstraint.
This will allow you to dynamically override the default routing functionality of your application.
public class ProductUrlConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> customPages = new List<string> {"Ferrari", "Porche"};
if (values[parameterName] != null)
{
var slug= values[parameterName].ToString();
var product = customPages.Where(p => p == slug).FirstOrDefault();
if(!string.isNullorEmpty(product))
{
HttpContext.Items["customProduct"] = product;
return true;
}
}
return false;
}
}
Use your implementation of the IRouteConstraint in your route definition as follows:
routes.MapRoute(
name: "ProductRoute",
url: "{*customProduct}",
defaults: new {controller = "Product", action = "Index"},
constraints: new { customProduct= new ProductUrlConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
The action would then follow like:
public ActionResult Index(string permalink)
{
var page = HttpContext.Items["customProduct"].ToString();
//dont forget to check for null :)
//model/ view logic
}
You might have to do some custom string processing within your action depending on your later requirements.
Storing the custom product in the HttpContext is simply a performance consideration.

Related

Create Route for a specific URL without changing the URL with MVC

I have a MVC Web Application that runs on www.domain.com and I need to configure a different URL binding for another domain www.domain2.com for the same web application.
The new domain www.domain2.com will have to return a specific Controller Action View like /Category/Cars:
routes.MapRoute(
name: "www.domain2.com",
url: "www.domain2.com",
defaults: new { controller = "Category", action = "Cars", id = UrlParameter.Optional }
);
How can I achieve this without changing the URL, so the visitor inserts the url www.domain2.com and receives the view www.domain.com/category/cars but the url remains www.domain2.com?
EDIT:
I have tried this approach but it's not working:
routes.MapRoute(
"Catchdomain2",
"{www.domain2.com}",
new { controller = "Category", action = "Cars" }
);
Domains are normally not part of routes, which is why your examples don't work. To make routes that work only on specific domains you have to customize routing.
By default, all of the routes in your route configuration will be available on all domains that can reach the web site.
The simplest solution for this is to create a custom route constraint and use it to control the domains that a specific URL will match.
DomainConstraint
public class DomainConstraint : IRouteConstraint
{
private readonly string[] domains;
public DomainConstraint(params string[] domains)
{
this.domains = domains ?? throw new ArgumentNullException(nameof(domains));
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string domain =
#if DEBUG
// A domain specified as a query parameter takes precedence
// over the hostname (in debug compile only).
// This allows for testing without configuring IIS with a
// static IP or editing the local hosts file.
httpContext.Request.QueryString["domain"];
#else
null;
#endif
if (string.IsNullOrEmpty(domain))
domain = httpContext.Request.Headers["HOST"];
return domains.Contains(domain);
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// This ignores Category/Cars for www.domain.com and www.foo.com
routes.IgnoreRoute("Category/Cars", new { _ = new DomainConstraint("www.domain.com", "www.foo.com") });
// Matches www.domain2.com/ and sends it to CategoryController.Cars
routes.MapRoute(
name: "HomePageDomain2",
url: "",
defaults: new { controller = "Category", action = "Cars" },
constraints: new { _ = new DomainConstraint("www.domain2.com") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
// This constraint allows the route to work either
// on "www.domain.com" or "www.domain2.com" (excluding any other domain)
constraints: new { _ = new DomainConstraint("www.domain.com", "www.domain2.com") }
);
}
}
If you fire this up in a new project in Visual Studio, you will notice it shows an error. This is because localhost:<port> is not a configured domain. However, if you navigate to:
/?domain=www.domain.com
You will see the home page.
This is because for the debug build only, it allows you to override the "local" domain name for testing purposes. You can configure your local IIS server to use a local static IP address (added to your network card) and add a local hosts file entry to test it locally without the query string parameter.
Note that when doing a "Release" build, there is no way to test using a query string parameter, as that would open up a potential security vulnerability.
If you use the URL:
/?domain=www.domain2.com
it will run the CategoryController.Cars action method (if one exists).
Note that since the Default route covers a wide range of URLs, most of the site will be available to both www.domain.com and www.domain2.com. For example, you will be able to reach the About page both at:
/Home/About?domain=www.domain.com
/Home/About?domain=www.domain2.com
You can use the IgnoreRoute extension method to block URLs that you don't want (and it accepts route constraints, so this solution will work there, too).
This solution will work if you largely want to share functionality between domains. If you would rather have 2 domains in one web site, but make them act like separate web sites, it would be easier to manage if you use an Area for each "web site" in your project by using the above route constraint for the Area routes.
public class Domain2AreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Domain2";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
name: "Domain2_default",
url: "{controller}/{action}/{id}",
defaults: new { action = "Index", id = UrlParameter.Optional },
constraints: new { _ = DomainConstraint("www.domain2.com") }
);
}
}
The above configuration would make every URL (that is 0, 1, 2, or 3 segments long) for www.domain2.com route to a controller in the Domain2 Area.
in the default action of the application make sure that the url is the one of the second domain, then return the method that needs. something like:
public ActionResult Index()
{
if (Request.Url.Host.Equals("domain2"))
return AnotherAction();
}
Agreed with the answer above.
If you want more beautiful implementation - try action filters.
Sample of action filters usage from there.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = (SomeControllerBase) filterContext.Controller;
filterContext.Result = controller.RedirectToAction("index", "home");
}
Sample of getting the URL inside action filter from there.
var url = filterContext.HttpContext.Request.Url;
Put the things together and have fun :)

URL Routing MVC 5 Asp.net

I know about routing in MVC. I added a new MapRoute under RegisterRoute method in RouteConfig.cs class and successfully called my function with the URL http://localhost:53363/package/PackageDetail/mypackage/5.
However, my question is do i have to add different Map Routes for every method or is there any better way ? Like in PackageController class you can see i have two methods one methods takes PackageId and PackageName and the other takes only PackageId. So do i have to register different Map Routes or not ?
RouteConfig
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Package",
url: "Package/PackageDetail/{packageName}/{packageId}",
defaults: new { controller = "Package", action = "PackageDetail", packageName = UrlParameter.Optional, packageId = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
PackageController.cs :
[HttpGet]
public ActionResult PackageListing(int packageId = 0)
{
return View();
}
[HttpGet]
public ActionResult PackageDetail(string packageName = "", int packageId = 0)
{
return View();
}
Despite the fact that Muhammed's answer will work, it is very repetitive, especially if you're using the same style of routes for multiple types.
There are a few things to consider before deciding upon a single approach to routing. The main one is why have both the name and ID in the route? If you want a more SEO friendly URL structure, don't bother with the ID at all.
If you have multiple products within the same type that have identical names, then there's no point in including the name as part of the URL since that won't get a user where they want to go by itself. In that event, just leave the original route.
However, if you have several different controllers (or actions) with a similar name/id structure for the routes, you'll be far better served with making your custom route more generic.
routes.MapRoute(
name: "NameAndId",
url: "{controller}/{action}/{name}/{id:int}",
defaults: new
{
controller = "Package",
action = "PackageDetail",
name = UrlParameter.Optional,
id = UrlParameter.Optional
});
Keep this above the default route, and this will redirect not just
/Package/PackageDetail/Deluxe/5
but also allow you to have stuff like this:
/Meals/Menu/Dinner/3
That may not necessarily be applicable for you in this project, but since you're learning MVC, this is a good skill to pick up. The more generic you're able to maintain your route definitions, the less you'll need to repeat it. Of course, if this is a one-time special route, there's nothing wrong with using the attributes.
Also to answer your final question, you do not need to create another custom route, because your PackageListing method will be routed through the default route that was provided when you created your project.
If you want to override default route url and generate custom url then you need to register route in route config file.
You can pass Package name and package Id as below.
http://sitename/Package/PackageListing?packageId=1
http://sitename/Package/PackageDetail?packageName=packagename&packageId=1
but if you want to generate URL as below than you need to add route in route.config file.
http://sitename/Package/PackageListing/1
http://sitename/Package/PackageDetail/packageName/1

How to register new route from the controller action method?

I have to develop some pages and public handles (aka, aliases) for those pages.
(To get the idea: in the facebook you can have alias for your page and the final URL will look like facebook/alias instead of facebook/somelongpieceofsomestuff).
I store public handle in the db table and make sure that all handles are unique.
Now I've added routing registration for my handles:
public override void RegisterArea(AreaRegistrationContext context)
{
// Assume, that I already have dictionary of handles and ids for them
foreach(var pair in publicHandlesDictionary)
{
var encId = SomeHelper.Encrypt(pair.Key);
context.MapRoute(pair.Value, pair.Value,
new {controller = "MyController", action="Index", id = encId});
}
}
So, now I can reach some page by using address http://example.com/alias1 instead of http://example.com/MyController/Index&id=someLongEncryptedId.
And this stuff works fine, ok.
But what if I start the applicatian, then add new handle? This new handle will not be registered, because all routes registration are performed when app is started. Basically, I have to restart the application (the IIS, the VS/IIS Express, the Azure, doesn't matter) to get all routes be registered again including my new handle.
So, is there any way to add new route registration from the controller's action method (when new handle is added)?
you dont need to create all routes at app start.
just use IRouteConstraint to determine what should follow alias logic
public class AliasConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var alias = values[parameterName];
// Assume, that I already have dictionary of handles and ids for them
var publicHandlesDictionary = SomeStaticClass.Dic;
if (publicHandlesDictionary.ContainsValue(alias))
{
//adding encId as route parameter
values["id"] = SomeHelper.Encrypt(publicHandlesDictionary.FirstOrDefault(x => x.Value == alias).Key);
return true;
}
return false;
}
}
//for all alias routes
routes.MapRoute(
name: "Alias",
url: "{*alias}",
defaults: new {controller = "MyController", action = "Index"},
constraints: new { alias = new AliasConstraint() }
);
//for all other default operations
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
this way you can update publicHandlesDictionary anytime and route will pick up the changes

C# MVC Redirect a dynamically specified route to a fixed action

I am using C# MVC.
I have an action which looks like this.
public class CustomController : Controller
{
public ActionResult CustomPage(int customPageId)
{
var model = new CustomPageViewModel()
{
customPageId = customPageId
};
return View(model);
}
}
I want to be able to hit this action but to use a different route. For example I want Home/Index to actually hit this action, but to report in the URL that its Home/Index.
My custom pages are stored in the database which tell it what route to have, how can I create routes for my pages programatically and have MVC perform the required actions?
As this is a CMS based system, I don't want to have to create a Home/Index controller and action, as the user may choose any route they wish.
Thanks, Tom
FYI: I have sort of figured this out. Unfortunately Routes are defined when the application starts, so I have to force a restart if I want to setup a new route while the app is running...
Here is the code I have used to setup my routes incase it helps anybody else.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
var pageList = new PageService().Get();
foreach (var page in pageList)
{
var targetRoute = string.Format("{1}/{2}", page.PageArea, page.PageController, page.PageAction);
if (!string.IsNullOrWhiteSpace(page.PageArea))
targetRoute = string.Format("{0}/{1}/{2}", page.PageArea, page.PageController, page.PageAction);
routes.MapRoute(
string.Format("PageBuilder_{0}", page.PageId),
targetRoute,
new { area = "Builder", controller = "Build", action = "Index", pageId = page.PageId }
);
}
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}

How to Create a dynamic Controller to handle many static Views in ASP.NET MVC?

I have a bunch of mostly-static pages (about 40),
like: order-form01.html, order-form02.html, orderform03.html etc..
Should each of them have its own Controller/Action, or is that possible to have one dynamic Controller/Action for all of them?
My Url should look like this: http://MyProject/GlobalController/IndividualView and for the above example: http://MyProject/OrderForm/order-form01, http://MyProject/OrderForm/order-form02 etc..
Thanks in advance.
Yes it's very easy AND you don't need a switch statement or any other redundant logic.
public class MyController
{
public ActionResult Page(string file)
{
return View(file);
}
}
The magic is in the Route Map:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// New MapRoute for your 40+ files..
routes.MapRoute(
"OrdeForm",
"OrderForm/{file}",
new { controller = "MyController", action = "Page", {file} = "" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
Additionally:
I pass the View name in the query string.
Is not required, but is supported. The following urls will work:
// Url Parameters
http://MyProject/OrderForm/order-form01
http://MyProject/OrderForm/order-form02
// Querystring Parameters
http://MyProject/OrderForm?file=order-form01
http://MyProject/OrderForm?file=order-form02
The only catch is that you need to rename your html files to cshtml and place them in the correct directory for the ViewEngine to find.
#Erik, I also bit of new to mvc . Could you please explain your route map as of how is it possible with default raute again and again
Routes are broken down into 3 values:
Controller
Action
Parameter(s)
At a bare minimum, the controller and action are required. Where the values come from is not dependent on the Url. For example, in the following Url and Map Route...
// Url
http://MyProject/
// MapRoute
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = "" }
);
// Controller named "Home" matches the default in the above route
// Method named "Index" matches the default in the above route
public class HomeController {
public ActionResult Index() {
return new EmptyResult();
}
}
... everything still works because we provided a default value for the controller and action.
Ok let's break down the URL you want:
http://MyProject/OrderForm/order-form01
http://MyProject/OrderForm/order-form02
http://MyProject/<identifier>/{parameter}
You have one identifier that tells me route (OrderForm) and one changing value that because it changes and you want one value, should be a parameter.
http://MyProject/<identifier>/{file}
The name of the parameter makes no difference as long as it matches the signature of the controller method:
http://MyProject/{Controller}/{file}
public class HomeController {
public ActionResult Index(string file) {
return new EmptyResult();
}
}
or
http://MyProject/{Controller}/{einstein}
public class HomeController {
public ActionResult Index(string einstein) {
return new EmptyResult();
}
}
I named the parameter file, because it tells me it's the parameter is a name of a file, whereas the name einstein has no inherent description so is a terrible name for a variable.
http://MyProject/{Controller}/{file}
// MapRoute
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = "" }
);
// Controller named "Home" matches the default in the above route
// Method named "Index" matches the default in the above route
public class HomeController {
public ActionResult Index() {
return new EmptyResult();
}
}
Now we only want this route to run when the identifier is OrderForm so we don't allow that to be a value, we hard code it.
url: "OrderForm/...
Next we have a value that keeps changing, so we to add url parameter:
url: "OrderForm/{file}"
Now we have an issue because we aren't allowing MVC to parse values from the url to populate Controller nor Action so we must supply them.
routes.MapRoute(
name: "",
url: "OrderForm/{file}",
defaults: new { controller = "Home", action = "Index", file = "" }
);
Here we've mapped the url http://MyProject/OrderForm/{file} to:
public class HomeController {
public ActionResult Index(string file) {
return new EmptyResult();
}
}
Now I would choose to to update the defaults to something more specific and descriptive:
routes.MapRoute(
name: "",
url: "OrderForm/{file}",
defaults: new { controller = "OrderForm", action = "Index", file = "" }
);
public class OrderFormController {
public ActionResult Index(string file) {
return new EmptyResult();
}
}
Hope that all makes sense.
After the question edited :my solution is, you can have one controller/action and it should call view (cshtml). Your querystring data should be pass to view as of viewbag variable and partial views should be called acording to the viewbag variable. noo need of editing routing table even(if you are willing to pass it as a query string).
//your routeconfig will be
routes.MapRoute(
name: "default",
url: "{controller}/{file}",
defaults: new { controller = "OrderForm", action = "Index", file = "" }
);
//here no need to have 40 routing table one is enough
//your controller/action will be
public class OrderForm
{
public ActionResult Index(string file)
{
ViewBag.Orderform=file
return View(file);
}
}
//view bag variable is accessible in view as well as in javascript
But I would say as best practice, you can modify default routing to access all urls and navigate it to same controller/action and let that action to return the view. After that use angular / knockout js to handle client side routing and based on it the partial views should be loaded.(still your url will be different for your 40 pages but noo need to pass it as query string)
//your route table will be
routes.MapRoute(
name: "default",
url: "{controller}/{file}",
defaults: new { controller = "OrderForm", action = "Index"}
);
//your controller will be
public class OrderForm
{
public ActionResult Index()
{
return View(file);
}
Navigation should be handled by client side routing

Categories