Customizing ASP.NET URL rewriting rules

05/23/2011 - Categories: MonoX
MonoX comes with a powerful rule-based URL rewriting engine based on UrlRewriter.NET, an excellent open source package written by Seth Yates. URL rewriting works hand-in-hand with UrlParams and related classes from the MonoSoftware.Web namespace to provide support for SEO-friendly URLs, rudimentary routing, automatic parameter binding, and other goodies that were very difficult to implement before the advent of ASP.NET MVC and similar frameworks.

A few days ago we received a support query that shed a new light on the functionality of MonoX URL rewriting engine. The user asked,

Is it possible to configure vanity urls for groups? For example:
http://localhost/MonoX/Pages/SocialNetworking/Groups/GroupView/ogTE0Fmv1E6yrZ2lAYWbkg/Web-design/

would become:
http://localhost/Web-design/

A little background: group Web parts already depend on the URL rewriting functionality, as most other advanced MonoX modules. We tried to distill the most common scenarios and encode them in the default MonoX URL rewriting rules. While it is impossible to forsee all real world scenarios, there are a few extension points that allow users to apply their own logic to the rewriting process. In this particular case, all social networking group parts have GroupId property that is automatically bound to the URL parameter. Although we are using ShortGuids to shorten the original GroupId parameter, and can easily remove the "MonoX/Pages/SocialNetworking" part from the URL, there is more space for improvement, resulting in a prettier and more efficient final format.
So, how to transform the existing URL to this new format? The first idea that cames to mind is to use the standard, inheritance-based approach: this would involve the creation of custom group Web parts that inherit from the base MonoX classes, picking up the group name from the URL, querying the database for its id, and setting the GroupId property early in the part lifecycle. As there are quite a few group parts in MonoX, this would require a bit of time for development and testing.

Fortunately, there is a much easier way to do this. Note that while having URLs in the requested format (http://localhost/Web-design/) is possible, it is not very practical, as each resource would be routed to the group system. A much better solution is to use a unique routing identifier in the URL (http://localhost/Group/Web-design/), to clearly separate different functional sections in the URL rewriting system. Additionally, as MonoX by default does not require users to use unique group names when opening a group, you should enforce the name uniqueness in the group editor part (GroupEdit.ascx) - this will not be a subject of this post.

A feature called a transform from the original UrlRewriter.NET comes very handy in this type of situations, as it provides means for mapping or transforming an input value into a different output value. In other words, while we will not have the group Id anywhere in our URL, we will transform it by taking the group name and retrieve its id by looking it up in the database. Best of all, this process is completely transparent to all existing Web parts, and you don't have to touch them at all. This is when custom transforms come into play.

I assume that you already have a custom MonoX-based project up and running. The code presented in this post works with the default installation of MonoX, but you can change the method names and namespaces as it suits you. The attached DLL works as-is with the default package: just drop it in the /bin folder and change the URL rewriting settings as described.

The final solution consists of only two simple classes and a few web.config entries. Let's start with a custom transform:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MonoSoftware.Core;
using MonoSoftware.MonoX.DAL.EntityClasses;
 
namespace MonoSoftware.MonoX.UrlTransforms
{
    public class GroupIdTransform : MonoSoftware.UrlRewriter.IRewriteTransform
    {
        public string ApplyTransform(string input)
        {
 
            using (GroupTransformRepository rep = GroupTransformRepository.GetInstance())
            {
                Guid id = Guid.Empty;
                if (!string.IsNullOrEmpty(input))
                {
                    SnGroupEntity group = rep.GetGroup(input);
                    if (group != null)
                        id = group.Id;
                }
                if (!GuidExtension.IsNullOrEmpty(id))
                    return ShortGuid.Encode(id).ToString();
                else
                    return ShortGuid.Encode(Guid.Empty).ToString();
            }
        }
 
        public string Name
        {
            get
            {
                return "GetId";
            }
        }
 
    }
}

As you can see, this transform class uses a custom repository to look up the group id via its name.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using MonoSoftware.MonoX.DAL;
using MonoSoftware.MonoX.DAL.EntityClasses;
using MonoSoftware.MonoX.DAL.HelperClasses;
using SD.LLBLGen.Pro.ORMSupportClasses;
 
namespace MonoSoftware.MonoX.UrlTransforms
{
    public class GroupTransformRepository : MonoSoftware.MonoX.Repositories.GroupRepository
    {
        protected GroupTransformRepository()
        { }
 
 
        public static new GroupTransformRepository GetInstance()
        {
            return new GroupTransformRepository();
        }
         
        public SnGroupEntity GetGroup(string groupName)
        {
            EntityCollection<SnGroupEntity> groups = new EntityCollection<SnGroupEntity>();
            RelationPredicateBucket filter = new RelationPredicateBucket();
            filter.PredicateExpression.Add(SnGroupFields.Slug == groupName);
 
            IPrefetchPath2 prefetch = new PrefetchPath2((int)EntityType.SnGroupEntity);
            prefetch.Add(SnGroupEntity.PrefetchPathSnGroupCategory);
             
            FetchEntityCollection(groups, filter, 1, null, prefetch);
            if (groups.Count > 0)
                return groups[0];
            else
                return null;
        }
 
    }
}

Note that this is not a production-quality code - it would be a good idea to cache the group name/id pairs to avoid hitting the database too frequently and to improve the exception handling. However, it illustrates the basic idea and can be reused in all other MonoX sections.

The only things that remains to be done is to register the custom transform in the UrlRewriter section of web.config (the first parameter is a fully qualified class name, while the second one is the name of the containing assembly):

...
<UrlRewriter>
    <register transform="MonoSoftware.MonoX.UrlTransforms.GroupIdTransform, UrlRewriterGroupTransform" />
...

You can now modify the existing URL rewriting rules, like the default group view rule, as displayed below:

<rewrite url="^(.*)/Group/(.*)/(\?(.+))?$" to="$1{SocialNetworkingFolder}Groups.aspx?GroupId=${GetId($2)}&$4" name="GroupView" defaultPage="/MonoX/Pages/SocialNetworking/Groups.aspx" urlPattern="/Group/{Slug}/" />

That's it - we now use the format from the beginning of the post: http://localhost/Group/Web-design/ This technique will come useful for all other SEO and URL optimization scenarios.

Attached is a complete solution you can use in your projects. Just drop the DLL to the MonoX bin folder, change the relevant URL rewriting rules in web.config, and you are ready to go. 

Rated 5.00, 1 vote(s).