ASP.NET MVC: Routing custom slugs without affecting performance

放肆的年华 提交于 2019-12-18 11:42:43

问题


I would like to create custom slugs for pages in my CMS, so users can create their own SEO-urls (like Wordpress).

I used to do this in Ruby on Rails and PHP frameworks by "abusing" the 404 route. This route was called when the requested controller could not be found, enabling me te route the user to my dynamic pages controller to parse the slug (From where I redirected them to the real 404 if no page was found). This way the database was only queried to check the requested slug.

However, in MVC the catch-all route is only called when the route does not fit the default route of /{controller}/{action}/{id}.

To still be able to parse custom slugs I modified the RouteConfig.cs file:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        RegisterCustomRoutes(routes);

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { Controller = "Pages", Action = "Index", id = UrlParameter.Optional }
        );
    }

    public static void RegisterCustomRoutes(RouteCollection routes)
    {
        CMSContext db = new CMSContext();
        List<Page> pages = db.Pages.ToList();
        foreach (Page p in pages)
        {
            routes.MapRoute(
                name: p.Title,
                url: p.Slug,
                defaults: new { Controller = "Pages", Action = "Show", id = p.ID }
            );
        }
        db.Dispose();
    }
}

This solves my problem, but requires the Pages table to be fully queried for every request. Because a overloaded show method (public ViewResult Show(Page p)) did not work I also have to retrieve the page a second time because I can only pass the page ID.

  1. Is there a better way to solve my problem?
  2. Is it possible to pass the Page object to my Show method instead of the page ID?

回答1:


Even if your route registration code works as is, the problem will be that the routes are registered statically only on startup. What happens when a new post is added - would you have to restart the app pool?

You could register a route that contains the SEO slug part of your URL, and then use the slug in a lookup.

RouteConfig.cs

routes.MapRoute(
    name: "SeoSlugPageLookup",
    url: "Page/{slug}",
    defaults: new { controller = "Page", 
                    action = "SlugLookup",
                  });

PageController.cs

public ActionResult SlugLookup (string slug)
{
    // TODO: Check for null/empty slug here.

    int? id = GetPageId (slug);

    if (id != null) {    
        return View ("Show", new { id });
    }

    // TODO: The fallback should help the user by searching your site for the slug.
    throw new HttpException (404, "NotFound");
}

private int? GetPageId (string slug)
{
    int? id = GetPageIdFromCache (slug);

    if (id == null) {
        id = GetPageIdFromDatabase (slug);

        if (id != null) {
            SetPageIdInCache (slug, id);
        }
    }

    return id;
}

private int? GetPageIdFromCache (string slug)
{
    // There are many caching techniques for example:
    // http://msdn.microsoft.com/en-us/library/dd287191.aspx
    // http://alandjackson.wordpress.com/2012/04/17/key-based-cache-in-mvc3-5/
    // Depending on how advanced you want your CMS to be,
    // caching could be done in a service layer.
    return slugToPageIdCache.ContainsKey (slug) ? slugToPageIdCache [slug] : null;
}

private int? SetPageIdInCache (string slug, int id)
{
    return slugToPageIdCache.GetOrAdd (slug, id);
}

private int? GetPageIdFromDatabase (string slug)
{
    using (CMSContext db = new CMSContext()) {
        // Assumes unique slugs.
        Page page = db.Pages.Where (p => p.Slug == requestContext.Url).SingleOrDefault ();

        if (page != null) {
            return page.Id;
        }
    }

    return null;
}

public ActionResult Show (int id)
{
    // Your existing implementation.
}

(FYI: Code not compiled nor tested - haven't got my dev environment available right now. Treat it as pseudocode ;)

This implementation will have one search for the slug per server restart. You could also pre-populate the key-value slug-to-id cache at startup, so all existing page lookups will be cheap.




回答2:


I've edited my answer to give a more complete answer to your questions:

Answer to Question 1:

Registering routes is initialized on startup. (Perhaps also when the Application Pool recycles, it's highly probable.) I also think there is nothing wrong with your approach since it is occuring only once. I do the same thing querying all supported languages from the database to register them as /TwoLetterISOLanguageName (/nl, /en, /de, etc.).

Answer to Question 2:

This should work passing a model: Put it before the Default route!

routes.MapRoute(
    name: "Contact",
    url: "contact/{action}",
    defaults: new { controller = "Contact", 
                    action = "Index",
                    MyModel = new MyModel { Name = "hello" } });

The ContactController:

public ActionResult Index(MyModel mymodel)
{
    return Content(mymodel.Name);
}

The Model:

public class MyModel
{
    public string Name { get; set; }
}


来源:https://stackoverflow.com/questions/11494184/asp-net-mvc-routing-custom-slugs-without-affecting-performance

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!