How to simulate Server.Transfer in ASP.NET MVC? - asp.net-mvc

In ASP.NET MVC you can return a redirect ActionResult quite easily:
return RedirectToAction("Index");
or
return RedirectToRoute(new { controller = "home", version = Math.Random() * 10 });
This will actually give an HTTP redirect, which is normally fine. However, when using Google Analytics this causes big issues because the original referrer is lost, so Google doesn't know where you came from. This loses useful information such as any search engine terms.
As a side note, this method has the advantage of removing any parameters that may have come from campaigns but still allows me to capture them server side. Leaving them in the query string leads to people bookmarking or twitter or blog a link that they shouldn't. I've seen this several times where people have twittered links to our site containing campaign IDs.
Anyway, I am writing a 'gateway' controller for all incoming visits to the site which I may redirect to different places or alternative versions.
For now I care more about Google for now (than accidental bookmarking), and I want to be able to send someone who visits / to the page that they would get if they went to /home/7, which is version 7 of a homepage.
Like I said before if I do this I lose the ability for google to analyse the referrer:
return RedirectToAction(new { controller = "home", version = 7 });
What I really want is a
return ServerTransferAction(new { controller = "home", version = 7 });
which will get me that view without a client side redirect.
I don't think such a thing exists, though.
Currently the best thing I can come up with is to duplicate the whole controller logic for HomeController.Index(..) in my GatewayController.Index Action. This means I had to move 'Views/Home' into 'Shared' so it was accessible. There must be a better way.

How about a TransferResult class? (based on Stans answer)
/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : ActionResult
{
public string Url { get; private set; }
public TransferResult(string url)
{
this.Url = url;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var httpContext = HttpContext.Current;
// MVC 3 running on IIS 7+
if (HttpRuntime.UsingIntegratedPipeline)
{
httpContext.Server.TransferRequest(this.Url, true);
}
else
{
// Pre MVC 3
httpContext.RewritePath(this.Url, false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(httpContext);
}
}
}
Updated: Now works with MVC3 (using code from Simon's post). It should (haven't been able to test it) also work in MVC2 by looking at whether or not it's running within the integrated pipeline of IIS7+.
For full transparency; In our production environment we've never use the TransferResult directly. We use a TransferToRouteResult which in turn calls executes the TransferResult. Here's what's actually running on my production servers.
public class TransferToRouteResult : ActionResult
{
public string RouteName { get;set; }
public RouteValueDictionary RouteValues { get; set; }
public TransferToRouteResult(RouteValueDictionary routeValues)
: this(null, routeValues)
{
}
public TransferToRouteResult(string routeName, RouteValueDictionary routeValues)
{
this.RouteName = routeName ?? string.Empty;
this.RouteValues = routeValues ?? new RouteValueDictionary();
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var urlHelper = new UrlHelper(context.RequestContext);
var url = urlHelper.RouteUrl(this.RouteName, this.RouteValues);
var actualResult = new TransferResult(url);
actualResult.ExecuteResult(context);
}
}
And if you're using T4MVC (if not... do!) this extension might come in handy.
public static class ControllerExtensions
{
public static TransferToRouteResult TransferToAction(this Controller controller, ActionResult result)
{
return new TransferToRouteResult(result.GetRouteValueDictionary());
}
}
Using this little gem you can do
// in an action method
TransferToAction(MVC.Error.Index());

Edit: Updated to be compatible with ASP.NET MVC 3
Provided you are using IIS7 the following modification seems to work for ASP.NET MVC 3.
Thanks to #nitin and #andy for pointing out the original code didn't work.
Edit 4/11/2011: TempData breaks with Server.TransferRequest as of MVC 3 RTM
Modified the code below to throw an exception - but no other solution at this time.
Here's my modification based upon Markus's modifed version of Stan's original post. I added an additional constructor to take a Route Value dictionary - and renamed it MVCTransferResult to avoid confusion that it might just be a redirect.
I can now do the following for a redirect:
return new MVCTransferResult(new {controller = "home", action = "something" });
My modified class :
public class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url)
: base(url)
{
}
public MVCTransferResult(object routeValues):base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()), RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException("TempData won't work with Server.TransferRequest!");
}
httpContext.Server.TransferRequest(Url, true); // change to false to pass query string parameters if you have already processed them
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}

You can use Server.TransferRequest on IIS7+ instead.

I found out recently that ASP.NET MVC doesn't support Server.Transfer() so I've created a stub method (inspired by Default.aspx.cs).
private void Transfer(string url)
{
// Create URI builder
var uriBuilder = new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port, Request.ApplicationPath);
// Add destination URI
uriBuilder.Path += url;
// Because UriBuilder escapes URI decode before passing as an argument
string path = Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
// Rewrite path
HttpContext.Current.RewritePath(path, false);
IHttpHandler httpHandler = new MvcHttpHandler();
// Process request
httpHandler.ProcessRequest(HttpContext.Current);
}

Couldn't you just create an instance of the controller you would like to redirect to, invoke the action method you want, then return the result of that? Something like:
HomeController controller = new HomeController();
return controller.Index();

Rather than simulate a server transfer, MVC is still capable of actually doing a Server.TransferRequest:
public ActionResult Whatever()
{
string url = //...
Request.RequestContext.HttpContext.Server.TransferRequest(url);
return Content("success");//Doesn't actually get returned
}

I wanted to re-route the current request to another controller/action, while keeping the execution path exactly the same as if that second controller/action was requested. In my case, Server.Request wouldn't work because I wanted to add more data. This is actually equivalent the current handler executing another HTTP GET/POST, then streaming the results to the client. I'm sure there will be better ways to achieve this, but here's what works for me:
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Public");
routeData.Values.Add("action", "ErrorInternal");
routeData.Values.Add("Exception", filterContext.Exception);
var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var request = new RequestContext(context, routeData);
IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(filterContext.RequestContext, "Public");
controller.Execute(request);
Your guess is right: I put this code in
public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter
and I'm using it to display errors to developers, while it'll be using a regular redirect in production. Note that I didn't want to use ASP.NET session, database, or some other ways to pass exception data between requests.

Just instance the other controller and execute it's action method.

You could new up the other controller and invoke the action method returning the result. This will require you to place your view into the shared folder however.
I'm not sure if this is what you meant by duplicate but:
return new HomeController().Index();
Edit
Another option might be to create your own ControllerFactory, this way you can determine which controller to create.

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.
What's more is that since you are responding to the original request, there is no need to tuck anything into TempData or other storage just for the sake of routing the request to the right place. Instead, you arrive at the controller action with the original request intact. You also can be rest assured that Google will approve of this approach as it happens entirely on the server side.
While you can do quite a bit from both IRouteConstraint and IRouteHandler, the most powerful extension point for routing is the RouteBase subclass. This class can be extended to provide both incoming routes and outgoing URL generation, which makes it a one stop shop for everything having to do with the URL and the action that URL executes.
So, to follow your second example, to get from / to /home/7, you simply need a route that adds the appropriate route values.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Routes directy to `/home/7`
routes.MapRoute(
name: "Home7",
url: "",
defaults: new { controller = "Home", action = "Index", version = 7 }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
But going back to your original example where you have a random page, it is more complex because the route parameters cannot change at runtime. So, it could be done with a RouteBase subclass as follows.
public class RandomHomePageRoute : RouteBase
{
private Random random = new Random();
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Only handle the home page route
if (httpContext.Request.Path == "/")
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
result.Values["version"] = random.Next(10) + 1; // Picks a random number from 1 to 10
}
// 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;
}
}
Which can be registered in routing like:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Routes to /home/{version} where version is randomly from 1-10
routes.Add(new RandomHomePageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Note in the above example, it might make sense to also store a cookie recording the home page version the user came in on so when they return they receive the same home page version.
Note also that using this approach you can customize routing to take query string parameters into consideration (it completely ignores them by default) and route to an appropriate controller action accordingly.
Additional Examples
https://stackoverflow.com/a/31958586
https://stackoverflow.com/a/36774498
https://stackoverflow.com/a/36168395

Doesn't routing just take care of this scenario for you? i.e. for the scenario described above, you could just create a route handler that implemented this logic.

For anyone using expression-based routing, using only the TransferResult class above, here's a controller extension method that does the trick and preserves TempData. No need for TransferToRouteResult.
public static ActionResult TransferRequest<T>(this Controller controller, Expression<Action<T>> action)
where T : Controller
{
controller.TempData.Keep();
controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
var url = LinkBuilder.BuildUrlFromExpression(controller.Request.RequestContext, RouteTable.Routes, action);
return new TransferResult(url);
}

Not an answer per se, but clearly the requirement would be not only for the actual navigation to "do" the equivalent functionality of Webforms Server.Transfer(), but also for all of this to be fully supported within unit testing.
Therefore the ServerTransferResult should "look" like a RedirectToRouteResult, and be as similar as possible in terms of the class hierarchy.
I'm thinking of doing this by looking at Reflector, and doing whatever RedirectToRouteResult class and also the various Controller base class methods do, and then "adding" the latter to the Controller via extension methods. Maybe these could be static methods within the same class, for ease/laziness of downloading?
If I get round to doing this I'll post it up, otherwise maybe somebody else might beat me to it!

I achieved this by harnessing the Html.RenderAction helper in a View:
#{
string action = ViewBag.ActionName;
string controller = ViewBag.ControllerName;
object routeValues = ViewBag.RouteValues;
Html.RenderAction(action, controller, routeValues);
}
And in my controller:
public ActionResult MyAction(....)
{
var routeValues = HttpContext.Request.RequestContext.RouteData.Values;
ViewBag.ActionName = "myaction";
ViewBag.ControllerName = "mycontroller";
ViewBag.RouteValues = routeValues;
return PartialView("_AjaxRedirect");
}

Related

ASP MVC - Access Controller/Action using parameter name

I am using ASP MVC4 and I would like to know if I can open a specific page as follow:
www.domain.com?Area=area1&controller=myController&Action=MyAction
instead of
www.domain.com/area1/mycontroller/MyAction
It works for me when I use the area as parameter but when I use the controller and action too as query parameters, it fails. Is there a way to make it work as Url parameters?
By default, the built-in routing ignores query string values (it does not add them to the RouteData.Values dictionary). However, there is no reason why you can't extend routing to consider them.
public class QueryStringRoute : RouteBase
{
public RouteData GetRouteData(HttpContextBase httpContext)
{
var path = httpContext.Request.Path;
if (!string.IsNullOrEmpty(path))
{
// Don't handle URLs that have a path /controller/action
return null;
}
var queryString = httpContext.Request.QueryString;
if (!queryString.HasKeys())
{
// Don't handle the route if there is no query string.
return null;
}
if (!queryString.AllKeys.Contains("controller") && !queryString.AllKeys.Contains("action"))
{
// Don't handle the case where controller and action are missing.
return null;
}
var controller = queryString["controller"];
var action = queryString["action"];
var area = queryString["area"];
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["controller"] = controller;
routeData.Values["action"] = action;
routeData.DataTokens["area"] = area;
return routeData;
}
public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
// NOTE: MVC automatically tacks unrecognized route values onto
// the query string. So, it is sufficient to just call your
// ActionLink normally and returning an empty string for the URL
// will send it to mysite.com/?controller=foo&action=bar
return new VirtualPathData(this, string.Empty);
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Add the query string route
routes.Add(new new QueryStringRoute());
}
}
I haven't tested this, so it may need some tweaking to get it to work right, but this is how you can do it.
Do note however, this is bad for SEO and also you will need to add additional code to handle parameters other than controller, action, and area (such as id). You could pass values to match into the route constructor, and then register the QueryStringRoute class with different parameters in order to overcome this problem.

Make ASP.net MVC 5 Routing Ignore Async Suffix on Action methods

WebAPI 2 intelligently handles the Async suffix on action methods. For example, if I create a default WebAPI project it will route to the correct action regardless of the suffix. I.e.:
http://host/api/controller/action - SUCCEEDS
http://host/api/controller/actionAsync - SUCCEEDS
However, if I create an equivalent controller using MVC 5 the behavior is different:
http://host/controller/actionAsync - SUCCEEDS
http://host/controller/action - FAILS - 404
The fact that it fails with a 404 when the Async suffix isn't present is surprising. Nevertheless, I tried to add a route to handle it, but it still fails:
routes.MapRoute(
name: "DefaultAsync",
url: "{controller}/{action}Async/{id}",
defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional}
);
Here's the MVC and WebAPI controllers that I used to test (based on a new MVC/WebAPI project with default routes):
public class SampleDto { public string Name; public int Age; }
public class SampleMvcController : Controller
{
public Task<JsonResult> GetJsonAsync()
{
// Illustration Only. This is not a good usage of an Async method,
// in reality I'm calling out to an Async service.
return Task.FromResult(
Json(new SampleDto { Name="Foo", Age = 42}, JsonRequestBehavior.AllowGet));
}
}
public class SampleWebApiController : ApiController
{
public Task<SampleDto> GetJsonAsync()
{
return Task.FromResult(new SampleDto {Name="Bar", Age=24});
}
}
As I'm in the middle of making a bunch of methods async, I'd prefer not to specify an action name. The routing documentation suggests that it can pick up literals which can separate segments, but I haven't had any luck yet.
UPDATE:
The problem is that the Action as retrieved by MVC contains the Async suffix, but no corresponding action (or action name) exists on the controller. The piece that matches the action, MyAction, doesn't identify MyActionAsync as a match.
In retrospect, that's why the route doesn't work. It attempts to identify the action as ending with Async but leave off the Async suffix from the action used in matching, which is not what I wanted to do. It would be useful in the event that I wanted to create only a MyAction method (that was async but didn't follow the Async naming convention) and have it map to the corresponding MyAction method.
Initially I was sorely disappointed that the AsyncController type accomplishes exactly what we're looking for out-of-the-box. But apparently it's old-hat and is only still around for "backwards compatibility with MVC3."
So what I ended up doing was making a custom AsyncControllerActionInvoker and assigning it to a custom controller's ActionInvoker. The CustomAsyncControllerActionInvoker overrides the BeginInvokeAction method to see if an action method ending in "Async" for the appropriate action exists (ex. you pass in "Index" it looks for "IndexAsync"). If it does, invoke that one instead, otherwise, continue on as you were.
public class HomeController : CustomController
{
public async Task<ActionResult> IndexAsync()
{
ViewBag.Header = "I am a page header."
var model = new List<int> {1, 2, 3, 4, 5};
await Task.Run(() => "I'm a task");
return View(model);
}
public ActionResult About()
{
return View();
}
}
public class CustomController : Controller
{
public CustomController()
{
ActionInvoker = new CustomAsyncControllerActionInvoker();
}
}
public class CustomAsyncControllerActionInvoker : AsyncControllerActionInvoker
{
public override IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state)
{
var asyncAction = FindAction(controllerContext, GetControllerDescriptor(controllerContext), $"{actionName}Async");
return asyncAction != null
? base.BeginInvokeAction(controllerContext, $"{actionName}Async", callback, state)
: base.BeginInvokeAction(controllerContext, actionName, callback, state);
}
}
Alas, navigating to /Home/Index properly calls the IndexAsync() method and serves up the Index.cshtml (not IndexAsync.cshtml) view appropriately. Routes to synchronous actions (ex. "About") are handled as normal.
Instead of trying to put a literal together with a placeholder, you should make a constraint to ensure your action name ends with Async.
routes.MapRoute(
name: "DefaultAsync",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "IndexAsync", id = UrlParameter.Optional },
constraints: new { action = #".*?Async" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Is it possible to have an anonymous controller in asp.net MVC 5?

We have built an MVC app that publishes a complete website with hierarchal Folders, SubFolders and Pages. The resulting pages, are strictly HTML and are not published in our MVC app. Our customers are able to name their Folders and Pages with any compliant string they choose. So conceivably, once the site is hosted, they could end up with a URL such as:
someDomain.com/folder/subfolder1/subfolder2/page-slug. There is no limit to the number of nested subfolders.
We would like to replicate their sites in our MVC app, so that they are able to test them before they publish and perhaps so we can provide hosting ourselves if required.
The obvious problem, is how can we handle,
ourMVCApp.com/folder/subfolder1/subfolder2/page-slug in an MVC app?
If there was a way that we could set routing to handle such a thing, then we could easily get the content required for the request by splitting the url into an array by "/".
The last segment would be a page contained in the previous segment's folder. We could then search our DB using these strings to get the required content.
Your help is greatly appreciated.
FURTHER QUESTION:
In response to the answer provided by Tomi.
I added the code to my controller's class but I am receiving the following warning:
I am not sure what I am missing? Did I put the code in the place? Thanks again.
UPDATE 2. I realized I had not actually created the controller factory, so I followed a partial example I found here: http://develoq.net/2010/custom-controller-factory-in-asp-net-mvc/. And since implementing it, I no longer receive any build-errors, but when I run the the debug, it crashes the built-in IISEXPRESS without any error message.
Here is my controller factory code:
public class FolderControllerFactory : IControllerFactory
{
public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
try
{
// Get the path
string path = requestContext.RouteData.Values["pathInfo"].ToString();
IController controller = new FolderController(path);
return controller;
}
catch
{
// Log routing error here and move on
return CreateController(requestContext, controllerName);
}
}
public void ReleaseController(IController controller)
{
var disposable = controller as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return SessionStateBehavior.Default;
}
}
Here is my global:
ControllerBuilder.Current.SetControllerFactory(typeof(ProofPixApp.Controllers.FolderControllerFactory));
And finally my controller:
public class FolderController : Controller
{
private string _path;
public FolderController(string path)
{
_path = path;
}
public ActionResult Index(string name)
{
ViewBag.Message = "Hello " + name;
return View("/Views/" + _path);
}
}
A couple of notes:
1. I removed the 'override' from public IController CreateController
because I kept receiving the initial error I posted.
2. I added public void ReleaseController and the public
SessionStateBehavior GetControllerSessionBehavior methods to the
CreateController class to avoid other build errors.
3. I removed 'base.' from the catch clause because it too was causing a
build error.
SOLUTION:
I was able to avoid the error by checking to see pathValue was not null in the createController method, like so:
public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
// Get the path
string path = "";
if (requestContext.RouteData.Values["pathInfo"] != null)
{
path = requestContext.RouteData.Values["pathInfo"].ToString();
}
IController controller = new FolderController(path);
return controller;
}
I have no idea what page slug is but here's my solution on how to achieve the routing you requested.
I made a custom ControllerFactory which handles the url and passes it to controller. This ControllerFactory constructs the controller we use to handle folder-route requests. We get the path from routevalues and then pass it to the FolderController.
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
try
{
// Get the path
string path = requestContext.RouteData.Values["pathInfo"].ToString();
IController controller = new FolderController(path);
return controller;
}
catch
{
// Log routing error here and move on
return base.CreateController(requestContext, controllerName);
}
}
Here's the controller. The actionmethod, which redirects to given path is called Index for now. The actionmethod returns view it finds from the url.
public class FolderController : Controller
{
private string _path;
public FolderController(string path)
{
_path = path;
}
public FolderController()
{
}
public ActionResult Index(string name)
{
ViewBag.Message = "Hello " + name;
return View("/Views/"+_path);
}
}
Last step is to write our own route and register the factory. Open up RouteConfig.cs. My new RegisterRoutes method looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Dynamic",
url: "{*pathInfo}",
defaults: new { controller = "Folder", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
In global.asax we need to register our FolderControllerFactory by adding this line into Application_Start method
ControllerBuilder.Current.SetControllerFactory(typeof(FolderControllerFactory));
And that's it! There's still much to be done, like handling improper urls and such. Also I don't think this supports plain html files, the files must be in .cshtml or asp format.
Here's the test:
My folder structure:
Url I request:
localhost:port/Mainfolder/Subfolder/Subfolder2/view.cshtml?name=Tomi
The result with Route Debugger plugin:

ASP.net MVC support for URL's with hyphens

Is there an easy way to get the MvcRouteHandler to convert all hyphens in the action and controller sections of an incoming URL to underscores as hyphens are not supported in method or class names.
This would be so that I could support such structures as sample.com/test-page/edit-details mapping to Action edit_details and Controller test_pagecontroller while continuing to use MapRoute method.
I understand I can specify an action name attribute and support hyphens in controller names which out manually adding routes to achieve this however I am looking for an automated way so save errors when adding new controllers and actions.
C# version of John's Post for anyone who would prefer it: C# and VB version on my blog
public class HyphenatedRouteHandler : MvcRouteHandler{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
...and the new route:
routes.Add(
new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Default", action = "Index", id = "" }),
new HyphenatedRouteHandler())
);
You can use the following method too but bear in mind you would need to name the view My-Action which can be annoying if you like letting visual studio auto generate your view files.
[ActionName("My-Action")]
public ActionResult MyAction() {
return View();
}
I have worked out a solution. The requestContext inside the MvcRouteHandler contains the values for the controller and action on which you can do a simple replace on.
Public Class HyphenatedRouteHandler
Inherits MvcRouteHandler
Protected Overrides Function GetHttpHandler(ByVal requestContext As System.Web.Routing.RequestContext) As System.Web.IHttpHandler
requestContext.RouteData.Values("controller") = requestContext.RouteData.Values("controller").ToString.Replace("-", "_")
requestContext.RouteData.Values("action") = requestContext.RouteData.Values("action").ToString.Replace("-", "_")
Return MyBase.GetHttpHandler(requestContext)
End Function
End Class
Then all you need to replace the routes.MapRoute with an equivalent routes.Add specifying the the new route handler. This is required as the MapRoute does not allow you to specify a custom route handler.
routes.Add(New Route("{controller}/{action}/{id}", New RouteValueDictionary(New With {.controller = "Home", .action = "Index", .id = ""}), New HyphenatedRouteHandler()))
All you really need to do in this case is name your views with the hyphens as you want it to appear in the URL, remove the hyphens in your controller and then add an ActionName attribute that has the hyphens back in it. There's no need to have underscores at all.
Have a view called edit-details.aspx
And have a controller like this:
[ActionName("edit-details")]
public ActionResult EditDetails(int id)
{
// your code
}
I realize this is quite an old question, but to me this is only half the story of accepting url's with hyphens in them, the other half is generating these urls while still being able to use Html.ActionLink and other helpers in the MVC framework, I solved this by creating a custom route class similar, here is the code in case it helps anyone coming here from a google search. It also includes the lower casing of the url too.
public class SeoFriendlyRoute : Route
{
// constructor overrides from Route go here, there is 4 of them
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var path = base.GetVirtualPath(requestContext, values);
if (path != null)
{
var indexes = new List<int>();
var charArray = path.VirtualPath.Split('?')[0].ToCharArray();
for (int index = 0; index < charArray.Length; index++)
{
var c = charArray[index];
if (index > 0 && char.IsUpper(c) && charArray[index - 1] != '/')
indexes.Add(index);
}
indexes.Reverse();
indexes.Remove(0);
foreach (var index in indexes)
path.VirtualPath = path.VirtualPath.Insert(index, "-");
path.VirtualPath = path.VirtualPath.ToLowerInvariant();
}
return path;
}
}
then when adding routes, you can either create a RouteCollection extensions or just use the following in your global routing declarations
routes.Add(
new SeoFriendlyRoute("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Default", action = "Index", id = "" }),
new HyphenatedRouteHandler())
);
Thanks dsteuernol for this answer - exactly what I was looking for. However I found that I needed to enhance the HyphenatedRouteHandler to cover the scenario where the Controller or Area was implied from the current page. For example using #Html.ActionLink("My Link", "Index")
I changed the GetHttpHandler method to the following:
public class HyphenatedRouteHandler : MvcRouteHandler
{
/// <summary>
/// Returns the HTTP handler by using the specified HTTP context.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>
/// The HTTP handler.
/// </returns>
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = ReFormatString(requestContext.RouteData.Values["controller"].ToString());
requestContext.RouteData.Values["action"] = ReFormatString(requestContext.RouteData.Values["action"].ToString());
// is there an area
if (requestContext.RouteData.DataTokens.ContainsKey("area"))
{
requestContext.RouteData.DataTokens["area"] = ReFormatString(requestContext.RouteData.DataTokens["area"].ToString());
}
return base.GetHttpHandler(requestContext);
}
private string ReFormatString(string hyphenedString)
{
// lets put capitals back in
// change dashes to spaces
hyphenedString = hyphenedString.Replace("-", " ");
// change to title case
hyphenedString = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(hyphenedString);
// remove spaces
hyphenedString = hyphenedString.Replace(" ", "");
return hyphenedString;
}
}
Putting the capitals back in meant that the implied controller or area was then hyphenated correctly.
I've developed an open source NuGet library for this problem which implicitly converts EveryMvc/Url to every-mvc/url.
Dashed urls are much more SEO friendly and easier to read. (More on my blog post)
NuGet Package: https://www.nuget.org/packages/LowercaseDashedRoute/
To install it, simply open the NuGet window in the Visual Studio by right clicking the Project and selecting NuGet Package Manager, and on the "Online" tab type "Lowercase Dashed Route", and it should pop up.
Alternatively, you can run this code in the Package Manager Console:
Install-Package LowercaseDashedRoute
After that you should open App_Start/RouteConfig.cs and comment out existing route.MapRoute(...) call and add this instead:
routes.Add(new LowercaseDashedRoute("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
new DashedRouteHandler()
)
);
That's it. All the urls are lowercase, dashed, and converted implicitly without you doing anything more.
Open Source Project Url: https://github.com/AtaS/lowercase-dashed-route
Don't know of a way without writing a map for each url:
routes.MapRoute("EditDetails", "test-page/edit-details/{id}", new { controller = "test_page", action = "edit_details" });
If you upgrade your project to MVC5, you can make use of attribute routing.
[Route("controller/my-action")]
public ActionResult MyAction() {
return View();
}
I much prefer this approach to the accepted solution, which leaves you with underscores in your controller action names and view filenames, and hyphens in your view's Url.Action helpers. I prefer consistency, and not having to remember how the names are converted.
In MVC 5.2.7 you can simply specifiy using the attribute
ActionName
[ActionName("Import-Export")]
public ActionResult ImportExport()
{
return View();
}
Then name the view
Import-Export.cshtml
The link would then be:
#Html.ActionLink("Import and Export", "Import-Export", "Services")
Which is of the form:
#Html.ActionLink("LinkName", "ActionName", "ControllerName")

Single Controller with Multiple Views

I am trying to create an MVC application with multiple view, but using a single controller. I started by creating a second route with another property that I could use to redirect to a second folder.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"xml", // Route name
"xml/{controller}/{action}/{id}", // URL with parameters
new { mode = "xml", controller = "Home", action = "Index", id = "" } // Parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
SessionManager.Instance.InitSessionFactory("acstech.helpWanted");
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModeViewEngine());
}
I followed that by descending from WebFormViewEngine and changed the path from ~/View to ~/{mode}View. This worked and ran rendered the pages properly. The problem that I ran into is that the Html.ActionLink always uses the mode version no matter what the view rendered. Is this the proper direction for accomplishing my goal, if so what am I missing to get the Action Link to work properly. Below is the ViewEngine. This is a lab test, so some liberties have been taken.
public class ModeViewEngine : WebFormViewEngine
{
public ModeViewEngine()
{
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
string mode = String.Empty;
if (controllerContext.RouteData.Values["mode"] != null)
mode = controllerContext.RouteData.Values["mode"] as string;
return new WebFormView(partialPath.Replace("~/Views", "~/" + mode + "Views"), null);
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
string mode = String.Empty;
if (controllerContext.RouteData.Values["mode"] != null)
mode = controllerContext.RouteData.Values["mode"] as string;
return new WebFormView(viewPath.Replace("~/Views", "~/" + mode + "Views"), masterPath);
}
}
Have you tried adding mode="" in the defaults array in the default route? That is, as I understand it, how the "Index" action gets omitted from URLs, so that should in theory make it match your default route I believe.
Why not have your controller just pick a different view based on the mode? Then you can return your Home view or Home_Xml.
I guess your way moves this decision out of the controller and centralizes the logic, but it also requires you to create a matching view in each Mode you have.
Well the great thing about the Asp.Net framework is that is that is is very extensible. I think you should check out this link. It will have exactly what you are looking for. My opion, along with the authors is to create a ActionFilter and decorate your views in the controller needing XML or even JSON views. I have even seen cases where all the serialization to XML happens in the Filter and that is returned, thus not needing a ViewEngine.
http://jesschadwick.blogspot.com/2008/06/aspnet-mvc-using-custom-view-engines.html

Resources