Linq QueryExtensions as expressions for SQL

Date: 2023-11-09
using System.Linq.Expressions;
using Domain.Forecasts;

namespace EfAdapter.Extensions
{
    public static class QueryExtensions
    {
        public static IQueryable<T> Between<T, TKey>(
            this IQueryable<T> query,
            Expression<Func<T, TKey>> keySelector,
            TKey minValue,
            TKey maxValue)
            where TKey : IComparable<TKey>
        {
            var parameter = keySelector.Parameters.Single();
            var body = Expression.AndAlso(
                Expression.GreaterThanOrEqual(keySelector.Body, Expression.Constant(minValue)),
                Expression.LessThanOrEqual(keySelector.Body, Expression.Constant(maxValue))
            );
            var predicate = Expression.Lambda<Func<T, bool>>(body, parameter);
            return query.Where(predicate);
        }

        public static IQueryable<T> BetweenYearWeek<T>(
            this IQueryable<T> query,
            Expression<Func<T, int>> yearWeekSelector,
            YearWeek minValue,
            YearWeek maxValue)
        {
            var parameter = yearWeekSelector.Parameters.Single();
            var body = Expression.AndAlso(
                Expression.GreaterThanOrEqual(yearWeekSelector.Body, Expression.Constant(minValue.AsInt())),
                Expression.LessThanOrEqual(yearWeekSelector.Body, Expression.Constant(maxValue.AsInt()))
            );
            var predicate = Expression.Lambda<Func<T, bool>>(body, parameter);

            return query.Where(predicate);
        }
    }
    
    /// <summary>
    /// Use as:
    /// .InDateTimeRange(x => x.ValidFrom, x => x.ValidTo, periodFrom, periodTo)
    /// 
    /// Is about equal to:
    /// .Where(x => x.ValidFrom == null || x.ValidFrom <= periodFrom)
    /// .Where(x => x.ValidTo == null || x.ValidTo >= periodTo)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="query"></param>
    /// <param name="minSelector"></param>
    /// <param name="maxSelector"></param>
    /// <param name="minValue"></param>
    /// <param name="maxValue"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static IQueryable<T> InDateTimeRange<T>(
        this IQueryable<T> query,
        Expression<Func<T, DateTime?>> minSelector,
        Expression<Func<T, DateTime?>> maxSelector,
        DateTime? minValue,
        DateTime? maxValue)
    {
        if (query == null) throw new ArgumentNullException(nameof(query));
        if (minSelector == null) throw new ArgumentNullException(nameof(minSelector));
        if (maxSelector == null) throw new ArgumentNullException(nameof(maxSelector));

        // Deze waarden hebben we al vooraf
        minValue ??= DateTime.MinValue;
        maxValue ??= DateTime.MaxValue;

        var minPropertyName = GetPropertyName(minSelector);
        var maxPropertyName = GetPropertyName(maxSelector);

        var parameter = Expression.Parameter(typeof(T), "x");

        var validFromExpression = Expression.Property(parameter, minPropertyName);
        var validToExpression = Expression.Property(parameter, maxPropertyName);

        var nullCheckValidFrom = Expression.Equal(validFromExpression, Expression.Constant(null));
        var validFromCondition = Expression.LessThanOrEqual(validFromExpression, Expression.Constant(minValue, typeof(DateTime?)));

        var nullCheckValidTo = Expression.Equal(validToExpression, Expression.Constant(null));
        var validToCondition = Expression.GreaterThanOrEqual(validToExpression, Expression.Constant(maxValue, typeof(DateTime?)));

        var body = Expression.AndAlso(
            Expression.OrElse(nullCheckValidFrom, validFromCondition),
            Expression.OrElse(nullCheckValidTo, validToCondition)
        );
        return query.Where(Expression.Lambda<Func<T, bool>>(body, parameter));
    }

    private static string GetPropertyName<T, TProp>(Expression<Func<T, TProp>> propertyExpression)
    {
        if (propertyExpression.Body is MemberExpression memberExpression)
            return memberExpression.Member.Name;
        throw new ArgumentException("Invalid property expression", nameof(propertyExpression));
    }

    public static IQueryable<T> InDateTimeRangeIf<T>(this IQueryable<T> source,
        bool condition,
        Expression<Func<T, DateTime?>> minSelector,
        Expression<Func<T, DateTime?>> maxSelector,
        DateTime? minValue,
        DateTime? maxValue)
    { 
        return condition? source.InDateTimeRange(minSelector, maxSelector, minValue, maxValue) : source;
    }
}
81200cookie-checkLinq QueryExtensions as expressions for SQL