Question:
How to create a route where only the single parameter would be displayed?
For example, my route currently looks like this:
routes.MapRoute(
name: "RouteEvent",
url: "{ProdutoNome}",
defaults: new {
controller = "Produto",
action = "Detalhe",
ProdutoNome= UrlParameter.Optional
});
and in this scenario I want the browser-visible URL
to look just like this:
URL:
localhost:43760/NomeDoMeuProduto
however, as it is, my return is like 404
when I try to call this URL
.
Answer:
The order in which routes are declared makes a difference. There is the possibility of a URL hitting with two routes, but the router will compare with the routes in the order they were declared. The first one to hit wins.
The correct thing is to put specific routes at the beginning, because if they are not suitable, then the Default route will be analyzed. We could start like this:
routes.MapRoute(
name: "RouteEvent",
url: "{ProdutoNome}",
defaults: new
{
controller = "Produto",
action = "Detalhe",
ProdutoNome = UrlParameter.Optional
}
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
First let's understand that EVERYTHING that comes after the domain and the port (eg after localhost:43760/), that is, the entire URL path comes as a parameter to the RouteValueDictionary
, including the Controller name, for Action (and the Area if it had any), as well as the actual parameters of the action.
It would even "work", but not as we would like. The problem is that all URLs with only one parameter (or none) would fall into the first route:
localhost:43760 (no parameters)
localhost:43760/MyProductName (with a MyProductName parameter)
localhost:43760/Home (with a Home parameter)
localhost:43760/Account (with an Account parameter)
…since the first route is made up of a single ProdutoNome
parameter , which is optional.
These URLs would not fall under it, however:
localhost:43760/Home/Index (two parameters, Home and Index)
localhost:43760/Account/Login (two parameters, Account and Login)
Because the RouteEvent route only expects one parameter ( ProdutoNome
that can even be omitted), but not two parameters (ex: Home + Index or Account + Login).
When removing ProdutoNome = UrlParameter.Optional
, at least localhost:43760
no longer falls on that route (as ProdutoNome
is required). Still, all other URLs with a single parameter (eg just the Controller, omitting the Action) would fall into that route.
Solution
To solve this, we have to create a Constraint on the specific route, to check the URLs with a single parameter, and find out if that parameter is in fact a Product or a Controller. Example:
public class ProdutoConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string nomeProduto = values[parameterName].ToString();
using (var ctx = new MeuDbContext())
return ctx.Produtos.Any(p => p.Nome == nomeProduto);
}
}
…or any other way you can ensure that the value in question is a product.
You could also do the opposite, if you wanted to avoid a SELECT
in the database. I could guarantee that the received parameter doesn't match the name of any Controller (maybe it's even better this way):
public class ProdutoConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return !Assembly.GetAssembly(typeof(MvcApplication))
.GetTypes().Where(type => typeof(Controller).IsAssignableFrom(type))
.Any(c => c.Name.Replace("Controller", "") == values[parameterName].ToString());
}
}
So your route would look like this:
routes.MapRoute(
name: "RouteEvent",
url: "{ProdutoNome}",
defaults: new
{
controller = "Produto",
action = "Detalhe"
},
constraints: new { ProdutoNome = new ProdutoConstraint() }
);
Note: If you happen to have a Controller whose name is also the name of a product, obviously your Controller will be ignored, as he will understand that you want to see the product, once it exists. But it would be the opposite if your routine was trying to ensure that the parameter was not an existing Controller, in which case the Controller would be displayed instead of the product.
Remembering that the RouteEvent
must be declared in RouteConfig.cs
above the Default
route.