10
\$\begingroup\$

I'm trying to build friendly URLs like /post/1/my-first-post. I started out with building my links like this:

@Html.ActionLink(Model.BlogPost.Title, "Index", "Post", new
{
   id = Model.BlogPost.Id,
   title = Model.FriendlyUrl
}, 
new { })

which were processed by this route:

routes.MapRoute(
   "Post", // Route name
   "posts/{id}/{title}", // URL with parameters
    new { controller = "Post", action = "Index" }, // Parameter defaults
    new { id = "^[0-9]+$" }
 );

When I create my view model, I would give it an IUrlNiceName which it used to create the output of FriendlyUrl:

public string FriendlyUrl
{
    get{ return UrlNiceName.ConvertToNiceName(BlogPost.Title); }
}

Then I felt like I didn't want to manage UrlNiceName-ing on the controller level so then I build a custom Route to do this for me:

public class NiceNameRoute : RouteBase
{
    private IUrlNiceNames UrlNiceNames { get; set; }
    private string UrlPrefix { get { return "post/"; } }

    public NiceNameRoute(IUrlNiceNames urlNiceNames)
    {
        UrlNiceNames = urlNiceNames;
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        RouteData result = null;
        string requestedURL = httpContext.Request.AppRelativeCurrentExecutionFilePath;

        //match e.g. post/1 or post/1/my-first-post
        Regex regex = new Regex(UrlPrefix + "[0-9]+($|/)");

        if (regex.Match(requestedURL).Success)
        {
            //get the start of the post id
            int prefixPos = requestedURL.IndexOf(UrlPrefix, StringComparison.OrdinalIgnoreCase);
            int startIdPos = prefixPos + UrlPrefix.Length;

            //get the end of the post id
            int endIdPos = requestedURL.IndexOf("/", startIdPos, StringComparison.OrdinalIgnoreCase);
            endIdPos = endIdPos == -1 ? requestedURL.Length : endIdPos;

            //get post id
            string id = requestedURL.Substring(startIdPos, endIdPos - startIdPos);

            result = new RouteData(this, new MvcRouteHandler());
            result.Values.Add("controller", "Post");
            result.Values.Add("action", "Index");
            result.Values.Add("id", id);
        }

        return result;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext,
        RouteValueDictionary values)
    {
        VirtualPathData result = null;

        if (values.ContainsKey("title")
            && Convert.ToString(values["action"]) == "Index"
            && Convert.ToString(values["controller"]) == "Post")
        {
            string niceName = UrlNiceNames.ConvertToNiceName(Convert.ToString(values["title"]));

            result = new VirtualPathData(this, UrlPrefix + Convert.ToString(values["id"]) + "/" + niceName);
        }
        return result;
    }
}

which I registered like this:

routes.Add(new NiceNameRoute(new HyphenUrlNiceNames()));

and generate links like this:

@Html.ActionLink(Model.BlogPost.Title, "Index", "Post", new
{
   id = Model.BlogPost.Id,
   title = Model.BlogPost.Title
}, 
new { })

I first wanted to take a crack at this but now I want to know how industry experts do it. Am I over engineering? Is there a simpler approach that is also elegant? Are there any issues with my custom Route?

\$\endgroup\$
7
\$\begingroup\$

Personally I think you are over thinking it. Why not just create a string extension method called FriendlyString or PrepareForUrl and then use the original route

@Html.ActionLink(Model.BlogPost.Title, "Index", "Post", new
{
   id = Model.BlogPost.Id,
   title = Model.BlogPost.Title.FriendlyString()
}, null)
\$\endgroup\$
  • 2
    \$\begingroup\$ Thank you. I WAS beginning to feel like I was making a mountain out of a mole hill. After reviewing your simple answer I realized that my simple goal did not require the complexity I was forcing unto it. And after enjoying making a custom route, I don't think it helped with my goal at all. Now I'm wondering how I got that far. I need to learn to catch waste-of-time stuff like this earlier. \$\endgroup\$ – enamrik Dec 7 '11 at 17:06
  • \$\begingroup\$ @enamrik I actually think your way is better. Yes it is more work but has a better separation of concerns - your View shouldn't have knowledge of how the routes are structured, which it does if you use this method. \$\endgroup\$ – dav_i Feb 26 '14 at 15:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.