Newer
Older
Warehouse / src / Infrastructure / Common / Extensions / QueryableExtensions.cs
@Derek Comartin Derek Comartin on 22 Aug 2023 4 KB Init
using StringToExpression.LanguageDefinitions;

namespace MyWarehouse.Infrastructure;

internal static class QueryableExtensions
{
    /// <summary>
    /// Applies filtering to a query by parsing the provided OData-standard filter string and translating it into expresions.
    /// </summary>
    /// <exception cref="FormatException">Thrown when the filter string is incorrectly formatted.</exception>
    public static IQueryable<T> ApplyFilter<T>(this IQueryable<T> query, string? oDataFilterString)
    {
        try
        {
            return ApplyFilterInternal(query, oDataFilterString);
        }
        catch(Exception e)
        {
            throw new FormatException($"The specified filter string '{oDataFilterString}' is invalid.", e);
        }
    }

    /// <summary>
    /// Applies sorting to a query by parsing the provided OData-standard OrderBy string and translating it into expressions.
    /// General expected string format is 'propertyName1, properyName2 asc, propertyName3 desc'. Specifying 'asc'/'desc' is optional. Nested property access is supported with '/' (e.g. Customer/Name).
    /// </summary>
    /// <exception cref="FormatException">Thrown when the orderBy string is incorrectly formatted.</exception>
    public static IQueryable<T> ApplyOrder<T>(this IQueryable<T> query, string? oDataOrderByString, int maximumNumberOfOrdering = 5)
    {
        try
        {
            return ApplyOrderInternal(query, oDataOrderByString, maximumNumberOfOrdering);
        }
        catch(Exception e)
        {
            throw new FormatException($"The specified orderBy string '{oDataOrderByString}' is invalid.", e);
        }
    }

    /// <summary>
    /// Applies paging to a query expression, where index 1 is the first page.
    /// </summary>
    public static IQueryable<T> ApplyPaging<T>(this IQueryable<T> query, int pageSize, int pageIndex)
        => query
            .Skip((pageIndex - 1) * pageSize)
            .Take(pageSize);

    private static IQueryable<T> ApplyFilterInternal<T>(IQueryable<T> query, string? oDataFilterString)
    {
        if (string.IsNullOrWhiteSpace(oDataFilterString))
        {
            return query;
        }

        var filterExpression = new ODataFilterLanguage().Parse<T>(oDataFilterString);
        return query.Where(filterExpression);
    }

    private static IQueryable<T> ApplyOrderInternal<T>(IQueryable<T> query, string? oDataOrderByString, int maximumNumberOfOrdering)
    {
        if (string.IsNullOrWhiteSpace(oDataOrderByString))
        {
            return query;
        }

        bool firstOrdering = true;
        foreach (var (propertyName, order) in GetOrderEntries(oDataOrderByString, maximumNumberOfOrdering))
        {
            query = ApplyOrdering(query, propertyName, order, firstOrdering);
            firstOrdering = false;
        }

        //TODO: Look into the necessity (or lack thereof) of filtering out multiple orderBy on the same property.
        return query;

        static IEnumerable<(string propertyPath, SortOrder order)> GetOrderEntries(string orderByString, int maxOrders)
        {
            return orderByString
                .Split(',', count: maxOrders, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                .Select(orderStr =>
                {
                    var divider = orderStr.IndexOf(' ');
                    if (divider < 0) return (propertyPath: orderStr, order: SortOrder.Asc);
                    else return (
                        propertyPath: orderStr[0..divider],
                        order: Enum.Parse<SortOrder>(orderStr[divider..].Trim(), ignoreCase: true)
                    );
                });
        }

        static IQueryable<T> ApplyOrdering(IQueryable<T> query, string propertyPath, SortOrder order, bool firstOrdering)
        {
            var param = Expression.Parameter(typeof(T), "p");
            var member = (MemberExpression)propertyPath.Split('/').Aggregate((Expression)param, Expression.Property); //Expression.Property(param, propertyPath);
            var exp = Expression.Lambda(member, param);
            string methodName = order switch
            {
                SortOrder.Asc => firstOrdering ? "OrderBy" : "ThenBy",
                SortOrder.Desc => firstOrdering ? "OrderByDescending" : "ThenByDescending"
            };
            Type[] types = new Type[] { query.ElementType, exp.Body.Type };
            var orderByExpression = Expression.Call(typeof(Queryable), methodName, types, query.Expression, exp);
            return query.Provider.CreateQuery<T>(orderByExpression);
        }
    }

    private enum SortOrder
    {
        Asc,
        Desc
    }
}