onebeyond/onebeyond-studio-core

View on GitHub
src/OneBeyond.Studio.Application.SharedKernel/QueryHandlers/ListHandler.cs

Summary

Maintainability
A
1 hr
Test Coverage
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using EnsureThat;
using MediatR;
using OneBeyond.Studio.Application.SharedKernel.Entities.Dto;
using OneBeyond.Studio.Application.SharedKernel.Entities.Queries;
using OneBeyond.Studio.Application.SharedKernel.Repositories;
using OneBeyond.Studio.Application.SharedKernel.Specifications;
using OneBeyond.Studio.Crosscuts.Expressions;
using OneBeyond.Studio.Crosscuts.Strings;
using OneBeyond.Studio.Domain.SharedKernel.Entities;
using OneBeyond.Studio.Domain.SharedKernel.Specifications;

#nullable enable

namespace OneBeyond.Studio.Application.SharedKernel.QueryHandlers;

/// <summary>
/// </summary>
/// <typeparam name="TResultDto"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TEntityId"></typeparam>
public class ListHandler<TResultDto, TEntity, TEntityId>
    : ListHandler<List<TResultDto, TEntity, TEntityId>, TResultDto, TEntity, TEntityId>
    where TEntity : DomainEntity<TEntityId>
    where TEntityId : notnull
{
    /// <summary>
    /// </summary>
    /// <param name="repository"></param>
    public ListHandler(IRORepository<TEntity, TEntityId> repository)
        : base(repository)
    {
    }
}

/// <summary>
/// </summary>
public abstract class ListHandler<TQuery, TResultDto, TEntity, TEntityId>
    : IRequestHandler<TQuery, PagedList<TResultDto>>
    where TQuery : List<TResultDto, TEntity, TEntityId>
    where TEntity : DomainEntity<TEntityId>
    where TEntityId : notnull
{
    /// <summary>
    /// </summary>
    /// <param name="repository"></param>
    protected ListHandler(IRORepository<TEntity, TEntityId> repository)
    {
        EnsureArg.IsNotNull(repository, nameof(repository));

        Repository = repository;
    }

    /// <summary>
    /// </summary>
    protected IRORepository<TEntity, TEntityId> Repository { get; }

    /// <summary>
    /// </summary>
    public virtual async Task<PagedList<TResultDto>> Handle(
        TQuery query,
        CancellationToken cancellationToken)
    {
        EnsureArg.IsNotNull(query, nameof(query));

        var preFilterExpression = await GetPreFilterExpressionAsync(
            query,
            cancellationToken).ConfigureAwait(false);

        var filterExpression = await GetFilterExpressionAsync(
            query,
            cancellationToken).ConfigureAwait(false);

        var sortExpressions = await GetSortExpressionsAsync(
            query,
            cancellationToken).ConfigureAwait(false);

        var paging = query.PageSize == null
            ? null
            : new Paging((query.PageNo - 1) * query.PageSize.Value, query.PageSize.Value);

        var sorting = sortExpressions;

        var pagedList = new PagedList<TResultDto>
        {
            Data = await Repository.ListAsync(
                preFilterExpression,
                filterExpression,
                paging,
                sorting,
                cancellationToken).ConfigureAwait(false),
            Count = await Repository.CountAsync(
                preFilterExpression,
                filterExpression,
                cancellationToken).ConfigureAwait(false)
        };

        return pagedList;
    }

    /// <summary>
    /// </summary>
    /// <param name="query"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    protected virtual ValueTask<Expression<Func<TEntity, bool>>?> GetPreFilterExpressionAsync(
        TQuery query,
        CancellationToken cancellationToken)
        => default;

    /// <summary>
    /// </summary>
    /// <param name="query"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    protected virtual ValueTask<Expression<Func<TResultDto, bool>>?> GetFilterExpressionAsync(
        TQuery query,
        CancellationToken cancellationToken)
        => new ValueTask<Expression<Func<TResultDto, bool>>?>(
            GetFilterExpression(query.FilterByFields, query.SearchText, query.ParentId));

    /// <summary>
    /// </summary>
    /// <param name="filterByFields"></param>
    /// <returns></returns>
    protected virtual Expression<Func<TResultDto, bool>>? GetFilterExpression(
        IReadOnlyDictionary<string, IReadOnlyCollection<string>> filterByFields)
        => FilterExpressionBuilder<TResultDto>.Build(filterByFields);

    /// <summary>
    /// </summary>
    protected virtual Expression<Func<TResultDto, bool>>? GetSearchExpression(string searchText)
        => default;

    /// <summary>
    /// Defines an expression checking whether a <typeparamref name="TResultDto"/> is bound to
    /// the specified <paramref name="parentId"/>.
    /// </summary>
    protected virtual Expression<Func<TResultDto, bool>>? GetParentIdExpression(string parentId)
        => default;

    /// <summary>
    /// </summary>
    /// <param name="query"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    protected virtual ValueTask<IReadOnlyCollection<Sorting<TResultDto>>?> GetSortExpressionsAsync(
        TQuery query,
        CancellationToken cancellationToken)
        => new ValueTask<IReadOnlyCollection<Sorting<TResultDto>>?>(
            GetSortExpressions(query.SortByFields, query.SortDirection));

    /// <summary>
    /// Defines an expression used for sorting a list of <typeparamref name="TResultDto"/> objects
    /// by the specified <paramref name="sortByFields"/> field name.
    /// </summary>
    protected virtual IReadOnlyCollection<Sorting<TResultDto>>? GetSortExpressions(
        IReadOnlyCollection<string> sortByFields,
        ListSortDirection defaultDirection)
        => sortByFields.Count <= 0
            ? default
            : SortExpressionBuilder<TResultDto>.Build(sortByFields, defaultDirection);

    private Expression<Func<TResultDto, bool>>? GetFilterExpression(
        IReadOnlyDictionary<string, IReadOnlyCollection<string>> filterByFields,
        string? searchText,
        string? parentId)
    {
        var filterExpression = filterByFields.Count == 0
            ? default
            : GetFilterExpression(filterByFields);

        var searchExpression = searchText.IsNullOrEmpty()
            ? default
            : GetSearchExpression(searchText);

        var parentIdExpression = parentId.IsNullOrWhiteSpace()
            ? default
            : GetParentIdExpression(parentId);

        var expression = CombineExpressions(parentIdExpression, filterExpression);

        expression = CombineExpressions(expression, searchExpression);

        return expression;
    }

    private Expression<Func<TResultDto, bool>>? CombineExpressions(
        Expression<Func<TResultDto, bool>>? first,
        Expression<Func<TResultDto, bool>>? second)
    {
        return first is null
            ? second
            : second is null
                ? first
                : first.And(second);
    }
}