Is there a way to discriminate a specific System.Web.HttpException? - asp.net-mvc

I've some "bad boys" users which call my website using controller that doesn't exist.
Such as www.myapp.com/.svn/, or www.myapp.com/wp-admin/.
The app basically throw a System.Web.HttpException, which this ExceptionMessage: The controller for path '...' was not found or does not implement IController.
Is there a way to exactly catch this "sub" System.Web.HttpException in a robust way? (not comparing the string of ExceptionMessage, of course).
Any sub InnerException type and/or some "code" which indicate that exact exception?

You might create a custom IControllerFactory, e.g. by deriving from DefaultControllerFactory.
Its GetControllerInstance method will be called with a null value for the controllerType argument, when no matching controller could be resolved.
At this point, you are ahead of the Exception.
IController GetControllerInstance(RequestContext requestContext, Type controllerType)
Here you decide how to handle the request, like e.g. logging and/or handling the request by a specific controller.
The example below shows how the ErrorController is being used as fallback, executing its Index action method.
class CustomControllerFactory : DefaultControllerFactory
{
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
var routeData = requestContext.RouteData;
routeData.Values["controller"] = "Error";
routeData.Values["action"] = "Index";
controllerType = typeof(ErrorController);
}
return base.GetControllerInstance(requestContext, controllerType);
}
}
A custom controller factory gets installed from within e.g. Application_Start in Global.asax.cs using:
ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
Note that this doesn't guard you from a related exception in case the contoller does exist, but the action doesn't.
E.g. given a HomeController without an Ufo action method, the url www.myapp.com/home/ufo results in an Exception "A public action method 'ufo' was not found on controller '...HomeController".
A simple solution to handle such scenario is by overriding HandleUnknownAction in a custom base controller of which all your controllers inherit.
The example below shows how a shared Error view (Shared\Error.cshtml) gets executed.
public abstract class BaseController : Controller
{
protected override void HandleUnknownAction(string actionName)
{
View("Error").ExecuteResult(ControllerContext);
}
}

The problem as you have stated seems like a routing problem.
Have you tried a catch all route?... That is a route that is executed when no other route matches the request's path. The catch all route is defined last and seems like:
// Specific routes here...
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
"Catch-All",
"{*controller}",
new { controller = "Home", action = "Index" }
);
I tested with .NET Core MVC
// This is how it is configured in .NET Core MMVC
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "catch-all",
pattern: "{*controller}",
defaults: new { controller="Home", action="Index" });
});
I think that managing the excepion, as sugested in previous answers, is a good practice, but does not solve the routing problem you are facing.
You might want to take a look to this discussion regarding similiar problem with ASP.NET MVC:
ASP.NET MVC - Catch All Route And Default Route

Like #Amirhossein mentioned, since you are using .net 4.6 framework you can have a global exception catch inside the
Global.asax file something like:
protected void Application_Error(object sender, EventArgs e)
{
var ex = Server.GetLastError();
Debug.WriteLine(ex);
var httpEx = ex as HttpException;
var statusCode = httpEx?.GetHttpCode() ?? 500;
Server.ClearError();
...
}
or you can address this issue via IIS (since I presume you host your application on IIS) by adding a mapping rule (which in turn can be done in the web.config) or by adding a filter, or even a firewall rule (since by my account those kind of probing scans are not worth to be logged in your application - I believe they should be handled on another level)

Related

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:

In which request event does MVC Routing occurs.

I have gone though MVC Request life cycle and I still did not get any clarity in where the requests are routed to handlers based on registered routes.
We place MVC requests with an url it then finds the controller and delivers the right cshtml page.
As the url is without any resource type, how http module events or http handlers differentiate MVC requests and passes it to the right controller.
You have this document from Microsoft´s ASP .Net site, describing in detail the MVC 5 request lifecycle and how it IS integrated into the ASP .Net lifecycle. Some sections of the diagrams in the pdf itself link to relevant pages in the msdn.
Another good resource are this set of slides from Lukasz Lysic that explains in detail the request lifecycle in MVC 4.
EDIT: I am not a fan of link-only answers, so I have added some more detail below.
On your machine level web.config, you will see the UrlRoutingModule registered as an IHttpModule. For example, on my computer I have:
<system.web>
<httpModules>
...
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" />
...
</httpModules>
<system.web>
On the PostResolveRequestCache application event, the routing module iterates through all the routes in the RouteCollection property and searches for a route that has a URL pattern that matches the format of the HTTP request. When the module finds a matching route, it retrieves the IRouteHandler object for that route. From the route handler, the module gets an IHttpHandler object and uses that as the HTTP handler for the current request (calling its ProcessRequest method or its async counterparts BeginProcessRequest and EndProcessRequest).
You can check the UrlRoutingModule code here on the Microsoft .Net Reference Source site.
In the case of an MVC application, when you add a route in global.asax using the MapRoute extension method, an MVCRouteHandler is created.
Check the MapRoute method in the RouteCollectionExtensions class:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
Route route = new Route(url, new MvcRouteHandler())
{
Defaults = CreateRouteValueDictionaryUncached(defaults),
Constraints = CreateRouteValueDictionaryUncached(constraints),
DataTokens = new RouteValueDictionary()
};
ConstraintValidation.Validate(route);
if ((namespaces != null) && (namespaces.Length > 0))
{
route.DataTokens[RouteDataTokenKeys.Namespaces] = namespaces;
}
routes.Add(name, route);
return route;
}
So when a request is matched to an MVC route, it will be handled by an MvcRouteHandler. As mentioned above, the purpose of the IRouteHandler of a route is to get an IHttpHandler that will be used by the application to continue handling the request. The MvcRouteHandler will return an MvcHandler which is the entry point of the MVC-specific pipeline.
Check the GetHttpHandler method of the MvcRouteHandler class:
protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext));
return new MvcHandler(requestContext);
}
The MVC-specific pipeline basically starts with the ProcessRequest method (Or the async BeginProcessRequest/EndProcessRequest). The MvcHandler will get the IControllerFactory (By default, DefaultControllerFactory will be used, unless you register your own using ControllerBuilder.Current.SetDefaultControllerFactory in global.asax Application_Start), use it to create the controller instance based on the current route values and start controller execution.
Check the ProcessRequest method of the MvcHandler class:
protected internal virtual void ProcessRequest(HttpContextBase httpContext)
{
IController controller;
IControllerFactory factory;
ProcessRequestInit(httpContext, out controller, out factory);
try
{
controller.Execute(RequestContext);
}
finally
{
factory.ReleaseController(controller);
}
}
private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
// If request validation has already been enabled, make it lazy. This allows attributes like [HttpPost] (which looks
// at Request.Form) to work correctly without triggering full validation.
// Tolerate null HttpContext for testing.
HttpContext currentContext = HttpContext.Current;
if (currentContext != null)
{
bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(currentContext);
if (isRequestValidationEnabled == true)
{
ValidationUtility.EnableDynamicValidation(currentContext);
}
}
AddVersionHeader(httpContext);
RemoveOptionalRoutingParameters();
// Get the controller type
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
// Instantiate the controller and call Execute
factory = ControllerBuilder.GetControllerFactory();
controller = factory.CreateController(RequestContext, controllerName);
if (controller == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.ControllerBuilder_FactoryReturnedNull,
factory.GetType(),
controllerName));
}
}
This should explain how incoming requests are matched to controllers in MVC. For the rest of the MVC pipeline, please check the links at the beggining of the post!

Having problems with ASP.NET MVC and StructureMap

I've got StructureMap working fine on my machine. Everything works great ... until I request a resource that doesn't exist. Instead of a 404, i get a 500 error.
eg. http://localhost:6969/lkfhklsfhskdfksdf
Checking the net, i was told to fix my structuremap controller class. Did that and joy! i now get the -original default 404 yellow screen page-. Ok, that's better than my 500 error page.
BUT, i want it to go to my custom 404 page :( If i call a bad action on a legit controller, i get my custom 404 page.
In my global.asax i have my custom routes, then the default, then finally the 404 route:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Post", action = "Index", id = "" } // Parameter defaults
);
// Invalid/Unknown route.
routes.MapRoute(
"404-ResourceNotFound",
"{*url}",
new { controller = "StaticContent", action = "ResourceNotFound" }
);
Here's my structuremap controller code:
public class StructureMapControllerFactory: DefaultControllerFactory {
protected override IController GetControllerInstance(Type controllerType) {
if (controllerType != null) {
return ObjectFactory.GetInstance(controllerType) as Controller;
return base.GetControllerInstance(controllerType);
}
}
Any ideas? is there some way i could somehow get the structure map controller factory to bubble back into the global.asax routes list? or have i done something really bad and need to fix up some other stuff.
cheers!
mmm... seems like it might be an exception thing. like the way MVC is designed to handle the 404 errors via exceptions.
here is my code:
public class StructureMapControllerFactory : DefaultControllerFactory
{
protected override IController GetControllerInstance(Type controllerType)
{
IController result = null;
try
{
result = ObjectFactory.GetInstance(controllerType) as Controller;
}
catch (StructureMapException)
{
System.Diagnostics.Debug.WriteLine(ObjectFactory.WhatDoIHave());
throw;
}
return result;
}
}
.. you may have even had this and changed it. If not, try it and see if there is a difference. I dont suspect there will be.
Maybe just try breaking into that override and seeing what exceptions are being thrown.
(side note: its weird how i keep stumbling on your questions Krome. What r u working on?)
EDIT: I tried the garb request and got the same exception. So i updated my class like you did.
My new class:
public class StructureMapControllerFactory : DefaultControllerFactory
{
protected override IController GetControllerInstance(Type controllerType)
{
if (controllerType == null)
return base.GetControllerInstance(controllerType);
IController result = null;
try
{
result = ObjectFactory.GetInstance(controllerType) as Controller;
}
catch (StructureMapException)
{
System.Diagnostics.Debug.WriteLine(ObjectFactory.WhatDoIHave());
throw;
}
return result;
}
}
..this seems to give me back the 404 like it should.. but i never get the custom error pages in development (locally).. i have to wait till i publish before i get those. Are you used to seeing the custom error pages in dev?

How to simulate Server.Transfer in 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");
}

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