Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.NET MVC-4 routing with custom slugs

I'm rewriting a website project with ASP.Net MVC 4 and I find it difficult to setup the right routes. The url structure is not RESTful or following a controller/action pattern - the pages have the following structure of slugs. All slugs are saved in the database.

/country
/country/carmake
/country/carmake/carmodel
/custom-landing-page-slug
/custom-landing-page-slug/subpage

Example:

/italy
/italy/ferrari
/italy/ferrari/360
/history-of-ferrari
/history-of-ferrari/enzo

Since Country, Car Make and Car Model are different models/entities, I would like to have something like a CountriesController, CarMakesController and CarModelsController where I can handle the differing logic and render the appropriate views from. Furthermore, I have the custom landing pages which can have slugs containing one or more slashes.

My first attempt was to have a catch-all PagesController which would look up the slug in the database and call the appropriate controller based on the page type (eg. CarMakesController), which would then perform some logics and render the view. However, I never succeed to "call" the other controller and render the appropriate view - and it didn't feel right.

Can anyone point me in the right direction here? Thanks!

EDIT: To clarify: I do not want a redirect - I want to delegate the request to a different controller for handling logic and rendering a view, depending on the type of content (Country, CarMake etc.).

like image 598
simonwh Avatar asked Oct 26 '13 21:10

simonwh


1 Answers

Since your links looks similar, you can't separate them at the routing level. But here are good news: you can write custom route handler and forget about typical ASP.NET MVC links' parsing.

First of all, let's append RouteHandler to default routing:

routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "Default", action = "Index", id = UrlParameter.Optional }
).RouteHandler = new SlugRouteHandler();

This allows you to operate with your URLs in different ways, e.g.:

public class SlugRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var url = requestContext.HttpContext.Request.Path.TrimStart('/');

        if (!string.IsNullOrEmpty(url))
        {
            PageItem page = RedirectManager.GetPageByFriendlyUrl(url);
            if (page != null)
            {
                FillRequest(page.ControllerName, 
                    page.ActionName ?? "GetStatic", 
                    page.ID.ToString(), 
                    requestContext);
            }
        }

        return base.GetHttpHandler(requestContext);
    }

    private static void FillRequest(string controller, string action, string id, RequestContext requestContext)
    {
        if (requestContext == null)
        {
            throw new ArgumentNullException("requestContext");
        }

        requestContext.RouteData.Values["controller"] = controller;
        requestContext.RouteData.Values["action"] = action;
        requestContext.RouteData.Values["id"] = id;
    }
}

Some explanations are required here.

First of all, your handler should derive MvcRouteHandler from System.Web.Mvc.

PageItem represents my DB-structure, which contains all the necessary information about slug:

PageItem structure

ContentID is a foreign key to contents table.

GetStatic is default value for action, it was convenient in my case.

RedirectManager is a static class which works with database:

public static class RedirectManager
{
    public static PageItem GetPageByFriendlyUrl(string friendlyUrl)
    {
        PageItem page = null;

        using (var cmd = new SqlCommand())
        {
            cmd.Connection = new SqlConnection(/*YourConnectionString*/);
            cmd.CommandText = "select * from FriendlyUrl where FriendlyUrl = @FriendlyUrl";
            cmd.Parameters.Add("@FriendlyUrl", SqlDbType.NVarChar).Value = friendlyUrl.TrimEnd('/');

            cmd.Connection.Open();
            using (var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection))
            {
                if (reader.Read())
                {
                    page = new PageItem
                               {
                                   ID = (int) reader["Id"],
                                   ControllerName = (string) reader["ControllerName"],
                                   ActionName = (string) reader["ActionName"],
                                   FriendlyUrl = (string) reader["FriendlyUrl"],
                               };
                }
            }

            return page;
        }
    }
}

Using this codebase, you can add all the restrictions, exceptions and strange behaviors.

It worked in my case. Hope this helps in yours.

like image 123
Smileek Avatar answered Nov 06 '22 14:11

Smileek