onebeyond/onebeyond-studio-core

View on GitHub
src/OneBeyond.Studio.Application.SharedKernel/Specifications/Includes.cs

Summary

Maintainability
B
4 hrs
Test Coverage
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using EnsureThat;

namespace OneBeyond.Studio.Application.SharedKernel.Specifications;

/// <summary>
/// </summary>
public static class Includes
{
    /// <summary>
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    /// <param name="includes"></param>
    /// <returns></returns>
    public static Includes<TEntity> Create<TEntity>(params Expression<Func<TEntity, object>>[] includes)
    {
        EnsureArg.IsNotNull(includes, nameof(includes));

        return includes.Aggregate(
            new Includes<TEntity>(false),
            (includes, include) => includes.Include(include));
    }
}

/// <summary>
/// Defines related object graph by navigation properties.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class Includes<TEntity> : IIncludes<TEntity>
{
    /// <param name="haveCartesianExplosion">
    /// Gives a hint on the fact that produced includes chain is a subject to
    /// the <see href="https://www.thinktecture.com/en/entity-framework-core/cartesian-explosion-problem-in-3-1/">Cartesian Explosion</see> problem.
    /// This hint allows its mitigation by enabling the <see href="https://docs.microsoft.com/en-us/ef/core/querying/single-split-queries">Query Splitting</see>
    /// feature of EF Core.
    /// </param>
    public Includes(bool haveCartesianExplosion = false)
    {
        HaveCartesianExplosion = haveCartesianExplosion;
    }

    public bool HaveCartesianExplosion { get; }

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// The <see cref="Includes{TObject, TProperty}.ThenInclude{TNextChild}(Expression{Func{TProperty, TNextChild}})"/> method
    /// can be used for further building the graph.
    /// </summary>
    /// <typeparam name="TChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild> Include<TChild>(
        Expression<Func<TEntity, TChild>> navigation)
        where TChild : class
        => new(this, navigation);

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// The <see cref="Includes{TObject, TProperty}.ThenInclude{TNextChild}(Expression{Func{TProperty, TNextChild}})"/> method
    /// can be used for further building the graph.
    /// </summary>
    /// <typeparam name="TChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild> Include<TChild>(
        Expression<Func<TEntity, IEnumerable<TChild>>> navigation)
        where TChild : class
        => new(this, navigation);

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// The <see cref="Includes{TObject, TProperty}.ThenInclude{TNextChild}(Expression{Func{TProperty, TNextChild}})"/> method
    /// can be used for further building the graph.
    /// </summary>
    /// <typeparam name="TChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild> Include<TChild>(
        Expression<Func<TEntity, ICollection<TChild>>> navigation)
        where TChild : class
        => new(
            this,
            Expression.Lambda<Func<TEntity, IEnumerable<TChild>>>(
                navigation.Body,
                navigation.Parameters));

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// The <see cref="Includes{TObject, TProperty}.ThenInclude{TNextChild}(Expression{Func{TProperty, TNextChild}})"/> method
    /// can be used for further building the graph.
    /// </summary>
    /// <typeparam name="TChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild> Include<TChild>(
        Expression<Func<TEntity, IReadOnlyCollection<TChild>>> navigation)
        where TChild : class
        => new(
            this,
            Expression.Lambda<Func<TEntity, IEnumerable<TChild>>>(
                navigation.Body,
                navigation.Parameters));

    /// <summary>
    /// Replays the selected graph on another one.
    /// </summary>
    /// <typeparam name="TIncludes"></typeparam>
    /// <param name="includes"></param>
    /// <returns></returns>
    public virtual TIncludes Replay<TIncludes>(TIncludes includes)
        where TIncludes : class, IIncludes<TEntity>
        => includes;

    IIncludes<TEntity, TChild> IIncludes<TEntity>.Include<TChild>(
        Expression<Func<TEntity, TChild>> navigation)
        => Include(navigation);

    IIncludes<TEntity, TChild> IIncludes<TEntity>.Include<TChild>(
        Expression<Func<TEntity, IEnumerable<TChild>>> navigation)
        => Include(navigation);

    IIncludes<TEntity, TChild> IIncludes<TEntity>.Include<TChild>(
        Expression<Func<TEntity, ICollection<TChild>>> navigation)
        => Include(navigation);

    IIncludes<TEntity, TChild> IIncludes<TEntity>.Include<TChild>(
        Expression<Func<TEntity, IReadOnlyCollection<TChild>>> navigation)
        => Include(navigation);
}

/// <summary>
/// Defines related object graph by navigation properties.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TChild"></typeparam>
public class Includes<TEntity, TChild> : Includes<TEntity>, IIncludes<TEntity, TChild>
    where TChild : class
{
    private readonly Expression<Func<TEntity, TChild>>? _navigation1;
    private readonly Expression<Func<TEntity, IEnumerable<TChild>>>? _navigation2;
    private readonly List<Expression<Func<TChild, bool>>> _predicates;

    internal Includes(Includes<TEntity> previous, Expression<Func<TEntity, TChild>> navigation)
        : this(previous)
    {
        EnsureArg.IsNotNull(navigation, nameof(navigation));

        _navigation1 = navigation;
        _navigation2 = default;
    }

    internal Includes(Includes<TEntity> previous, Expression<Func<TEntity, IEnumerable<TChild>>> navigation)
        : this(previous)
    {
        EnsureArg.IsNotNull(navigation, nameof(navigation));

        _navigation1 = default;
        _navigation2 = navigation;
    }

    /// <summary>
    /// </summary>
    internal protected Includes(Includes<TEntity> previous)
        : base(previous.HaveCartesianExplosion)
    {
        EnsureArg.IsNotNull(previous, nameof(previous));

        _predicates = new List<Expression<Func<TChild, bool>>>();
        Previous = previous;
    }

    protected Includes<TEntity> Previous { get; }
    protected IEnumerable<Expression<Func<TChild, bool>>> Predicates => _predicates;

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild, TNextChild> ThenInclude<TNextChild>(
        Expression<Func<TChild, TNextChild>> navigation)
        where TNextChild : class
        => new(this, navigation);

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild, TNextChild> ThenInclude<TNextChild>(
        Expression<Func<TChild, IEnumerable<TNextChild>>> navigation)
        where TNextChild : class
        => new(this, navigation);

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild, TNextChild> ThenInclude<TNextChild>(
        Expression<Func<TChild, ICollection<TNextChild>>> navigation)
        where TNextChild : class
        => new(
            this,
            Expression.Lambda<Func<TChild, IEnumerable<TNextChild>>>(
                navigation.Body,
                navigation.Parameters));

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild, TNextChild> ThenInclude<TNextChild>(
        Expression<Func<TChild, IReadOnlyCollection<TNextChild>>> navigation)
        where TNextChild : class
        => new(
            this,
            Expression.Lambda<Func<TChild, IEnumerable<TNextChild>>>(
                navigation.Body,
                navigation.Parameters));

    /// <summary>
    /// </summary>
    /// <param name="predicate"></param>
    /// <returns></returns>
    public Includes<TEntity, TChild> Where(Expression<Func<TChild, bool>> predicate)
    {
        _predicates.Add(predicate);
        return this;
    }

    /// <summary>
    /// Replays the selected graph on another one.
    /// </summary>
    /// <typeparam name="TIncludes"></typeparam>
    /// <param name="includes"></param>
    /// <returns></returns>
    public override TIncludes Replay<TIncludes>(TIncludes includes)
    {
        EnsureArg.IsNotNull(includes, nameof(includes));

        includes = Previous.Replay(includes);
        var updatedIncludes = _navigation1 == default
            ? includes.Include(_navigation2!)
            : includes.Include(_navigation1);
        updatedIncludes = Predicates.Aggregate(
            updatedIncludes,
            (result, predicate) => result.Where(predicate));
        return (TIncludes)updatedIncludes;
    }

    IIncludes<TEntity, TNextChild> IIncludes<TEntity, TChild>.ThenInclude<TNextChild>(
        Expression<Func<TChild, TNextChild>> navigation)
        => ThenInclude(navigation);

    IIncludes<TEntity, TNextChild> IIncludes<TEntity, TChild>.ThenInclude<TNextChild>(
        Expression<Func<TChild, IEnumerable<TNextChild>>> navigation)
        => ThenInclude(navigation);

    IIncludes<TEntity, TNextChild> IIncludes<TEntity, TChild>.ThenInclude<TNextChild>(
        Expression<Func<TChild, ICollection<TNextChild>>> navigation)
        => ThenInclude(navigation);

    IIncludes<TEntity, TNextChild> IIncludes<TEntity, TChild>.ThenInclude<TNextChild>(
        Expression<Func<TChild, IReadOnlyCollection<TNextChild>>> navigation)
        => ThenInclude(navigation);

    IIncludes<TEntity, TChild> IIncludes<TEntity, TChild>.Where(Expression<Func<TChild, bool>> predicate)
        => Where(predicate);
}

/// <summary>
/// Defines related object graph by navigation properties.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TChild"></typeparam>
/// <typeparam name="TNextChild"></typeparam>
public class Includes<TEntity, TChild, TNextChild> : Includes<TEntity, TNextChild>
    where TNextChild : class
{
    private readonly Expression<Func<TChild, TNextChild>>? _navigation1;
    private readonly Expression<Func<TChild, IEnumerable<TNextChild>>>? _navigation2;

    internal Includes(Includes<TEntity> previous, Expression<Func<TChild, TNextChild>> navigation)
        : base(previous)
    {
        EnsureArg.IsNotNull(navigation, nameof(navigation));

        _navigation1 = navigation;
        _navigation2 = default;
    }

    internal Includes(Includes<TEntity> previous, Expression<Func<TChild, IEnumerable<TNextChild>>> navigation)
        : base(previous)
    {
        EnsureArg.IsNotNull(navigation, nameof(navigation));

        _navigation1 = default;
        _navigation2 = navigation;
    }

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild1"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public new Includes<TEntity, TNextChild1> ThenInclude<TNextChild1>(
        Expression<Func<TNextChild, TNextChild1>> navigation)
        where TNextChild1 : class
        => new Includes<TEntity, TNextChild, TNextChild1>(this, navigation);

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild1"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public new Includes<TEntity, TNextChild1> ThenInclude<TNextChild1>(
        Expression<Func<TNextChild, IEnumerable<TNextChild1>>> navigation)
        where TNextChild1 : class
        => new Includes<TEntity, TNextChild, TNextChild1>(this, navigation);

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild1"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public new Includes<TEntity, TNextChild1> ThenInclude<TNextChild1>(
        Expression<Func<TNextChild, ICollection<TNextChild1>>> navigation)
        where TNextChild1 : class
        => new Includes<TEntity, TNextChild, TNextChild1>(
            this,
            Expression.Lambda<Func<TNextChild, IEnumerable<TNextChild1>>>(
                navigation.Body,
                navigation.Parameters));

    /// <summary>
    /// Adds a related object into the graph via a navigation property.
    /// </summary>
    /// <typeparam name="TNextChild1"></typeparam>
    /// <param name="navigation"></param>
    /// <returns></returns>
    public new Includes<TEntity, TNextChild1> ThenInclude<TNextChild1>(
        Expression<Func<TNextChild, IReadOnlyCollection<TNextChild1>>> navigation)
        where TNextChild1 : class
        => new Includes<TEntity, TNextChild, TNextChild1>(
            this,
            Expression.Lambda<Func<TNextChild, IEnumerable<TNextChild1>>>(
                navigation.Body,
                navigation.Parameters));

    /// <summary>
    /// Replays the selected graph on another one.
    /// </summary>
    /// <typeparam name="TIncludes"></typeparam>
    /// <param name="includes"></param>
    /// <returns></returns>
    public override TIncludes Replay<TIncludes>(TIncludes includes)
    {
        EnsureArg.IsNotNull(includes, nameof(includes));

        includes = Previous.Replay(includes);
        var updatedIncludes = _navigation1 == default
            ? ((IIncludes<TEntity, TChild>)includes).ThenInclude(_navigation2!)
            : ((IIncludes<TEntity, TChild>)includes).ThenInclude(_navigation1);
        updatedIncludes = Predicates.Aggregate(
            updatedIncludes,
            (result, predicate) => result.Where(predicate));
        return (TIncludes)updatedIncludes;
    }
}