miércoles, 3 de diciembre de 2014

Universal PredicateBuilder... tunned for Azure AD Graph API queries

Las consultas a la Azure AD Graph API no admiten le sean provistos valores nulos o vacíos, y las opciones de filtro soportadas al día de hoy se limitan a and, or, eq, ge, le, Startswith, y Any.

La Microsoft Azure Active Directory Graph Client Library facilita notablemente el manejo de estas consultas desde las aplicaciones .NET, pero aún es algo incomodo la construcción dinámica de predicados, debido a lo cual recurrí para esta labor al PredicateBuilder generosamente compartido por Pete Montgomery allá por febrero del 2011.

Afortunadamente todo el entorno de Microsoft Azure está evolucionando rápidamente así que es de esperar estas limitaciones no perduren por mucho tiempo, pero comparto esta pequeña solución por si algún otro desarrollador se encuentra ante la misma situación.

Los ajustes introducidos a la versión de Pete fueron mínimos, y solo para asegurar la compatibilidad con los operadores lógicos and y or (Pete usó AndAlso, y OrElse, no soportados por la Graph API).

Consulta de usuarios

Basada en AzureADSamples/ConsoleApp-GraphAPI-DotNet

    IPagedCollection<IUser> searchResults = null;

    var predicate = GraphAPIPredicateBuilder.Create<IUser>(u => u.AccountEnabled == true);
    if (!string.IsNullOrWhiteSpace(canonicalUser.Name))
        predicate = predicate.And(u => u.UserPrincipalName.StartsWith(canonicalUser.Name));
    if (!string.IsNullOrWhiteSpace(displayname))
        predicate = predicate.And(u => u.DisplayName.StartsWith(displayname));
    if (!string.IsNullOrWhiteSpace(description))
        predicate = predicate.And(u => u.GivenName.StartsWith(givenname));

    try
    {
        ActiveDirectoryClient activeDirectoryClient =
            AzureADAuthenticationHelper.GetActiveDirectoryClientAsApplication();
        IUserCollection userCollection = activeDirectoryClient.Users;
        searchResults = userCollection.Where(predicate).ExecuteAsync().Result;
    }
    catch (Exception e)
    {
        Debug.WriteLine(string.Format("Error searching users - {0} {1}", e.Message,
            e.InnerException != null ? e.InnerException.Message : ""));
    }


GraphAPIPredicateBuilder


    /// <summary>
    /// Enables the efficient, dynamic composition of query predicates... tunned for Azure AD Graph API
    /// 
    /// Based on the work of Pete Montgomery at February 10, 2011
    /// http://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/
    /// </summary>
    public static class GraphAPIPredicateBuilder
    {
        /// <summary>
        /// Creates a predicate that evaluates to true.
        /// </summary>
        public static Expression<Func<T, bool>> True<T>() { return param => true; }

        /// <summary>
        /// Creates a predicate that evaluates to false.
        /// </summary>
        public static Expression<Func<T, bool>> False<T>() { return param => false; }

        /// <summary>
        /// Creates a predicate expression from the specified lambda expression.
        /// </summary>
        public static Expression<Func<T, bool>> Create<T>(Expression<Func<T, bool>> predicate) { return predicate; }

        /// <summary>
        /// Combines the first predicate with the second using the logical "and".
        /// </summary>
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, Expression.And);
        }

        /// <summary>
        /// Combines the first predicate with the second using the logical "andAlso".
        /// </summary>
        public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, Expression.AndAlso);
        }

        /// <summary>
        /// Combines the first predicate with the second using the logical "or".
        /// </summary>
        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, Expression.Or);
        }

        /// <summary>
        /// Combines the first predicate with the second using the logical "orElse".
        /// </summary>
        public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, Expression.OrElse);
        }

        /// <summary>
        /// Negates the predicate.
        /// </summary>
        public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression)
        {
            var negated = Expression.Not(expression.Body);
            return Expression.Lambda<Func<T, bool>>(negated, expression.Parameters);
        }

        /// <summary>
        /// Combines the first expression with the second using the specified merge function.
        /// </summary>
        static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
        {
            // zip parameters (map from parameters of second to parameters of first)
            var map = first.Parameters
                .Select((f, i) => new { f, s = second.Parameters[i] })
                .ToDictionary(p => p.s, p => p.f);

            // replace parameters in the second lambda expression with the parameters in the first
            var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);

            // create a merged lambda expression with parameters from the first expression
            return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
        }

        class ParameterRebinder : ExpressionVisitor
        {
            readonly Dictionary<ParameterExpression, ParameterExpression> map;

            ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
            {
                this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
            }

            public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
            {
                return new ParameterRebinder(map).Visit(exp);
            }

            protected override Expression VisitParameter(ParameterExpression p)
            {
                ParameterExpression replacement;

                if (map.TryGetValue(p, out replacement))
                {
                    p = replacement;
                }

                return base.VisitParameter(p);
            }
        }
    }


That's all folks!


No hay comentarios:

Publicar un comentario