Friday, March 25, 2011

LINQ To LDAP: Sorting

I recently checked in my implementation of sorting for LINQ to LDAP. It's a pretty straight forward implementation. I have sort options that I pick up when visiting the expression and they get applied when I execute the request. Here's my QueryCommand that handles standard execution:
internal class StandardQueryCommand : QueryCommand
{
    public StandardQueryCommand(IQueryCommandOptions options, IObjectMapping mapping)
        : base(options, mapping, true)
    {
    }

    public override object Execute(DirectoryConnection connection, SearchScope scope)
    {
        SearchRequest.Scope = scope;
            
        var isSorted = Options.SortingOptions != null;
        if (isSorted)
        {
            var sortRequest = new SortRequestControl(Options.SortingOptions.Keys)
                                    {
                                        IsCritical = false
                                    };
            SearchRequest.Controls.Add(sortRequest);
        }
            
        return Options.PagingOptions == null 
            ? HandleStandardRequest(isSorted, connection) 
            : HandlePagedRequest(connection);
    }

    private object HandleStandardRequest(bool isSorted, DirectoryConnection connection)
    {
        if (isSorted)
        {
            var pageRequest = new PageResultRequestControl(LdapConfiguration.ServerMaxPageSize);
            SearchRequest.Controls.Add(pageRequest);
        }
        var response = connection.SendRequest(SearchRequest) as SearchResponse;

        AssertSuccess(response);

        AssertSortSuccess(response.Controls);
        return Options.GetEnumerator(response.Entries);
    }

    private object HandlePagedRequest(DirectoryConnection connection)
    {
        var pageRequestControl = new PageResultRequestControl(Options.PagingOptions.PageSize)
                                        {
                                            Cookie = Options.PagingOptions.NextPage
                                        };

        SearchRequest.Controls.Add(pageRequestControl);

        var response = connection.SendRequest(SearchRequest) as SearchResponse;

        AssertSuccess(response);

        AssertSortSuccess(response.Controls);
        var pageResponseControl = GetControl<PageResultResponseControl>(response.Controls);
        var parameters = new[]
                                {
                                    Options.PagingOptions.PageSize, 
                                    pageResponseControl.Cookie,
                                    Options.GetEnumerator(response.Entries)
                                };

        return Activator.CreateInstance(
            typeof(LdapPage<>).MakeGenericType(Options.GetEnumeratorReturnType()),
            parameters);
    }

    private static void AssertSortSuccess(IEnumerable<DirectoryControl> controls)
    {
        var control = GetControl<SortResponseControl>(controls);

        if (control != null && control.Result != ResultCode.Success)
        {
            throw new LdapException(
                string.Format("Sort request returned {0}", control.Result));
        }
    }
}

Sort requests require paging or else you'll get a size limit exceeded if you have too many results. I thought this was all I needed to worry about, but when I tried to filter 50K entries I found another gem. The response of the sort request was "UnwillingToPerform". I felt like my laptop just told me to take a hike. I Googled and only turned up that something was wrong with my sort. I looked at my query and it hit me:
var orderBy = context.Query<User>()
    .OrderByDescending(u => u.LastName)
    .Select(u => new User {CommonName = u.CommonName, FirstName = u.FirstName, LastName = u.LastName})
    .ToPage(50);

Basically this is the same error as size limit exceeded. Even with a page request, the filter is too broad to effectively perform a server side sort, so it fails. I applied a filter and everything worked.

Sorting is not an efficient operation for the server which is why it has a size limit. Unless you're working on a horribly constrained client, I recommend doing your sorting there.