Route gets mapped to parameters of different route - c#

I’m trying to set up some routes for my ASP.NET MVC 5 project.
I defined custom routes to get nice blog post permalinks – those
seem to be working fine
I added a XmlRpc Handler (similar to how it’s done
in Mads' Miniblog and Scott’s post)
Now I have some strange behavior:
/Home/About is routed correctly
/Home/Index gets routed to /XmlRpc?action=Index&controller=Blog
/HOme/Index works (yes I
discovered that due to a typo) – I always thought routes are case
insensitive?
Using Url.Action("Foo","Bar") also creates /XmlRpc?action=Foo&controller=Bar
This is my RouteConfig file:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("XmlRpc", new Route("XmlRpc", new MetaWeblogRouteHandler()));
routes.MapRoute("Post", "Post/{year}/{month}/{day}/{id}", new {controller = "Blog", action = "Post"}, new {year = #"\d{4,4}", month = #"\d{1,2}", day = #"\d{1,2}", id = #"(\w+-?)*"});
routes.MapRoute("Posts on Day", "Post/{year}/{month}/{day}", new {controller = "Blog", action = "PostsOnDay"}, new {year = #"\d{4,4}", month = #"\d{1,2}", day = #"\d{1,2}"});
routes.MapRoute("Posts in Month", "Post/{year}/{month}", new {controller = "Blog", action = "PostsInMonth"}, new {year = #"\d{4,4}", month = #"\d{1,2"});
routes.MapRoute("Posts in Year", "Post/{year}", new {controller = "Blog", action = "PostsInYear"}, new {year = #"\d{4,4}"});
routes.MapRoute("Post List Pages", "Page/{page}", new {controller = "Blog", action = "Index"}, new {page = #"\d{1,6}"});
routes.MapRoute("Posts by Tag", "Tag/{tag}", new {controller = "Blog", action = "PostsByTag"}, new {id = #"(\w+-?)*"});
routes.MapRoute("Posts by Category", "Category/{category}", new {controller = "Blog", action = "PostsByCategory"}, new {id = #"(\w+-?)*"});
routes.MapRoute("Default", "{controller}/{action}/{id}", new {controller = "Blog", action = "Index", id = UrlParameter.Optional});
}
And that’s the definition of MetaWeblogRouteHandler:
public class MetaWeblogRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MetaWeblog();
}
}
Basically I’d like to have the usual ASP.NET MVC routing behavior (/controller/action) + my defined custom routes for permalinks + XML-RPC handling via the XmlRpc handler only at /XmlRpc.
Since the parameters are the same that are defined in the Default route I tried to remove the route, but without success.
Any ideas?
Update:
When calling /Home/Index the AppRelativeCurrentExecutionFilePath is set to "~/XmlRpc" so the XmlRpc route is legally chosen. Something seems to be messing around with the request?
Update2: The problem fixed itself in every but one case: when starting IE via Visual Studio for Debug it still fails. In every other case it now works (yes I checked browser cache and even tried it on a different machine to be sure; IE started from VS = fail, all other combinations are fine). Anyway, since it will now work for the end user I'm satisfied for the moment ;)

When you execute Url.Action("Foo","Bar"), MVC will create a collection of route values from your inputs (In that case action=Foo, controller=Bar) and it will then look at your routes, trying to match one that matches based on its segments and default values.
Your XmlRpc route has no segments and no default values, and is the first one defined. This means it will always be the first match when generating urls using #Url.Action, #Html.ActionLink etc.
A quick way to prevent that route from being matched when generating urls would be adding a default controller parameter (using a controller name that you are sure you will never use). For example:
routes.Add("XmlRpc", new Route("XmlRpc", new RouteValueDictionary() { { "controller", "XmlRpc" } }, new MetaWeblogRouteHandler()));
Now when you execute Url.Action("Foo","Bar"), you will get the expected /Bar/Foo url, as "Bar" doesn´t match the default controller value in the route definition, "XmlRpc".
However that seems a bit hacky.
A better option would be creating your own RouteBase class. This willonly care for the url /XmlRpc, which will then be served using MetaWeblogRouteHandler and will be ignored when generating links using the Html and Url helpers:
public class XmlRpcRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
//The route will only be a match when requesting the url ~/XmlRpc, and in that case the MetaWeblogRouteHandler will handle the request
if (httpContext.Request.AppRelativeCurrentExecutionFilePath.Equals("~/XmlRpc", StringComparison.CurrentCultureIgnoreCase))
return new RouteData(this, new MetaWeblogRouteHandler());
//If url is other than /XmlRpc, return null so MVC keeps looking at the other routes
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//return null, so this route is skipped by MVC when generating outgoing Urls (as in #Url.Action and #Html.ActionLink)
return null;
}
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Add the route using our custom XmlRpcRoute class
routes.Add("XmlRpc", new XmlRpcRoute());
... your other routes ...
}
However, in the end you are creating a route just to run an IHttpHandler outside the MVC flow, for a single url. You are even struggling to keep that route from interferring with the rest of the MVC components, like when generating urls using helpers.
You could then just add directly a handler for that module in the web.config file, also adding an ignore rule for /XmlRpc in your MVC routes:
<configuration>
...
<system.webServer>
<handlers>
<!-- Make sure to update the namespace "WebApplication1.Blog" to whatever your namespace is-->
<add name="MetaWebLogHandler" verb="POST,GET" type="WebApplication1.Blog.MetaWeblogHandler" path="/XmlRpc" />
</handlers>
</system.webServer>
</configuration>
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Make sure MVC ignores /XmlRpc, which will be directly handled by MetaWeblogHandler
routes.IgnoreRoute("XmlRpc");
... your other routes ...
}
Using either of these 3 approaches, this is what I get:
/Home/Index renders the Index view of the HomeController
/ renders the Index view of the BlogController
#Url.Action("Foo","Bar") generates the url /Bar/Foo
#Html.ActionLink("MyLink","Foo","Bar") renders the following html: MyLink
/XmlRcp renders the a view describing the MetaWeblogHandler and its available methods, where there is a single method available (blog.index, taking no parameters and returning a string)
In order for testing this, I have created a new empty MVC 5 application, adding the NuGet package xmlrpcnet-server.
I have created a HomeController and a BlogController, both with an index action, and I have created the following MetaWeblog classes:
public interface IMetaWeblog
{
[XmlRpcMethod("blog.index")]
string Index();
}
public class MetaWeblogHandler : XmlRpcService, IMetaWeblog
{
string IMetaWeblog.Index()
{
return "Hello World";
}
}
public class MetaWeblogRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MetaWeblogHandler();
}
}

Related

Redirect to Area without changing the URL and TempData

In my project I have to redirect from one controller to another controller which is present inside the Areas named SIP. If use the following method the redirection works successfully and also the TempData value is passed to the other controller:
TempData["sipModel"] = 1;
return RedirectToAction("Index", "Home", new { area = "SIP" });
But in this case the URL gets changed while my requirement is to keep the same URL, to achieve that I went though other answers and used the method TransferToAction() mentioned
in this answer
This works perfectly and I'm able to redirect to the other area without changing the URL with the following code:
TempData["sipModel"] = 1;
return this.TransferToAction("Index", "Home", new { area = "SIP"});
However, in this case the TempData value is not retained and I get Null Reference Exception while trying to read the same.
I tried to use the following code mentioned in the other answer:
public static TransferToRouteResult TransferToAction(this System.Web.Mvc.Controller controller, string actionName, string controllerName, object routeValues)
{
controller.TempData.Keep();
controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
return new TransferToRouteResult(controller.Request.RequestContext, actionName, controllerName, routeValues);
}
But this doesn't work out. Can somebody please suggest me how can I fix this or any other better approach to achieve this result. Thanks.
Edited:
The URL is like:
https://myproject/Home/Index?cid=ABC-1234&pid=xyz123456abc
I have a complex data in a class which also needs to be passed from the one controller to the other (which is present in the Area SIP), for that I've been using TempData, I've used an integer here just as a sample.
In the first controller method I've if-else condition, so:
if (companyCode = 'X')
return View();
else
TempData["sipModel"] = 1;
return RedirectToAction("Index", "Home", new { area = "SIP" }); OR (this.TransferToAction("Index", "Home", new { area = "SIP"});)
Server.TransferRequest is completely unnecessary in MVC. This is an antiquated feature that was only necessary in ASP.NET because the request came directly to a page and there needed to be a way to transfer a request to another page. Modern versions of ASP.NET (including MVC) have a routing infrastructure that can be customized to route directly to the resource that is desired. There is no point of letting the request reach a controller only to transfer it to another controller when you can simply make the request go directly to the controller and action you want.
So, given your example is not a complete set of requirements I will make the following assumptions. Adjust these as necessary for your requirements.
If there are no query string parameters passed to the home page, it will stay on the home page.
If there is a query parameter cid or pid on the home page, we will send the request to the Index action of the HomeController of the SID area.
We will pass a metadata parameter "sipModel" with value 1 in the latter case and omit the parameter in the first case.
First of all, we subclass RouteBase and put our custom logic there. A more complete scenario might have dependent services and options passed in through the constructor, and even have its own MapRoute extension methods to wire it together.
public class CustomHomePageRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Only handle the home page route
if (httpContext.Request.Path == "/")
{
var cid = httpContext.Request.QueryString["cid"];
var pid = httpContext.Request.QueryString["pid"];
result = new RouteData(this, new MvcRouteHandler());
if (string.IsNullOrEmpty(cid) && string.IsNullOrEmpty(pid))
{
// Go to the HomeController.Index action of the non-area
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
// NOTE: Since the controller names are ambiguous between the non-area
// and area route, this extra namespace info is required to disambiguate them.
// This is not necessary if the controller names differ.
result.DataTokens["Namespaces"] = new string[] { "WebApplication23.Controllers" };
}
else
{
// Go to the HomeController.Index action of the SID area
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
// This tells MVC to change areas to SID
result.DataTokens["area"] = "SID";
// Set additional data for sipModel.
// This can be read from the HomeController.Index action by
// adding a parameter "int sipModel".
result.Values["sipModel"] = 1;
// NOTE: Since the controller names are ambiguous between the non-area
// and area route, this extra namespace info is required to disambiguate them.
// This is not necessary if the controller names differ.
result.DataTokens["Namespaces"] = new string[] { "WebApplication23.Areas.SID.Controllers" };
}
}
// If this isn't the home page route, this should return null
// which instructs routing to try the next route in the route table.
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (controller.Equals("Home", StringComparison.OrdinalIgnoreCase) &&
action.Equals("Index", StringComparison.OrdinalIgnoreCase))
{
// Route to the Home page URL
return new VirtualPathData(this, "");
}
return null;
}
}
To wire this into MVC, we just edit the RouteConfig as follows:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Add the custom route to the static routing collection
routes.Add(new CustomHomePageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "WebApplication23.Controllers" }
);
}
}
This passes an extra route value sipModel to the PID area's HomeController.Index method. So, we need to adjust the method signature to accept that parameter.
namespace WebApplication23.Areas.SID.Controllers
{
public class HomeController : Controller
{
// GET: SID/Home
public ActionResult Index(int sipModel)
{
return View();
}
}
}
As you can see, there really is no reason to use TempData, either. TempData relies on session state by default. It has its uses, but you should always think twice about using session state in MVC as it can often be avoided entirely.

urls like "home.asp" redirect in mvc

I need to rebuild a website (in old they use classic ASP but they want to make it now MVC 4) and they dont want to change the urls. for example if the search screen's url is blabla.com/search.asp they want to keep it like this. But mvc doesn't allow to make urls like "search.asp". I want to keep url like this but render search View. Am I need to do this all one by one or there is a dynamic way for it?
for example
requested url = "blabla.com/string variable"
if variable.Substring(variable.Length - 4) == ".asp";
return View("variable.Substring(0, (variable.Length - 4))")
Note: Syntax is all wrong, I know. I just tried to explain the condition..
Note2: They want this because of "SEO" things. they don't want to lose their ratings. any method that doesn't change anything for google, they will accept that method I guess.
You need two things.
Define a route for *.asp
Add an handler for *.asp
RouteConfig
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "DefaultAsp",
url: "{controller}/{action}.asp/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Handler (WebConfig)
(This one needs to be inserted inside /system.webServer/handlers
<add name="AspFileHandler" path="*.asp" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
Doing this you are also making all URL's built with MVC available with .asp, that means that if you have an anchor that calls another View, that view will have the normal mvc URL with .asp suffix.
Note
This is generic, you only need to add this line once.
Home/Index displayed are just the default Controller and Action.
Note 2
Forgot to explain the handler.
You need it because IIS thinks you are asking for an ASP file an it'll try to reach that page and return an error since it doesn't exist.
With this handler you are allowing your application to handle those pages itself.
MVC does allows you to write a Route containing an extension, and pointing it to a specific Controller:
In RouteConfig.cs:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Test", "test.asp", new {controller = "Test", action = "test" });
}
Then, in your TestController.cs:
public class TestController : Controller
{
public ActionResult Test()
{
var obj = new Foo();
//Do some processing
return View(obj);
}
}
In this way, you can access http://www.foo.com/test.asp without issues, and maintaining the .asp your client requires.

If Route doesn't exist -> go to other route using its default action?

So I'm having a little problem here with routing.
There are two parts to this web application:
  1. Brochure / Display Website
  2. Internal Site / Client Application
We wanted a way to release changes for the brochure without having to do a whole release of said Web application.
Visiting existing named views will take the user to a brochure page, however if it doesn't exist, it will act like they are a client and will redirect them to their company's login screen.
Global.asax:
//if view doesnt exist then url is a client and should be redirected
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
);
//This is home page
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
Controller:
/// <summary> Check if the view exists in our brochure list </summary>
private bool ViewExists(string name) {
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, name, null);
return (result.View != null);
}
/// <summary> Generic action result routing for all pages.
/// If the view doesn't exist in the brochure area, then redirect to interal web
/// This way, even when we add new pages to the brochure, there is no need to re-compile & release the whole Web project. </summary>
public ActionResult Brochure(string id) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToRoute("HomeDefault", new { client = id });
}
This code works fine up until we log in and go to the landing page. It seems to keep the Brochure action in the route and doesn't want to go to the subsequent controller which results in a 500 error.
e.g. 'domain/client/Brochure' when it needs to be: 'domain/client/Index'
Things tried but not worked:
Changing RedirectToRoute() to a RedirectToAction() - this results in a
finite loop of going back to the ActionResult Brochure(). So
changing controllers through that didn't work.
Create an ActionResult called Brochure() inside the 'HomeController'. It
doesn't even get hit.
Passed in namespaces for RedirectToRoute() as an attribute. I knew this would probably not work, but it was worth a try.
So the question is:
How can I get the route to act properly?
If you can restrict id to some subset of all values you can add that constraints to route (i.e. numbers only) to let default handle the rest.
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
constraints : new { category = #"\d+"}
);
If you can't statically determine restrictions - automatically redirecting in your BrochureController similar to your current code would work. The only problem with sample in the question is it hits the same route again and goes into infinite redirect loop - redirect to Url that does not match first rule:
// may need to remove defaults from second route
return RedirectToRoute("HomeDefault", new { client = id, action = "index" });
If standard constraints do not work and you must keep single segment in url - use custom constraints - implement IRouteConstraint and use it in first route. See Creating custom constraints.
There are several issues with your configuration. I can explain what is wrong with it, but I am not sure I can set you on the right track because you didn't provide the all of the URLs (at least not all of them from what I can tell).
Issues
Your Brouchure route, which has 1 optional URL segment named {id}, will match any URL that is 0 or 1 segments (such as / and /client). The fact that it matches your home page (and you have another route that is named HomeDefault that will never be given the chance to match the home page) leads me to believe this wasn't intended. You can make the {id} value required by removing the default value id = "Index".
The Brouchure route has a namespace that indicates it is probably in an Area. To properly register the area, you have to make the last line of that route ).DataTokens["area"] = "Brochure"; or alternatively put it into the /Areas/Brouchure/AreaRegistration.cs file, which will do that for you.
The only way to get to the HomeDefault route is to supply a 2 segment URL (such as /client/Index, which will take you to the Index method on the HomeController). The example URLs you have provided have 3 segments. Neither of the routes you have provided will match a URL with 3 segments, so if these URLs are not getting 404 errors they are obviously matching a route that you haven't provided in your question. In other words, you are looking for the problem in the wrong place.
If you provide your entire route configuration including all Area routes and AttributeRouting routes (including the line that registers them), as well as a complete description of what URL should go to what action method, I am sure you will get more helpful answers.
So the question is:
How can I get the route to act properly?
Unknown. Until you describe what properly is.
Related: Why map special routes first before common routes in asp.net mvc?
Two ways I could have solved this issue:
Way 1
I reviewed the redirect and just passed in an action in order to get a route that has 2 segments in the url. i.e. client/Index. The Index action now handles logins - going past a custom controller.
public class HomeController : CustomController
public ActionResult Brochure(string id, string action) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToAction("Index", "Home", new { client = id, action = "Index" });
}
Way 2
(from #Alexei_Levenkov)
Create a custom Route constraint so the route will be ignored if the view cannot be found.
namespace Web.Contraints {
public class BrochureConstraint : IRouteConstraint {
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
//Create our 'fake' controllerContext as we cannot access ControllerContext here
HttpContextWrapper context = new HttpContextWrapper(HttpContext.Current);
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "brochure");
ControllerContext controllerContext = new ControllerContext(new RequestContext(context, routeData), new BrochureController());
//Check if our view exists in the folder -> if so the route is valid - return true
ViewEngineResult result = ViewEngines.Engines.FindView(controllerContext, "~/Areas/Brochure/Views/Brochure/" + values["id"] + ".cshtml", null);
return result.View != null;
}
}
}
namespace Web {
public class MvcApplication : System.Web.HttpApplication {
//If view doesnt exist then url is a client so use the 'HomeDefault' route below
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" },
constraints: new { isBrochure = new BrochureConstraint() }
);
//This is home page for client
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
}
}
I hope this helps someone else out there.

MVC Routing config: URL Pattern with a mixed Segment and defaults

I have the following routing configuration under a MVC sample project:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("", "X{controller}/{action}",
new { controller = "Customer", action = "List" });
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
}
}
I redirect all controllers (Home, Customer) to the same view that displays the Current controller and action name.
So, for the URL http://localhost:5O44O/XCustomer I have the following output:
The controller is: Customer
The action is: List
I expected that for the URL http://localhost:5O44O/X I should have the same output... But this is not the case...
Server Error in '/' Application.
The resource cannot be found.
Description: HTTP 404. The resource you
are looking for (or one of its dependencies) could have been removed,
had its name changed, or is temporarily unavailable. Please review
the following URL and make sure that it is spelled correctly.
Requested URL: /X
Why that? I placed the "X" condition first, so I should obtain the default replacements with Customer and List ?!
You a are receiving 404 error, because you don't have XController. If you have it, you'd receive route: http://localhost:5O44O/XX
routes.MapRoute("", "X{controller}/{action}" - this is just a syntaxis to generete string of route. And it doesn't have a behavior you were expected.
All manipulations should be done here:
new { controller = "Customer", action = "List" });
If you want to have such route: http://localhost:5O44O/X/List you need to write your MapRoute as follows:
routes.MapRoute("name", "X/{action}",
new { controller = "Customer", action = "List" });
You may even write:
routes.MapRoute("name", "HelloBro",
new { controller = "Customer", action = "List" });
It will return you route http://localhost:5O44O/HelloBro for your List action of Customer controller

Url generation not giving desired results according to route config

I am using ASP.NET MVC 4.
I have a controller called Server, and 2 action methods called Search and Component. I have the following route configuration:
routes.MapRoute("Component",
"{controller}/{serverId}/{action}",
new { controller = "Server", action = "Component" },
new { serverId = #"\d+" });
I am looking for a url similar to:
/Server/12345/Component
My Search action method:
return RedirectToAction("Component", new { serverId = 12345 });
My Component action method:
public ActionResult Component(int serverId)
{
return View();
}
The url that is generated is:
/Server/12345/
It is wrong, it is leaving out "Component". Why is this?
new { controller = "Server", action = "Component" },
Becase you are setting the default action to "Component", I think the link generation is smart enough to leave it off.
You defined Component as Default-Action, so why should it been appended?
If you want it in your route, then remove it from default and add it to your RedirectToAction call.

Categories