5

I'm having a heck of a time figuring out how to properly implement my 404 redirecting.

If I use the following

<HandleError()> _
Public Class BaseController : Inherits System.Web.Mvc.Controller
''# do stuff
End Class

Then any unhandled error on the page will load up the "Error" view which works great. http://example.com/user/999 (where 999 is an invalid User ID) will throw an error while maintaining the original URL (this is what I want)

However. If someone enters http://example.com/asdfjkl into the url (where asdfjkl is an invalid controller), then IIS is throwing the generic 404 page. (this is not what I want). What I need is for the same thing above to apply. The original URL stays, and the "NotFound" controller is loaded.

I'm registering my routes like this

Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
    routes.RouteExistingFiles = False
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
    routes.IgnoreRoute("Assets/{*pathInfo}")
    routes.IgnoreRoute("{*robotstxt}", New With {.robotstxt = "(.*/)?robots.txt(/.*)?"})

    routes.AddCombresRoute("Combres")

    routes.MapRoute("Start", "", New With {.controller = "Events", .action = "Index"})

    ''# MapRoute allows for a dynamic UserDetails ID
    routes.MapRouteLowercase("UserProfile", "Users/{id}/{slug}", _
                             New With {.controller = "Users", .action = "Details", .slug = UrlParameter.Optional}, _
                             New With {.id = "\d+"} _
    )


    ''# Default Catch All MapRoute
    routes.MapRouteLowercase("Default", "{controller}/{action}/{id}/{slug}", _
                             New With {.controller = "Events", .action = "Index", .id = UrlParameter.Optional, .slug = UrlParameter.Optional}, _
                             New With {.controller = New ControllerExistsConstraint})

    ''# Catch everything else cuz they're 404 errors
    routes.MapRoute("CatchAll", "{*catchall}", _
                    New With {.Controller = "Error", .Action = "NotFound"})

End Sub

Notice the ControllerExistsConstraint? What I need to do is use Reflection to discover whether or not a the controller exists.

Can anybody help me fill in the blanks?

Public Class ControllerExistsConstraint : Implements IRouteConstraint

    Public Sub New()
    End Sub

    Public Function Match(ByVal httpContext As System.Web.HttpContextBase, ByVal route As System.Web.Routing.Route, ByVal parameterName As String, ByVal values As System.Web.Routing.RouteValueDictionary, ByVal routeDirection As System.Web.Routing.RouteDirection) As Boolean Implements System.Web.Routing.IRouteConstraint.Match


        ''# Bah, I can't figure out how to find if the controller exists


End Class

I'd also like to know the performance implications of this... how performance heavy is Reflection? If it's too much, is there a better way?

Chase Florell
  • 46,378
  • 57
  • 186
  • 376

3 Answers3

10

I have a C# solution, I hope it helps. I plagiarized some of this code, though for the life of me, I cannot find where I got it from. If anyone know, please let me know so I can add it to my comments.

This solution does not use reflection, but it looks at all the application errors (exceptions) and checks to see if it's a 404 error. If it is, then it just routes the current request to a different controller. Though I am not an expert in any way, I think this solution might be faster than reflection. Anyway, here's the solution and it goes into your Global.asax.cs,

    protected void Application_Error(object sender, EventArgs e)
    {
        Exception exception = Server.GetLastError();

        // A good location for any error logging, otherwise, do it inside of the error controller.

        Response.Clear();
        HttpException httpException = exception as HttpException;
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "YourErrorController");

        if (httpException != null)
        {
            if (httpException.GetHttpCode() == 404)
            {
                routeData.Values.Add("action", "YourErrorAction");

                // We can pass the exception to the Action as well, something like
                // routeData.Values.Add("error", exception);

                // Clear the error, otherwise, we will always get the default error page.
                Server.ClearError();

                // Call the controller with the route
                IController errorController = new ApplicationName.Controllers.YourErrorController();
                errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
            }
        }
    }

So the controller would be,

public class YourErrorController : Controller
{
    public ActionResult YourErrorAction()
    {
        return View();
    }
}
Anh-Kiet Ngo
  • 2,151
  • 1
  • 14
  • 11
  • Though **NOT** an answer to the question. This **DOES** solve my problem. I'll award the bounty, but not mark as answer. – Chase Florell Aug 18 '10 at 19:18
  • You're probably right about the "faster than reflection" bit. This is nice because I don't have to call my `ControllerExistsConstraint` all the time. – Chase Florell Aug 18 '10 at 19:37
  • I did not notice that you had another post open regarding the question. I should have answered that one instead. Maybe you can link the solution of the other one to here. You are too nice about the bounty points :). – Anh-Kiet Ngo Aug 18 '10 at 20:42
  • you're right... I did have the other question first but for some reason, the way I asked it resulted in minimal activity. I figured I'd describe the issue a little better and ask a more direct question. – Chase Florell Aug 19 '10 at 21:01
  • ok, I answered my other question with your answer. Gave credit where credit is due ;-) - though if you want to edit it there, I'll give you the +10 checkmark. – Chase Florell Aug 19 '10 at 21:07
  • The 100 points were plentiful, the link was all that's needed there. You should mark that as an answer though. – Anh-Kiet Ngo Aug 22 '10 at 06:06
  • This is a good solution however before errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData)); is called, this line needs to be added Response.StatusCode = 404; If this line is not added, the page response is still a 200 regardless of what is rendered for the user. – Paul Oct 18 '10 at 22:23
  • That's a good point Paul, though I'm questioning if that should be put inside of Application_Error. Personally, I think the error controller should handle the status, which I think should still work. – Anh-Kiet Ngo Oct 21 '10 at 08:43
  • When I use this the content type is empty in the response showing a text document in the browser instead of a html page. – Mike Flynn Dec 05 '13 at 22:38
2

That's a very similar problem to mine, but I like your alternate approach.

I think the reflection as a dynamic filter might be too performance heavy, but I think I have a better way - you can filter allowed actions by a Regex:

// build up a list of known controllers, so that we don't let users hit ones that don't exist
var allMvcControllers = 
    from t in typeof(Global).Assembly.GetTypes()
    where t != null &&
        t.IsPublic &&
        !t.IsAbstract &&
        t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
        typeof(IController).IsAssignableFrom(t)
    select t.Name.Substring(0, t.Name.Length - 10);

// create a route constraint that requires the controller to be one of the reflected class names
var controllerConstraint = new
{
    controller = "(" + string.Join("|", allMvcControllers.ToArray()) + ")"
};

// default MVC route
routes.MapRoute(
    "MVC",
    "{controller}/{action}/{id}",
    new { action = "Index", id = UrlParameter.Optional },
    controllerConstraint);

// fall back route for unmatched patterns or invalid controller names
routes.MapRoute(
    "Catch All", 
    "{*url}",
    new { controller = "System", action = "NotFound" });

Then I add to this an additional method on my base Controller:

protected override void HandleUnknownAction(string actionName)
{
    this.NotFound(actionName).ExecuteResult(this.ControllerContext);
}

In this case BaseController.NotFound handles the missing action on a valid controller.

So finally:

  • {site}/invalid - found by new reflection based filter
  • {site}/valid/notAnAction - found by HandleUnknownAction
  • {site}/valid/action/id - found by checks in code for the id (as before)
  • {site}/valid/action/id/extraPath - found by not matching any route but the catch all

I think that's all the 404 scenarios covered :-)

Community
  • 1
  • 1
Keith
  • 150,284
  • 78
  • 298
  • 434
-1

Why don't you just capture them with custom errors in your web.config file and avoid a bunch of reflection all together?

<customErrors mode="On">   
    <error statusCode="404" redirect="/Error/NotFound" />
</customErrors>
Justin
  • 1,428
  • 1
  • 13
  • 29
  • because in my question I said "The original URL stays, and the "NotFound" controller is loaded.". **I do NOT want to redirect to a not found page** – Chase Florell Aug 17 '10 at 15:53