How to have a route that goes to 2 destinations - asp.net

I need the ability to have routes like the following URLs.
/authorid
/documentid
/authorid/documentid
I am trying to not to do
/author/1
/document/5
author/1/document/25
How can this be done?
Clarifications.
A document and an author can have the same id. BUT the author should take precedence.
Author: {ID: "djQuery" }
Document: { Aurhor: "Bob", ID: "djQuery" }
Should be accessible via /Bob/djQuery
But if you just go to /djQuery you should get a list of djQuery's documents.

You can achieve what you want with three routes and the appropriate RouteConstraints.
First, you need to establish your Routes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Author_Document", // Route name
"{author}/{document}", // URL
new { controller = "ShowDocument", action = "AuthorDocument" }, // Parameters
new { author = new MustBeValidAuthor(), document = new MustBeValidDocument() }
routes.MapRoute(
"Author", // Route name
"{author}", // URL
new { controller = "ShowDocument", action = "Author" }, // Parameters
new { author = new MustBeValidAuthor() }
routes.MapRoute(
"Document", // Route name
"{document}", // URL
new { controller = "ShowDocument", action = "Document" }, // Parameters
new { document = new MustBeValidDocument() }
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
Next, you need to create your RouteConstraints, which must verify that the information provided is valid:
using System;
using System.Web;
using System.Web.Routing;
namespace ExampleApp.Extensions
{
public class MustBeValidAuthor : IRouteConstraint
{
public MustBeValidAuthor() { }
private DbContext _db = new DbContext();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return (_db.Authors.Where(u => u.AuthorName == values[parameterName].ToString()).Count() > 0);
}
}
public class MustBeValidDocument : IRouteConstraint
{
public MustBeValidDocument() { }
private DbContext _db = new DbContext();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return (_db.Documents.Where(u => u.DocumentName == values[parameterName].ToString()).Count() > 0);
}
}
}
The RouteConstraints will enfoce the validity of the request, and placing the Routes in this order fulfills your requirements.

I do not think that it is possible to do this just by configuring routes.
The problem is how does the program know if the number that you send is an authorid or a documentid.
What you can do is that /1 goes to a page, on that page you do a check to see if it is a document or an author, then you do a Response.Redirect to the actual page, say /author/1.

Assuming you want
/1
/2
/1/2
I don't think you can achive that beacuse the first two urls will call the same routing rule.
You can't know when to call a method instead of another.
The only solution I can think of, is that if you can discern the ids (for example id from 1 to 10 are authorid while the others are documentId) you can write your custom rulehandler and act accordinlgy calling the right method.
Otherwise you have to put something in the url to make it unique:
"/a1" for authorid and "/d2" for DoumentId would have something like that
routes.MapRoute("", "a{authorId}", new { controller = "Home", action = "AuthorDetail" }, new { authorId = #"\d+" });
routes.MapRoute("", "d{documentId}", new { controller = "Home", action = "DocumentDetail" }, new { documentId = #"\d+" });
EDIT: after your clarification the answer from counsellorben should do what you want

You can use something like this where IsAuthorConstraint has a look up to determine if document or author. Alternatively you can route to a bridging Action and put the logic there, before passing on to document or author action.
context.MapRoute(
"authorid",
"{authorid}",
new { action = "Author", controller = "Controller",
authorid = -1 },
new { authorid = new IsAuthorConstraint() }
);
context.MapRoute(
"document",
"{documentid}",
new { action = "Document", controller = "Controller",
documentid = -1 }
);
// assume overload on author action
context.MapRoute(
"document and author",
"{authorid}/{documentid}",
new { action = "Author", controller = "Controller",
documentid = -1, authorid = -1 }
);
I'm not at a computer so may not be exactly right, but hope you get the idea.

You can only achieve this with Route constraints as #counsellorben suggested but there should be no same id for authorid and documentid.
If there is, a conflict is going to occur there.

Related

Getting an Actionresult Parameter in MVC4

I'm working on a simple MVC application, where I am generating below path
#Html.ActionLink(#objcity.CityName, "AgentProfiles", "Home", new {#Id=objcity.MaProvision.ProvinceName+"/"+#objcity.CityName }, null)
It makes a url like this:
http://localhost:45896/Home/AgentProfiles/Ontario/testt
In the controller I have written this method:
public ActionResult AgentProfiles(String Id)
{
//Code
}
Is it possible to get in /Ontario/testt in Id variable?
You want to get /Ontario/testt in Id(route parameter) for this you have to modify your default routes little bit or you have to make a custom route but in my opinion for your simple requirement try below answer.
Instead of
#Html.ActionLink(#objcity.CityName, "AgentProfiles", "Home", new {#Id=objcity.MaProvision.ProvinceName+"/"+#objcity.CityName }, null)
Modify Actionlink this way
#Html.ActionLink(#objcity.CityName, "AgentProfiles", "Home", new { ProvinceName=objcity.MaProvision.ProvinceName ,CityName = objcity.CityName }, null)
Controller Action :
public ActionResult AgentProfiles(string ProvinceName,string CityName ) //get ProvinceName and CityName which will be coming as querystring variables as shown here.
{......}
OR
EDIT :- Try this as per your comment.
Add one more route in RouteConfig.cs file inside AppStart folder as shown below :
routes.MapRoute(
"MvcRoutes", // Route name
"{controller}/{action}/{provincename}/{cityname}", // URL with parameters
new { controller = "Home", action = "Index", provincename = "", cityname= "" } // Parameter defaults
);
Don't forget to put this custom route above default route.
Modify ActionLink as shown below :
#Html.ActionLink(#objcity.CityName, "AgentProfiles", "Home", new { provincename = objcity.MaProvision.ProvinceName , cityname = objcity.CityName }, null)
Controller Action :
public ActionResult AgentProfiles(string provincename ,string cityname)
{......}
you can modify your routing like-
{controller}/{action}/{*catchall}
and in action method
public ActionResult AgentProfiles(string catchall)
{
// your code
}
Then you will have value /Ontario/testt in your catchall parameter which you can use in action method.

Duplicate routes need to fallback to next matching route when no Action is found

I have a situation where I need to override methods in an old HttpHandler API in an ASP.NET WebForms application. I'm using WebAPI as the base for the new API and it's been working great except I need the routing to fallback when an action is not found in the new API. This is not true currently, instead I get a "404: Not Found" whenever the WebAPI controller does not have a matching Action.
My routing for the new API is as follows:
var apiroute = routes.MapHttpRoute(
"API Default",
"api/{language}-{country}/{controller}/{action}/{id}",
new { id = RouteParameter.Optional }
);
The old Httphandler APIs register their routes as follows (order is after WebAPI route):
var route = new Route("api/{language}-{country}/Person/{command}", this);
RouteTable.Routes.Add(route);
I want /api/en-US/Person/List to match the first route if theres a PersonController with a List action but to fallback to the old api if there is not.
Question: Is it possible to add a filter to the first route to only match if the Action is indeed available on the controller? How else could I accomplish this?
Update
You may also want to investigate attribute based routing this may be a cleaner solution only the actions you annotate with the routing attributes will be matched to the route and that may help you achieve what you want.
Or
Although there may be better ways than this, it is possible if you implement a custom IRouteConstraint e.g. this article.
A highly rough and ready approach you could improve upon is below:
public class IfActionExistsConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var actionName = values["Action"] as string;
if (actionName == null)
{
return false;
}
var controllerName = values["Controller"] as string;
var controllerTypeResolver = GlobalConfiguration.Configuration.Services.GetHttpControllerTypeResolver();
var controllerTypes = controllerTypeResolver.GetControllerTypes(GlobalConfiguration.Configuration.Services.GetAssembliesResolver());
var controllerType = controllerTypes.SingleOrDefault(ct => ct.Name == string.Format("{0}Controller", controllerName));
if(controllerType == null)
{
return false;
}
return controllerType.GetMethod(actionName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) != null;
}
}
Registration:
var apiroute = routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{language}-{country}/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { IfActionExistsConstraint = new IfActionExistsConstraint() }
);

Area routes being ignored?

I have these routes:
routes.MapRoute(
"Advertisers",
"advertisers/{controller}/{action}/{id}",
new { controller = "Index", action = "Index", id = UrlParameter.Optional },
new string[] { "Portal.Areas.Advertisers.Controllers" }
);
routes.MapRoute(
"Root", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Index", action = "Index", id = UrlParameter.Optional }, // Parameter defaultsm
new string[] { "Portal.Controllers" }
);
However whenever I go to /advertisers/controller/action/id it is not reading the id parameter... what am I doing wrong?
Thanks
I'd suggest you take a look at the Route Debugger
nuget install
PM> Install-Package routedebugger
After you've installed it into your project, put this one line of code inside your application start method, and hit the url you're debugging.
protected void Application_Start()
{
RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
}
This will tell you exactly why your routes aren't working as expected.
As for your actual question, is your Controller actually called IndexController? Because this doesn't seem right to me
public class IndexController : Controller
{
public ActionResult Index()
{
return View();
}
}
My assumption is that you actually have something like HomeController or AdvertiserController, and if that's the case you should have something like this
routes.MapRoute(
"advertisers_default", // Route name
"advertisers/{controller}/{action}/{id}/{advertiserName}", // URL with parameters
new { controller = "Home",
action = "Index",
advertiserName = UrlParameter.Optional },
new { id = "[0-9]+",
controller = "[a-zA-Z]+",
action = "[a-zA-Z]+" }
);
and then hit the url http://example.com/advertisers/{id}/{advertiser-name}
Simply said, this url looks wrong to me
/advertisers/controller/action/{id}
it should be
/advertisers/home/{id}
or even
/advertisers/home/{id}/{advertiser-name}

ASP.NET MVC 3 Route Constraint : Regular Expression for not null

I am trying to create a route constraint but not sure what would be the best for this. Here is the route with no constraint :
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty" },
new { tag = #"" } //Here I would like to put a RegEx for not null match
);
What would be the best solution for this?
Could you create an IRouteConstraint:
public class NotNullRouteConstraint : IRouteConstraint
{
public bool Match(
HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
return (values[parameterName] != null);
}
}
Which you can wire up:
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty" },
new { tag = new NotNullRouteConstraint() }
);
Why do you need a constraint for a not null/empty match? Normally if you define your route like this:
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty" },
);
and tag is not specified in the request url this route simply won't match.
And if you want a token to be optional, then:
context.MapRoute(
"Accommodation_accomm_tags",
"accomm/{controller}/{action}/{tag}",
new { action = "Tags", controller = "AccommProperty", tag = UrlParameter.Optional },
);
Constraints are used when you want to constrain the value of a given route token to some specific format.
At first, I tried to create a RegEx for empty string which is ^$ (which is what null would be). However, it doesn't look like route constraints can be !=. How about matching one or more character with ^.+$?
So:
tag = #"^.+$"

ASP.NET MVC catch-all routing

I have read a few threads on StackOverflow about this, but cannot get it to work. I have this at the end of my RegisterRoutes in Global.asax.
routes.MapRoute(
"Profile",
"{*url}",
new { controller = "Profile", action = "Index" }
);
Basically what I'm trying to achieve is to have mydomain.com/Username point to my member profilepage. How would I have to set up my controller and RegisterRoutes in order for this to work?
Currently mydomain.com/somethingthatisnotacontrollername gets a 404-error.
Solution that works for your case but is not recommended
You have a predefined set of controllers (usually less than 10) in your application so you can put a constraint on controller name and then route everything else to user profile:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "Home|Admin|Reports|..." }
);
routes.MapRoute(
"Profile",
"{username}/{action}",
new { controller = "Profile", action = "Details" }
);
But this will not work in case some username is the same as your controller name. It's a small possibility based on experience end empirical data, but it's not 0% chance. When username is the same as some controller it automatically means it will get handled by the first route because constraints won't fail it.
Recommended solution
The best way would be to rather have URL requests as:
www.mydomain.com/profile/username
Why do I recommend it to be this way? Becasue this will make it much simpler and cleaner and will allow to have several different profile pages:
details www.mydomain.com/profile/username
settings www.mydomain.com/profile/username/settings
messages www.mydomain.com/profile/username/messages
etc.
Route definition in this case would be like this:
routes.MapRoute(
"Profile",
"Profile/{username}/{action}",
new { controller = "Profile", action = "Details" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
having something that matches to mydomain.com/Username isn't really going to work because there is no way for the routing engine to distinguish between
mydomain.com/someusername
and
mydomain.com/controllername
What might be possible is, if your username scheme has a unique set of properties, i.e. is a sequence of 9 digits, you could define a route to check for something that looks like a username.
routes.MapRoute("",
"UserRoute",
"{username}",
new { controller = "Profile", action = "Index"},
new { {"username", #"\d{9}"}}
);
The key point tho, is that you need to provide some way for the routing engine to differentiate between a username and a standard controller action
You can find out more about constraints here
I had a requirement like this for my project. What i did was creating a route constraint like below:
public class SeoRouteConstraint : IRouteConstraint
{
public static HybridDictionary CacheRegex = new HybridDictionary();
private readonly string _matchPattern = String.Empty;
private readonly string _mustNotMatchPattern;
public SeoRouteConstraint(string matchPattern, string mustNotMatchPattern)
{
if (!string.IsNullOrEmpty(matchPattern))
{
_matchPattern = matchPattern.ToLower();
if (!CacheRegex.Contains(_matchPattern))
{
CacheRegex.Add(_matchPattern, new Regex(_matchPattern));
}
}
if (!string.IsNullOrEmpty(mustNotMatchPattern))
{
_mustNotMatchPattern = mustNotMatchPattern.ToLower();
if (!CacheRegex.Contains(_mustNotMatchPattern))
{
CacheRegex.Add(_mustNotMatchPattern, new Regex(_mustNotMatchPattern));
}
}
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var matchReg = string.IsNullOrEmpty(_matchPattern) ? null : (Regex)CacheRegex[_matchPattern];
var notMatchReg = string.IsNullOrEmpty(_mustNotMatchPattern) ? null : (Regex)CacheRegex[_mustNotMatchPattern];
var paramValue = values[parameterName].ToString().ToLower();
return IsMatch(matchReg, paramValue) && !IsMatch(notMatchReg, paramValue);
}
private static bool IsMatch(Regex reg, string str)
{
return reg == null || reg.IsMatch(str);
}
}
Then in the register route method:
routes.MapRoute("",
"UserRoute",
"{username}",
new { controller = "Profile", action = "Index"},
new { username = new SeoRouteConstraint(#"\d{9}", GetAllControllersName())}
);
The method GetAllControllersName will return all the controller name in your project separated by | :
private static string _controllerNames;
private static string GetAllControllersName()
{
if (string.IsNullOrEmpty(_controllerNames))
{
var controllerNames = Assembly.GetAssembly(typeof(BaseController)).GetTypes().Where(x => typeof(Controller).IsAssignableFrom(x)).Select(x => x.Name.Replace("Controller", ""));
_controllerNames = string.Join("|", controllerNames);
}
return _controllerNames;
}

Resources