Tuesday, August 17, 2010

Dynamic SQL-like Linq OrderBy Extension

So, it's been a while, but I thought I take moment and do my annual blog post ;).

I've been playing around with ASP.NET MVC and the Linq stuff for NHibernate recently. I was in need of an OrderBy extension method that could take a SQL-Like OrderBy string and sort a IQueryable<> or IEnumerable<> collection. I wrote up an implementation that worked, but I just wasn't satisfied with its internals (quite a bit of reflection to get the correct type to construct a LambdaExpression, etc)

At any rate, I couldn't leave well enough alone, and, after a bit of Googling, I ran across this StackOverflow answer about Dynamic LINQ OrderBy. The extension method wasn't exactly what I was looking for, but that ApplyOrder method is slick, and solved the portion of my implementation that was bothering me.

So, I though I would post up my version in case anybody finds it useful. It handles the following inputs:

list.OrderBy("SomeProperty");
list.OrderBy("SomeProperty DESC");
list.OrderBy("SomeProperty DESC, SomeOtherProperty");
list.OrderBy("SomeSubObject.SomeProperty ASC, SomeOtherProperty DESC");
Dynamic SQL-like Linq OrderBy Extension
    public static class OrderByHelper
    {
        public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> enumerable, string orderBy)
        {
            return enumerable.AsQueryable().OrderBy(orderBy).AsEnumerable();
        }

        public static IQueryable<T> OrderBy<T>(this IQueryable<T> collection, string orderBy)
        {
            foreach(OrderByInfo orderByInfo in ParseOrderBy(orderBy))
                collection = ApplyOrderBy<T>(collection, orderByInfo);

            return collection;
        }

        private static IQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo)
        {
            string[] props = orderByInfo.PropertyName.Split('.');
            Type type = typeof(T);

            ParameterExpression arg = Expression.Parameter(type, "x");
            Expression expr = arg;
            foreach (string prop in props)
            {
                // use reflection (not ComponentModel) to mirror LINQ
                PropertyInfo pi = type.GetProperty(prop);
                expr = Expression.Property(expr, pi);
                type = pi.PropertyType;
            }
            Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
            LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
            string methodName = String.Empty;

            if (!orderByInfo.Initial && collection is IOrderedQueryable<T>)
            {
                if (orderByInfo.Direction == SortDirection.Ascending)
                    methodName = "ThenBy";
                else
                    methodName = "ThenByDescending";
            }
            else
            {
                if (orderByInfo.Direction == SortDirection.Ascending)
                     methodName = "OrderBy";
                else
                     methodName = "OrderByDescending";
            }

            //TODO: apply caching to the generic methodsinfos?
            return (IOrderedQueryable<T>)typeof(Queryable).GetMethods().Single(
                method => method.Name == methodName
                        && method.IsGenericMethodDefinition
                        && method.GetGenericArguments().Length == 2
                        && method.GetParameters().Length == 2)
                .MakeGenericMethod(typeof(T), type)
                .Invoke(null, new object[] { collection, lambda });

        }

        private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy)
        {
            if (String.IsNullOrEmpty(orderBy))
                yield break;

            string[] items = orderBy.Split(',');
            bool initial = true;
            foreach(string item in items)
            {
                string[] pair = item.Trim().Split(' ');

                if (pair.Length > 2)
                    throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC",item));

                string prop = pair[0].Trim();

                if(String.IsNullOrEmpty(prop))
                    throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");
                
                SortDirection dir = SortDirection.Ascending;
                
                if (pair.Length == 2)
                    dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending);

                yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial };

                initial = false;
            }

        }

        private class OrderByInfo
        {
            public string PropertyName { get; set; }
            public SortDirection Direction { get; set; }
            public bool Initial { get; set; }
        }

        private enum SortDirection
        {
            Ascending = 0,
            Descending = 1
        }
    }
Anyway, hope someone finds it useful. And if you see any areas that could use some TLC please let me know A

Friday, July 10, 2009

URL Rewriting and ASP.NET Login Controls

So, this is the initial post to the Blog I set up almost a year ago (How's that for procrastinating). I figure rather than a bunch of boring, all about me, blah blah blah, I would write about an issue I ran into today, that might be of use to someone else. So here goes...

I've recently been working on a .NET WinForms site that employs quite a bit of URL Rewriting. I'm using UrlRewriter.NET to handle the rewriting. I ran up against the issue, mentioned elsewhere, with URL rewriting and the ASP.NET Login Controls. Specifically, left in their default state, both the Login control and the LoginStatus control will redirect to the rewritten URL (the one I don't want my users to see) rather than my pretty original URL.

The solution I came up with to fix the issue across all instances of the Login controls in the site, which I hadn't run across on the 'tubes, was to create a simple custom control, employ Request.RawUrl to get the original URL, and use tag mapping to replace the problematic controls.

In order to implement this little fix I needed to add the following two classes to my App_Code directory.

New Login Control

public class UrlRewriteLogin : Login
{
   public override string DestinationPageUrl
   {
       get
       {
           if (String.IsNullOrEmpty(base.DestinationPageUrl))
               return this.Page.Request.RawUrl;

           return base.DestinationPageUrl;
       }
       set
       {
           base.DestinationPageUrl = value;
       }
   }
}

The mods to the Login control are fairly straightforward. Using .NET Reflector I can see that the issues arise in the Login Control's private GetRedirectUrl method. When DestinationPageUrl is empty, the method returns Page.Request.Path which shows the underlying URL. If we return the Request.RawUrl from an overridden DestinationPageUrl when the base DestinationPageUrl is blank, we never hit the problematic code, get the desired behavior, and still allow for a custom DestinationPageUrl

New LoginStatus Control

public class UrlRewiteLoginStatus : LoginStatus
{
   public override LogoutAction LogoutAction
   {
       get
       {
           if (base.LogoutAction == LogoutAction.Refresh)
               return LogoutAction.Redirect;

           return base.LogoutAction;
       }
       set
       {
           base.LogoutAction = value;
       }
   }

   public override string LogoutPageUrl
   {
       get
       {
           if (base.LogoutAction == LogoutAction.Refresh)
               return this.Page.Request.RawUrl;

           return base.LogoutPageUrl;
       }
       set
       {
           base.LogoutPageUrl = value;
       }
   }
}

These mods are a bit more tricky. Again using .NET Reflector I see that the issues arise in the LoginStatus Control's private LogoutClicked method, but only when LogoutAction is Refresh (the default). The problem is the same as above, if the LogoutAction is Refresh, the control uses Page.Request.Path. My solution here was twofold: 1.) Never return a LogoutAction of Refresh, and 2.) When base control's LogoutAction is Refresh, return the Request.RawUrl from LogoutPageUrl. This skips the problematic code, but still allows for normal LogoutPageUrl behavior.

Note:The override of LogoutAction might be a bit confusing to a developer unaware of the hack, as even if you manually set LogoutAction to Refresh, it never returns Refresh from the getter.

Then, all I needed to do is add the following tagMappings to our web.config. Now, every instance of <asp:Login> and <asp:LoginStatus /> are the new custom controls. w00t!

<pages>
  <tagMapping>
     <add tagType="System.Web.UI.WebControls.Login"
         mappedTagType="UrlRewriteLogin"/>
     <add tagType="System.Web.UI.WebControls.LoginStatus"
         mappedTagType="UrlRewiteLoginStatus"/>
  </tagMapping>
</pages>

Hope this is useful for someone else.