Multilingual URLs with ASP.NET MVC

2020-06-23 05:30发布

问题:

I’m working out the concepts for a new project where I need to support for multilingual URL’s. Ideally all URL’s need to be in the native language of the user. So we don’t want to use domain.com/en/contact and domain.com/es/contact but we like domain.com/contact and domain.com/contactar (contactar is Spanish for contact). Internally both should be routed to the same ContactController class.

This could be handled by adding multiple static routes to Global.asax.cs for each language but we’d like to make this very dynamic and would like the user of the system to be able to change the translation of the URL’s through the content management system. So we need some kind of dynamic mapping from URL’s to controllers and actions.

By looking at the source code of MVC3 I figured out that the ProcessRequestInit method of MvcHandler is responsible for determining which controller to create. It simply looks in the RouteData to get the name of the controller. One way to override the default MVC routing would be to create a simple default route that uses a custom RouteHandler. This RouteHandler forces MVC to use my own custom subclassed version of MvcHandler that overrides the ProcessRequestInit method. This overridden method insert my own dynamically found controller and action into the RouteData before calling back to the original ProcessRequestInit.

I’ve tried this:

Global.asax.cs

routes.Add(
    new Route("{*url}", new MultilingualRouteHandler())
    {
        Defaults = new RouteValueDictionary(new { controller = "Default", action = "Default" })
    }
);

MultilingualRouteHandler.cs

public class MultilingualRouteHandler : IRouteHandler
{

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new MultilingualMVCHandler(requestContext);
    }

}

MultilingualMvcHandler.cs

public class MultilingualMVCHandler : MvcHandler
{

    public MultilingualMVCHandler(RequestContext context) : base(context)
    {
    }

    protected override void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
    {

        if (RequestContext.RouteData.Values.ContainsKey("controller"))
        {
            RequestContext.RouteData.Values.Remove("controller");
        }

        if (RequestContext.RouteData.Values.ContainsKey("action"))
        {
            RequestContext.RouteData.Values.Remove("action");
        }

        RequestContext.RouteData.Values.Add("controller", "Product");
        RequestContext.RouteData.Values.Add("action", "Index");

        base.ProcessRequestInit(httpContext, out controller, out factory);

    }

}

In this handler I hardcoded the controller and action for testing purposes to some fixed values but it’s not difficult to make this dynamic. It works but the only problem is that I had to modify the source code of ASP.NET MVC3 to get it working. The problem is that the ProcessRequestInit method of MvcHandler is private and thus cannot be overridden. I’ve modified the source code and changed it to protected virtual which allows me to override it.

This is all great but possibly not the best solution. It’s cumbersome that I would always need to distribute my own version of System.Web.Mvc.dll. It would be much better that it would work with the RTM version.

Am I missing any other possibilities of hooking into ASP.NET MVC that would allow me to dynamically determine the controller and action to launch, depending on the URL? One other way I thought of is to build the RouteCollection dynamically on *Application_Start* but I think that will make it more difficult to change it on the fly.

I would appreciate any tips of hooks that I’ve not yet found.

回答1:

This is fairly old now, nut just in case anyone else is looking for something similar...

Unless I'm completely misunderstanding what you want to do, it's pretty simple really.

Step 1: Add a new route to global.ascx.cs containing a reference to your personal routing engine

routes.Add(new MyProject.Routing.ContentRoutingEngine());

Make sure that it is in the right place in the list of routes so that other routing engines can catch stuff before it if required, or continue the route search if your engine doesn't handle a particular route. I put it after the ignores, but before the MVC default routes.

Step 2: Create the Content Routing Engine, making sure that it inherites from System.Web.Routing.RouteBase abstract class, and overrides the GetRouteData and GetVirtualPath methods as required e.g.

public class ContentRoutingEngine : RouteBase
{
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var routeHandler = new MvcRouteHandler();
        var currentRoute = new Route("{controller}/{action}", routeHandler);
        var routeData = new RouteData(currentRoute, routeHandler);

        // set your values dynamically here
        routeData.Values["controller"] = "Home" ;
        // or
        routeData.Values.Add("action", "Index");

        // return the route, or null to have it passed to the next routing engine in the list
        return routeData;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        //implement this to return url's for routes, or null to just pass it on
        return null;
    }
}

and that should do it. You can change routes as dynamically as you wish within your engine, and no changes to MVC source required. Let the standard MVC RouteHandler actually invoke the controller.

Postscript: Obviously the code above is not production standard - it's written to make it as obvious as possible what's going on.



回答2:

If you are allowing modification of urls through your CMS, then you will have to keep all old versions of the urls so that you can 301 redirect to the new ones.

The best bet for this will be to put the url tokens eg "contactar" in the db along with its corresponding controller.

query that, and create your routes out of that.

create a route that will handle the 301s



回答3:

I think that most elegant solution would be using some action filter combined with custom ActionInvoker. That way, you could invoke an action that has specific filters applied. Something like ActionName attribute, only capable to accept multiple values (names).

Edit: Take a look at ActionMethodSelectorAttribute, meybe you don't need a custom ActionInvoker after all.