首頁  >  文章  >  後端開發  >  使用 Linq、Criteria API 和 Query Over 擴充 NHibernate 的 Ardalis.Specification

使用 Linq、Criteria API 和 Query Over 擴充 NHibernate 的 Ardalis.Specification

DDD
DDD原創
2024-09-18 11:52:24298瀏覽

Extending Ardalis.Specification for NHibernate with Linq, Criteria API, and Query Over

Ardalis.Specification 是一個功能強大的函式庫,支援查詢資料庫的規範模式,主要為Entity Framework Core 設計,但在這裡我將示範如何擴充Ardalis.Specification 來使用NHibernate也是一種ORM。

這篇部落格文章假設您對 Ardalis.Specification 有一定的經驗,並且希望在使用 NHibernate 的專案中使用它。如果您還不熟悉 Ardalis.Specification,請參閱文件以了解更多資訊。

首先,NHibernate 中有三種不同的內建方法來執行查詢

  • Linq 查詢(使用 IQueryable)
  • 標準 API
  • 查詢結束

我將介紹如何擴充 Ardalis.Specification 來處理所有 3 種方式,但由於 Linq to Query 也可以像 Entity Framework Core 一樣與 IQueryable 配合使用,因此我將首先介紹該選項。

Linq 查詢

在建立連線關係時,Entity Framework Core 和 NHIbernate 之間有細微差別。在 Entity Framework Core 中,我們在 IQueryable 上有擴充方法: Include 和 ThenInclude (這些也是 Ardalis.Specification 中使用的方法名稱)。

Fetch、FetchMany、ThenFetch 和 ThenFetchMany 是 IQueryable 上執行連線的 NHibernate 特定方法。 IEvaluator 為我們提供了使用 NHibernate 時呼叫正確擴充方法所需的可擴展性。

新增 IEvaluator 的實現,如下所示:

public class FetchEvaluator : IEvaluator
{
   private static readonly MethodInfo FetchMethodInfo = typeof(EagerFetchingExtensionMethods)
        .GetTypeInfo().GetDeclaredMethods(nameof(EagerFetchingExtensionMethods.Fetch))
        .Single();

   private static readonly MethodInfo FetchManyMethodInfo = typeof(EagerFetchingExtensionMethods)
       .GetTypeInfo().GetDeclaredMethods(nameof(EagerFetchingExtensionMethods.FetchMany))
       .Single();

   private static readonly MethodInfo ThenFetchMethodInfo
       = typeof(EagerFetchingExtensionMethods)
           .GetTypeInfo().GetDeclaredMethods(nameof(EagerFetchingExtensionMethods.ThenFetch))
           .Single();

   private static readonly MethodInfo ThenFetchManyMethodInfo
       = typeof(EagerFetchingExtensionMethods)
           .GetTypeInfo().GetDeclaredMethods(nameof(EagerFetchingExtensionMethods.ThenFetchMany))
           .Single();

    public static FetchEvaluator Instance { get; } = new FetchEvaluator();

    public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification) where T : class
    {
        foreach (var includeInfo in specification.IncludeExpressions)
        {
            query = includeInfo.Type switch
            {
                IncludeTypeEnum.Include => BuildInclude<T>(query, includeInfo),
                IncludeTypeEnum.ThenInclude => BuildThenInclude<T>(query, includeInfo),
                _ => query
            };
        }

        return query;
    }

    public bool IsCriteriaEvaluator { get; } = false;

    private IQueryable<T> BuildInclude<T>(IQueryable query, IncludeExpressionInfo includeInfo)
    {
        _ = includeInfo ?? throw new ArgumentNullException(nameof(includeInfo));

        var methodInfo = (IsGenericEnumerable(includeInfo.PropertyType, out var propertyType)
            ? FetchManyMethodInfo 
            : FetchMethodInfo);

       var method = methodInfo.MakeGenericMethod(includeInfo.EntityType, propertyType);

       var result = method.Invoke(null, new object[] { query, includeInfo.LambdaExpression });
        _ = result ?? throw new TargetException();

        return (IQueryable<T>)result;
    }

    private IQueryable<T> BuildThenInclude<T>(IQueryable query, IncludeExpressionInfo includeInfo)
    {
        _ = includeInfo ?? throw new ArgumentNullException(nameof(includeInfo));
        _ = includeInfo.PreviousPropertyType ?? throw new ArgumentNullException(nameof(includeInfo.PreviousPropertyType));

        var method = (IsGenericEnumerable(includeInfo.PreviousPropertyType, out var previousPropertyType)
            ? ThenFetchManyMethodInfo
            : ThenFetchMethodInfo);

        IsGenericEnumerable(includeInfo.PropertyType, out var propertyType);

        var result = method.MakeGenericMethod(includeInfo.EntityType, previousPropertyType, propertyType)
            .Invoke(null, new object[] { query, includeInfo.LambdaExpression });

        _ = result ?? throw new TargetException();

        return (IQueryable<T>)result;
    }

    private static bool IsGenericEnumerable(Type type, out Type propertyType)
    {
        if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
        {
            propertyType = type.GenericTypeArguments[0];

            return true;
        }

        propertyType = type;

        return false;
    }
}

接下來我們需要設定 ISpecificationEvaluator 以使用我們的 FetchEvaluator(和其他評估器)。我們新增一個實作 ISpecificationEvaluator ,如下所示,並在建構函式中配置評估器。 WhereEvaluator、OrderEvaluator 和 PaginationEvaluator 都在 Ardalis.Specification 中,並且在 NHibernate 中也能很好地工作。

public class LinqToQuerySpecificationEvaluator : ISpecificationEvaluator
{
    private List<IEvaluator> Evaluators { get; } = new List<IEvaluator>();

    public LinqToQuerySpecificationEvaluator()
    {
        Evaluators.AddRange(new IEvaluator[]
        {
            WhereEvaluator.Instance,
            OrderEvaluator.Instance,
            PaginationEvaluator.Instance,
            FetchEvaluator.Instance
        });
    }


    public IQueryable<TResult> GetQuery<T, TResult>(IQueryable<T> query, ISpecification<T, TResult> specification) where T : class
    {
        if (specification is null) throw new ArgumentNullException(nameof(specification));
        if (specification.Selector is null && specification.SelectorMany is null) throw new SelectorNotFoundException();
        if (specification.Selector is not null && specification.SelectorMany is not null) throw new ConcurrentSelectorsException();

        query = GetQuery(query, (ISpecification<T>)specification);

        return specification.Selector is not null
            ? query.Select(specification.Selector)
            : query.SelectMany(specification.SelectorMany!);
    }

    public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification, bool evaluateCriteriaOnly = false) where T : class
    {
        if (specification is null) throw new ArgumentNullException(nameof(specification));

        var evaluators = evaluateCriteriaOnly ? Evaluators.Where(x => x.IsCriteriaEvaluator) : Evaluators;

        foreach (var evaluator in evaluators)
           query = evaluator.GetQuery(query, specification);

        return query;
    }
}

現在我們可以在我們的儲存庫中建立對 LinqToQuerySpecificationEvaluator 的引用,可能如下所示:

public class Repository : IRepository
{
    private readonly ISession _session;
    private readonly ISpecificationEvaluator _specificationEvaluator;

    public Repository(ISession session)
    {
        _session = session;
        _specificationEvaluator = new LinqToQuerySpecificationEvaluator();
    } 

    ... other repository methods

    public IEnumerable<T> List<T>(ISpecification<T> specification) where T : class
    {
        return _specificationEvaluator.GetQuery(_session.Query<T>().AsQueryable(), specification).ToList();
    }

    public IEnumerable<TResult> List<T, TResult>(ISpecification<T, TResult> specification) where T : class
    {    
        return _specificationEvaluator.GetQuery(_session.Query<T>().AsQueryable(), specification).ToList();
    }

    public void Dispose()
    {
        _session.Dispose();
    }
}

就是這樣。現在,我們可以在規格中使用 Linq to Query,就像我們通常使用 Ardalis 一樣。規範:

public class TrackByName : Specification<Core.Entitites.Track>
{
    public TrackByName(string trackName)
    {
        Query.Where(x => x.Name == trackName);
    }
}

現在我們已經介紹了基於 Linq 的查詢,讓我們繼續處理 Criteria API 和 Query Over,這需要不同的方法。

在 NHibernate 中混合 Linq、Criteria 和 Query Over

由於 Criteria API 和 Query Over 有自己的實作來產生 SQL,且不使用 IQueryable,因此它們與 IEvaluator 介面不相容。我的解決方案是在這種情況下避免對這些方法使用 IEvaluator 接口,而是專注於規範模式的好處。但我也希望能夠混搭
我的解決方案中包含 Linq to Query、Criteria 和 Query Over(如果您只需要其中一種實現,您可以根據您的最佳需求進行挑選)。

為了能夠做到這一點,我新增了四個繼承Specification或Specification的新類別

注意: 定義這些類別的組件需要對 NHibernate 的引用,因為我們為 Criteria 和 QueryOver 定義操作,這可以在 NHibernate
中找到

public class CriteriaSpecification<T> : Specification<T>
{
    private Action<ICriteria>? _action;
    public Action<ICriteria> GetCriteria() => _action ?? throw new NotSupportedException("The criteria has not been specified. Please use UseCriteria() to define the criteria.");
    protected void UseCriteria(Action<ICriteria> action) => _action = action;
}

public class CriteriaSpecification<T, TResult> : Specification<T, TResult>
{
    private Action<ICriteria>? _action;
    public Action<ICriteria> GetCriteria() => _action ?? throw new NotSupportedException("The criteria has not been specified. Please use UseCriteria() to define the criteria.");
    protected void UseCriteria(Action<ICriteria> action) => _action = action;
}

public class QueryOverSpecification<T> : Specification<T>
{
    private Action<IQueryOver<T, T>>? _action;
    public Action<IQueryOver<T, T>> GetQueryOver() => _action ?? throw new NotSupportedException("The Query over has not been specified. Please use the UseQueryOver() to define the query over.");
    protected void UseQueryOver(Action<IQueryOver<T, T>> action) => _action = action;
}

public class QueryOverSpecification<T, TResult> : Specification<T, TResult>
{
    private Func<IQueryOver<T, T>, IQueryOver<T, T>>? _action;
    public Func<IQueryOver<T, T>, IQueryOver<T, T>> GetQueryOver() => _action ??  throw new NotSupportedException("The Query over has not been specified. Please use the UseQueryOver() to define the query over.");
    protected void UseQueryOver(Func<IQueryOver<T, T>, IQueryOver<T, T>> action) => _action = action;
}

然後我們可以在儲存庫中使用模式匹配來更改使用 NHibernate 進行查詢的方式

public IEnumerable<T> List<T>(ISpecification<T> specification) where T : class
{
    return specification switch
    {
        CriteriaSpecification<T> criteriaSpecification => 
            _session.CreateCriteria<T>()
                .Apply(query => criteriaSpecification.GetCriteria().Invoke(query))
                .List<T>(),

        QueryOverSpecification<T> queryOverSpecification => 
            _session.QueryOver<T>()
                .Apply(queryOver => queryOverSpecification.GetQueryOver().Invoke(queryOver))
                .List<T>(),

        _ => _specificationEvaluator.GetQuery(_session.Query<T>().AsQueryable(), specification).ToList()
    };
}

public IEnumerable<TResult> List<T, TResult>(ISpecification<T, TResult> specification) where T : class
{

    return specification switch
    {
        CriteriaSpecification<T, TResult> criteriaSpecification => 
            _session.CreateCriteria<T>()
                .Apply(query => criteriaSpecification.GetCriteria().Invoke(query))
                .List<TResult>(),

        QueryOverSpecification<T, TResult> queryOverSpecification =>
            _session.QueryOver<T>()
                .Apply(queryOver => queryOverSpecification.GetQueryOver().Invoke(queryOver))
                .List<TResult>(),

        _ => _specificationEvaluator.GetQuery(_session.Query<T>().AsQueryable(), specification).ToList()
    };
}

上面的Apply()方法是一個擴充方法,它將查詢簡化為一行:

public static class QueryExtensions
{
    public static T Apply<T>(this T obj, Action<T> action)
    {
        action(obj);
        return obj;
    }

    public static TResult Apply<T, TResult>(this T obj, Func<T, TResult> func)
    {
        return func(obj);
    }
}

標準規格範例

注意: 定義這些類別的組件需要對 NHibernate 的引用,因為我們定義 Criteria 的操作,可以在 NHibernate
中找到

public class TrackByNameCriteria : CriteriaSpecification<Track>
{
    public TrackByNameCriteria(string trackName)
    {
        this.UseCriteria(criteria => criteria.Add(Restrictions.Eq(nameof(Track.Name), trackName)));
    }
}

查詢超規格範例

注意: 定義這些類別的組件需要對 NHibernate 的引用,因為我們定義 QueryOver 的操作,可以在 NHibernate
中找到

public class TrackByNameQueryOver : QueryOverSpecification<Track>
{
    public TrackByNameQueryOver(string trackName)
    {
        this.UseQueryOver(queryOver => queryOver.Where(x => x.Name == trackName));
    }
}

透過擴充 NHibernate 的 Ardalis.Specification,我們解鎖了在單一儲存庫模式中使用 Linq to Query、Criteria API 和 Query Over 的能力。這種方法為 NHibernate 用戶提供了高度適應性和強大的解決方案

以上是使用 Linq、Criteria API 和 Query Over 擴充 NHibernate 的 Ardalis.Specification的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn