ASP.NET. Several Optional Parameters in body of Urls 2

In this post, I describe the implementation detail of the Custom Routing. It extends the capabilities of the ASP.NET MVC routing in such a way that developers can define optional parameters in the Urls paths. The library detects which parameters are provided and which ones are missed in each request or url. In other words, we expects from the library to detect the route values as follows:

http://[DomainName]/austria/tablets/samsung
{Country = austria, category=tablets, brand = samsung}

http://[DomainName]/samsung
{Country= null, Category =null, brand = samsung}

The first step is defining a custom route that gets a list of optional parameters in the path.

  public class CustomRoute : Route
    {
        /// <summary>
        /// it shouldn't hide the base constructors.
        /// </summary>
        public CustomRoute(string url, IRouteHandler routeHandler)
            : base(url, routeHandler)
        {
            this.DefaultUrl = url;
            this.DataTokens = new RouteValueDictionary();
        }

        /// <summary>
        /// it shouldn't hide the base constructors.
        /// </summary>
        public CustomRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
            : base(url, defaults, routeHandler)
        {
            this.DefaultUrl = url;
            this.DataTokens = new RouteValueDictionary();
        }

        protected string DefaultUrl { get; set; }
        public List<string> OptionalParameters { get; set; }

        /// <summary>
        /// Remove parameters that don't have any value
        /// </summary>
        /// <param name="requestContext"></param>
        /// <param name="values"></param>
        /// <returns></returns>
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            this.Url = this.DefaultUrl;

            if (OptionalParameters != null)
            {
                foreach (var key in OptionalParameters)
                {
                    if (values[key] == null)
                    {
                        this.Url = this.Url.Replace(string.Format("/{{{0}}}", key), string.Empty);
                    }
                }
            }

            return base.GetVirtualPath(requestContext, values);
        }
    }

Well, there are two expected functionality. First, creating the Urls from the route, second, mapping the route values of a coming request. The first feature has been implemented in the GetVirtualPath in the above code. The method manipulates the original Url by removing the missing parameters from it. As can be seen, in any call of GetVirtualPath method, the Url will be modified, so there is a DefaultUrl property to keep the original value of the Url.

The second functionality needs a little more work. The library must detect which parameters exist in the coming request and which ones are missed. In order to do so, it needs a plug-in service that must be passed to it in definition of the route. The service has the following interface.

    public interface IRouteValueLookupService
    {
        string GetRouteTypeName(string routeValue);
    }

The interface has only one method. It gets a routeValue and returns back the parameter name of the given value. Unique route values is the main assumption here, or in simple words, optional route parameters don’t have common values.

Now, the question is, where we should embed our customization code.... The Route class gets an instance of IRouteHandler interface. The best option would be passing a customized IRouteHandler to the CustomRoute.

/// <summary>
    /// Customize MvcRouteHandler to return CustomMvcHandler instead of
    /// default IHttpHandler
    /// </summary>
    public class CustomMvcRouteHandler : MvcRouteHandler
    {
        public CustomMvcRouteHandler()
            : base()
        {
        }

        public CustomMvcRouteHandler(IControllerFactory controllerFactory)
            : base(controllerFactory)
        {
        }

        public IRouteValueLookupService RouteValueService { get; set; }

        protected override System.Web.IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
        {
            requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext));
            return new CustomMvcHandler(requestContext)
            {
                RouteValueService = this.RouteValueService
            };
        }
    }

Our customized RouteHandler override the GetHttpHandler method and returned a customized IHttpHandler. Actually, the main work of remapping the parameters will be done in the customized MvcHandler.

  /// <summary>
    /// Extend MvcHandler to sort the Optional Parameters before
    /// Processing the request
    /// </summary>
    class CustomMvcHandler : MvcHandler
    {
        public CustomMvcHandler(RequestContext requestContext)
            : base(requestContext)
        {
        }

        public IRouteValueLookupService RouteValueService { get; set; }

        protected override void ProcessRequest(System.Web.HttpContext httpContext)
        {
            this.SortOptionalParameters();
            base.ProcessRequest(httpContext);
        }

        protected override IAsyncResult BeginProcessRequest(System.Web.HttpContextBase httpContext, AsyncCallback callback, object state)
        {
            this.SortOptionalParameters();
            return base.BeginProcessRequest(httpContext, callback, state);
        }

        protected void SortOptionalParameters()
        {
            CustomRoute customRoute = this.RequestContext.RouteData.Route as CustomRoute;
            RouteValueDictionary routeValues = this.RequestContext.RouteData.Values;

            RouteValueDictionary temp = new RouteValueDictionary();

            var toDelete = new List<string>();

            foreach (var key in routeValues.Keys)
            {
                if (customRoute.OptionalParameters.Count(c => c == key) > 0)
                {
                    var value = routeValues[key].ToString();
                    var routeParameterName = this.RouteValueService.GetRouteTypeName(value);
                    temp[routeParameterName] = value;
                    toDelete.Add(key);
                }
            }

            foreach (var item in toDelete)
            {
                routeValues.Remove(item);
            }

            foreach (var item in temp)
            {
                routeValues[item.Key] = temp[item.Key];
            }
        }
   }

CustomMvcHandler class overrides the ProcessRequest method and remaps the route values there. It is still not finished. We need to add an extension MapRoute method, so that using the code becomes easy.

   /// <summary>
    /// Extend RouteCollection to support adding CustomRoute
    /// </summary>
    public static class CustomRouteCollectionExtensions
    {
        /// <summary>
        /// Create an instance of CustomRoute and register it in the given
            route collection
        /// </summary>
        public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] lookupParameters, IRouteValueLookupService lookupService)
        {
            CustomRoute route = new CustomRoute(url, new CustomMvcRouteHandler()
            {
                RouteValueService = lookupService
            });

            if (defaults != null)
            {
                route.Defaults = CreateRouteValueDictionary(defaults);
            }

            if (lookupParameters != null)
            {
                route.OptionalParameters = lookupParameters.ToList();
            }

            routes.Add(name, route);

            return route;
        }

        private static RouteValueDictionary CreateRouteValueDictionary(object values)
        {
            var dictionary = values as IDictionary<string, object>;
            if (dictionary != null)
            {
                return new RouteValueDictionary(dictionary);
            }

            return new RouteValueDictionary(values);
        }

    }

That is finished. You can download the latest version from here. Now one can easily define the routes as follows:

routes.MapRoute(
               name: "Test",
               url: "Test/{Country}/{Maker}/{Type}",
               defaults: new { controller = "Test", action = "Index", Country = UrlParameter.Optional, Maker = UrlParameter.Optional, Type = UrlParameter.Optional },
               lookupParameters: new string[] { "Country", "Maker", "Type" },
               lookupService: new LookupService());


No Comments

Post Reply