I am facing an issue having 2 default asp.net mvc routes (applied through custom constraints). What I am trying to do is, load different views based on if the parameters are supplied in the routedictionary or not. Below are my two routes in RouteConfig.cs
routes.MapRoute(
name: "DefaultWatch",
url: "{controller}/{action}/{title}",
defaults: new { controller = "Watch", action = "Index", title = ""},
constraints: new { title = new VideoTypeRouteConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}",
defaults: new { controller = "Main", action = "Index"}
);
I want to open /watch/Index/{title} if the title string is supplied or just open my default route /Main/Index. Below is the implementation for my route constraint.
In VideoTypeRouteConstraint.cs
public class VideoTypeRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.ContainsKey(parameterName))
{
string value = values[parameterName].ToString();
return !String.IsNullOrEmpty(value) ? true : false;
}
return false;
}
}
What I am trying to do check if RouteValueDictionary contains the title variable and if so, returns true so my /Watch/Index/{title} is executed.
Now it works when I hit the below urls
http://localhost:53923/ //returns /Main/Index correctly
http://localhost:53923/?title=routing-optional-parameters-in-asp-net-mvc-5 //Also returns /Main/Index because the value in RouteValueDictionary is null but I can see the value in httpContext.Request[parameterName]
http://localhost:53923/routing-optional-parameters-in-asp-net-mvc-5 //this DOES NOT WORK - Returns 404
RouteValueDictionary contains the key (title) but its value is always null. This is where the issue is I believe but I'm not being able to identify it.
The whole idea of this was to clean my urls for SEO which were way longer when I used a separate controller.
Related
I am using the standard MVC template from MVC 2013.
There is a Home controller with actions About, Contact, etc.
There is an Account controller with actions Login, Logout, etc.
The app is deployed at domain website. The url http://website will produce the output of /Home/Index, without changing the url in the browser address box, ie what the browser shows is not the result of a Http redirect.
How do I make the url http://website/X route to /Home/X if X is not another controller in my application? Otherwise it should route to /Home/X/Index.
The reason is that I would like http://website/about, http://website/contact etc without the Home.
A naive solution would be to simply define a new route above the default (catch-all) that looks like:
routes.MapRoute(
name: "ShortUrlToHomeActions",
url: "{action}",
defaults: new { controller = "Home" }
);
The problem with this approach is that it will prevent accessing the Index (default action) of other controllers (requesting /Other, when you have OtherContoller with Index action would result in 404, requesting /Other/Index would work).
A better solution would be to create a RouteConstraint that will match our /{action} only in case there is no other controller with the same name:
public class NoConflictingControllerExists : IRouteConstraint
{
private static readonly Dictionary<string, bool> _cache = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var path = httpContext.Request.Path;
if (path == "/" || String.IsNullOrEmpty(path))
return false;
if (_cache.ContainsKey(path))
return _cache[path];
IController ctrl;
try
{
var ctrlFactory = ControllerBuilder.Current.GetControllerFactory();
ctrl = ctrlFactory.CreateController(httpContext.Request.RequestContext, values["action"] as string);
}
catch
{
_cache.Add(path, true);
return true;
}
var res = ctrl == null;
_cache.Add(path, res);
return res;
}
}
Then applying the constraint:
routes.MapRoute(
name: "ShortUrlToHomeActions",
url: "{action}",
defaults: new { controller = "Home" },
constraints: new { noConflictingControllerExists = new NoConflictingControllerExists() }
);
See MSDN
I'm creating a cut down version of a CMS using MVC 5 and I'm trying to get through the routing side of things.
I need to handle pages with urls such as /how-it-works/ and /about-us/ etc and therefore content is keyed on these paths.
In my RouteConfig file I'm using a 'catch all' route as follows::
routes.MapRoute("Static page", "{*path}", new { controller = "Content", action = "StaticPage" });
This successfully hits the controller action I'm looking, however it therefore means that requests for controller actions that actually do exist (for example /navigation/main also get sent down this route).
I know that I can have a route that matches /navigation/main however I'd rather configure MVC to do this as default, like it does when I don't add the rule I have above, any ideas?
Add your "catch all" route on top of "default" route and add a route constrain to path like this:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Static page",
"{*path}",
new { controller = "Content", action = "StaticPage" }
new { path = new PathConstraint() });
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
}
PathConstraint should derive from from IRouteConstraint interface and can be something like this:
public class PathConstraint: IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (values[parameterName] != null)
{
var permalink = values[parameterName].ToString();
//gather all possible paths from database
//and check if permalink is any of them
//return true or false
return database.GetPAths().Any(p => p == permalink);
}
return false;
}
}
So if "path" is not one of your pages paths, PathConstrain will not be satisified and "Static page" route will be skiped and pass to next route.
can I have domain.com/action/id as well as domain.com/controller/action?
how would I register these in the route-table?
Is ID always guaranteed to be a number? If yes, then you could use RouteConstraints:
routes.MapRoute("ActionIDRoute",
"{action}/{id}",
new { controller = "SomeController" },
new {id= new IDConstraint()});
routes.MapRoute("ControllerActionRoute",
"{controller}/{action}",
new {}); // not sure about this last line
The IDConstraint class looks like this:
public class IDConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
var value = values[parameterName] as string;
int ID;
return int.TryParse(value,out ID);
}
}
Basically what is happening is that you have two identical routes here - two parameters, so it's ambigous. Route Constraints are applied to parameters to see if they match.
So:
You call http://localhost/SomeController/SomeAction
It will hit the ActionIDRoute, as this has two placeholders
As there is a constraint on the id parameter (SomeAction), ASP.net MVC will call the Match() function
As int.TryParse fails on SomeAction, the route is discarded
The next route that matches is the ControllerActionRoute
As this matches and there are no constraints on it, this will be taken
If ID is not guaranteed to be a number, then you have the problem to resolve the ambiguity. The only solution I am aware of is hardcoding the routes where {action}/{id} applies, which may not be possible always.
Yes, you can add a new rule above the default rule and provide a default value for the controller.
routes.MapRoute(
"MyRole", // Route name
"{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
The sample routs all actions to the "Home" controller.
For a scenario, I have a ASP.NET MVC application with URLs that look like the following:
http://example.com/Customer/List
http://example.com/Customer/List/Page/2
http://example.com/Customer/List
http://example.com/Customer/View/8372
http://example.com/Customer/Search/foo/Page/5
These URLs are achieved with following routes in Global.asax.cs
routes.MapRoute(
"CustomerSearch"
, "Customer/Search/{query}/Page/{page}"
, new { controller = "Customer", action = "Search" }
);
routes.MapRoute(
"CustomerGeneric"
, "Customer/{action}/{id}/Page/{page}"
, new { controller = "Customer" }
);
//-- Default Route
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Customer", action = "Index", id = "" }
);
These all have gone well until a new requirement arrived and wants to drop keyword 'Customer' off the URL, to make the URLs look like:
http://example.com/List
http://example.com/List/Page/2
http://example.com/List
http://example.com/View/8372
http://example.com/Search/foo/Page/5
Edit: corrected example links, thanks to #haacked.
I tried to add new MapRoutes to take {action} only and have default controller set to Customer. eg/
routes.MapRoute(
"CustomerFoo"
, "{action}"
, new { controller = "Customer", action = "Index" }
);
This seems to work, however now all links generated by Html.ActionLink() are weird and no longer URL friendly.
So, is this achievable? Am I approaching in the right direction?
don't mix a rule like: "{action}/{id}" with one that's "{controller}/{action}/{id}" ... specially when id in the later has a default value i.e. is optional.
In that case you have nothing that allows routing to know which one is the right one to use.
A workaround, if that's what you need, would be to add a constrain (see this) to the action in the earlier to a set of values i.e. List, View. Of course that with these types of rules, you can't have a controller with the same name of an action.
Also remember that if you specify a default action & id in the "{action}/{id}" rule, that will be used when you hit the route of your site.
Why does the first URL in the new list still have "Customer". I assume that's a typo and you meant:
http://example.com/List
http://example.com/List/Page/2
http://example.com/List
http://example.com/View/8372
http://example.com/Search/foo/Page/5
The following routes work for me:
routes.MapRoute(
"CustomerSearch"
, "Search/{query}/Page/{page}"
, new { controller = "Customer", action = "Search" }
);
routes.MapRoute(
"CustomerGeneric"
, "{action}/{id}/Page/{page}"
, new { controller = "Customer" }
);
//-- Default Route
routes.MapRoute(
"Default",
"{action}/{id}",
new { controller = "Customer", action = "Index", id = "" }
);
How are you generating your links. Since the Controller is no longer in the URL of your route (aka, you don't have "{controller}" in the route URL), but it's a default value, you need to make sure to specify the controller when generating routes.
Thus instead of
Html.ActionLink("LinkText", "ActionName")
do
Html.ActionLink("LinkText", "ActionName", "Customer")
Why? Suppose you had the following routes.
routes.MapRoute(
"Default",
"foo/{action}",
new { controller = "Cool" }
);
routes.MapRoute(
"Default",
"bar/{action}",
new { controller = "Neat" }
);
Which route did you mean when you call this?
<%= Html.ActionLink("LinkText", "ActionName") %>
You can differentiate by specifying the controller and we'll pick the one that has a default value that matches the specified one.
You can create a route that is constrained to only match actions in your Customer controller.
public static class RoutingExtensions {
///<summary>Creates a route that maps URLs without a controller to action methods in the specified controller</summary>
///<typeparam name="TController">The controller type to map the URLs to.</typeparam>
public static void MapDefaultController<TController>(this RouteCollection routes) where TController : ControllerBase {
routes.MapControllerActions<TController>(typeof(TController).Name, "{action}/{id}", new { action = "Index", id = UrlParameter.Optional });
}
///<summary>Creates a route that only matches actions from the given controller.</summary>
///<typeparam name="TController">The controller type to map the URLs to.</typeparam>
public static void MapControllerActions<TController>(this RouteCollection routes, string name, string url, object defaults) where TController : ControllerBase {
var methods = typeof(TController).GetMethods()
.Where(m => !m.ContainsGenericParameters)
.Where(m => !m.IsDefined(typeof(ChildActionOnlyAttribute), true))
.Where(m => !m.IsDefined(typeof(NonActionAttribute), true))
.Where(m => !m.GetParameters().Any(p => p.IsOut || p.ParameterType.IsByRef))
.Select(m => m.GetActionName());
routes.Add(name, new Route(url, new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(defaults) { { "controller", typeof(TController).Name.Replace("Controller", "") } },
Constraints = new RouteValueDictionary { { "action", new StringListConstraint(methods) } }
});
}
private static string GetActionName(this MethodInfo method) {
var attr = method.GetCustomAttribute<ActionNameAttribute>();
if (attr != null)
return attr.Name;
return method.Name;
}
class StringListConstraint : IRouteConstraint {
readonly HashSet<string> validValues;
public StringListConstraint(IEnumerable<string> values) { validValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase); }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
return validValues.Contains(values[parameterName]);
}
}
#region GetCustomAttributes
///<summary>Gets a custom attribute defined on a member.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
///<returns>The first attribute of the type defined on the member, or null if there aren't any</returns>
public static TAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider provider) where TAttribute : Attribute {
return provider.GetCustomAttribute<TAttribute>(false);
}
///<summary>Gets the first custom attribute defined on a member, or null if there aren't any.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
///<param name="inherit">Whether to look up the hierarchy chain for attributes.</param>
///<returns>The first attribute of the type defined on the member, or null if there aren't any</returns>
public static TAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider provider, bool inherit) where TAttribute : Attribute {
return provider.GetCustomAttributes<TAttribute>(inherit).FirstOrDefault();
}
///<summary>Gets the custom attributes defined on a member.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
public static TAttribute[] GetCustomAttributes<TAttribute>(this ICustomAttributeProvider provider) where TAttribute : Attribute {
return provider.GetCustomAttributes<TAttribute>(false);
}
///<summary>Gets the custom attributes defined on a member.</summary>
///<typeparam name="TAttribute">The type of attribute to return.</typeparam>
///<param name="provider">The object to get the attribute for.</param>
///<param name="inherit">Whether to look up the hierarchy chain for attributes.</param>
public static TAttribute[] GetCustomAttributes<TAttribute>(this ICustomAttributeProvider provider, bool inherit) where TAttribute : Attribute {
if (provider == null) throw new ArgumentNullException("provider");
return (TAttribute[])provider.GetCustomAttributes(typeof(TAttribute), inherit);
}
#endregion
}
I have an application here with a mix of webform and mvc. I specify the routing as below
routes.Add("AspxRoute", new Route("Upload/New", new WebFormRouteHandler<Page>("~/Uploads.aspx")));
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
So that virtual path to "Upload/New" actually maps to an aspx webform page.
But my problem is that Html.ActionLink("Test", "Controller", "Action") now renders
/Upload/New?Controller=Controller&Action=Action
Having looked at the MVC source code, I understand that it is because ActionLink calls to RouteCollection.GetVirtualPath(requestContext, routeName, mergedRouteValues), where routeName is left to null. And somehow this defaults to use the AspxRoute route to construct the url. I tried to added another route before "AspxRoute", but it seems it always defaults to the non-mvc routehandler one.
How does RouteCollection.GetVirtualPath behave when routeName is null? And why is it behaving this way for my case?
How do I construct a correct url? Do I need to write a new Htmlhelper extension?
Cheers
An alternative option would be to add a custom constraint to your WebFormRoute(s). For example, you could create an implementation of IRouteConstraint to match RouteDirection.IncomingRequest, then use it to ensure the route is ignored by Server-Generated routes (such as ActionLink) but still used by client-generated requests. Something like:
public class IncomingOnlyRouteConstraint: IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.IncomingRequest)
{
return true;
}
return false;
}
}
And then add the constraint to your route:
routes.Add("AspxRoute", new Route("Upload/New", null,
new RouteValueDictionary() { {"WebFormsConstraint", new IncomingOnlyRouteConstraint()} },
new WebFormRouteHandler<Page>("~/Uploads.aspx")));
Of course you may prefer to add your own style of constraint, this one is quite limiting on the route that implements it, but it's just an example of one way you could resolve the issue.
Try:
<%=Html.RouteLink("Test", "Default", new {controller = "Controller", action = "Action"})%>
Using RouteLink instead of ActionLink allows you to specify the route you want to use, which in this case is the Default MVC route mapping as opposed to the custom one you have added.
Also: Make sure your Default route is the LAST entry in the routing table. That's another easy way to wind up with the sort of html action link you're getting.
Force the route defaults to have no controller:
var routeDefaults = new RouteValueDictionary() { { "controller", null } };
routes.Add("RouteName", new Route("some/path", routeDefaults, new SomeHandler()));
I experienced the same thing where the routes worked correctly "inbound", but Html.ActionLink() was picking the wrong route. I worked around it by adding a route constraint so that the controller must be empty:
var constraints = new RouteValueDictionary()
{
{ "controller", string.Empty }
};
routes.Add(new Route("sso/server", null, constraints, new OpenIDServerRouteHandler()));
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{identity}",
defaults: new { controller = "Pages", action = "Home", identity = UrlParameter.Optional }
);
Since the "controller" route value is restricted to nothing, a call to ActionLink() ends up ignoring the route. Hope this helps someone!