」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 使用 Linq、Criteria API 和 Query Over 擴充 NHibernate 的 Ardalis.Specification

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

發佈於2024-11-07
瀏覽:704

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

Ardalis.Specification is a powerful library that enables the specification pattern for querying databases, primarily designed for Entity Framework Core, but here I'll demonstrate how you can extend Ardalis.Specification to use NHibernate as an ORM as well.

This blog post assumes you have some experience with Ardalis.Specification, and want to use it in a project using NHibernate. If you are not familiar with Ardalis.Specification yet, head over to the documentation to learn more.

First, in NHibernate there are three different built-in ways to perform queries

  • Linq to query (using IQueryable)
  • Criteria API
  • Query Over

I'll go through how you can extend Ardalis.Specification to handle all 3 ways, but since Linq to Query also works with IQueryable like Entity Framework Core, I'll go through that option first.

Linq to query

There is a small nuance between Entity Framework Core and NHIbernate when it comes to create join relationships. In Entity Framework Core we have extensions methods on IQueryable: Include and ThenInclude (these are also the method names used in Ardalis.Specification).

Fetch, FetchMany, ThenFetch and ThenFetchMany are NHibernate specific methods on IQueryable that do joins. The IEvaluator gives us the extensibility we need to invoke the correct extension method when we work with NHibernate.

Add an implementation of IEvaluator as follows:

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

        return query;
    }

    public bool IsCriteriaEvaluator { get; } = false;

    private IQueryable BuildInclude(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)result;
    }

    private IQueryable BuildThenInclude(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)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;
    }
}

Next we need to configure ISpecificationEvaluator to use our FetchEvaluator (and other evaluators). We add an implementation ISpecificationEvaluator as follows with the Evaluators configured in the constructor. WhereEvaluator, OrderEvaluator and PaginationEvaluator are all in the Ardalis.Specification and works well NHibernate as well.

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

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


    public IQueryable GetQuery(IQueryable query, ISpecification 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)specification);

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

    public IQueryable GetQuery(IQueryable query, ISpecification 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;
    }
}

Now we can create a reference to LinqToQuerySpecificationEvaluator in our repository that may look something like this:

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 List(ISpecification specification) where T : class
    {
        return _specificationEvaluator.GetQuery(_session.Query().AsQueryable(), specification).ToList();
    }

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

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

That's it. We can now use Linq to Query in our specifications just like we normally do with Ardalis.Specification:

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

Now that we've covered Linq-based queries, let's move on to handling Criteria API and Query Over, which require a different approach.

Mixing Linq, Criteria, and Query Over in NHibernate

Since Criteria API and Query Over has their own implementation to generate SQL, and doesn't use IQueryable, they are incompatible with the IEvaluator interface. My solution is to avoid using the IEvaluator interface for these methods in this case, but rather focus on the benefits of the specification pattern. But I also want to be able to mix
Linq to Query, Criteria and Query Over with in my solution (if you only need one of these implementations, you can cherry-pick for your best needs).

To be able to do that, I add four new classes that inherits Specification or Specification

NOTE: The assembly where you define these classes needs a reference to NHibernate as we define actions for Criteria and a QueryOver, that can be found in NHibernate

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

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

public class QueryOverSpecification : Specification
{
    private Action>? _action;
    public Action> 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> action) => _action = action;
}

public class QueryOverSpecification : Specification
{
    private Func, IQueryOver>? _action;
    public Func, IQueryOver> 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> action) => _action = action;
}

Then we can use pattern matching in our repository to change how we do queries with NHibernate

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

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

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

public IEnumerable List(ISpecification specification) where T : class
{

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

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

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

The Apply() method above are an extension method that simplifies the query to a single line:

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

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

Criteria specification example

NOTE: The assembly where you define these classes needs a reference to NHibernate as we define actions for Criteria, that can be found in NHibernate

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

Query over specification example

NOTE: The assembly where you define these classes needs a reference to NHibernate as we define actions for a QueryOver, that can be found in NHibernate

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

By extending Ardalis.Specification for NHibernate, we unlock the ability to use Linq to Query, Criteria API, and Query Over—all within a single repository pattern. This approach offers a highly adaptable and powerful solution for NHibernate users

版本聲明 本文轉載於:https://dev.to/greenfieldcoder/extending-ardalisspecification-for-nhibernate-with-linq-criteria-api-and-query-over-24pn如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 了解 MongoDB 的distinct() 操作:實用指南
    了解 MongoDB 的distinct() 操作:實用指南
    MongoDB 的distinct() 操作是一個強大的工具,用於從集合中的指定欄位檢索唯一值。本指南將幫助您了解distinct() 的用途、使用它的原因和時間,以及如何在 MongoDB 查詢中有效地實現它。 什麼是distinct()? distinct() 方法傳回集合或集...
    程式設計 發佈於2024-11-07
  • 為什麼 JavaScript 中的「0」在比較中為 False,而在「if」語句中為 True?
    為什麼 JavaScript 中的「0」在比較中為 False,而在「if」語句中為 True?
    揭開JavaScript 的悖論:為什麼「0」在比較中為假,但在If 語句中為假在JavaScript中,原語" 的行為0」給開發者帶來了困惑。雖然諸如“==”之類的邏輯運算符將“0”等同於假,但“0”在“if”條件下表現為真。 比較悖論代碼下面演示了比較悖論:"0" ...
    程式設計 發佈於2024-11-07
  • GitHub Copilot 有其怪癖
    GitHub Copilot 有其怪癖
    過去 4 個月我一直在將 GitHub Copilot 與我們的生產代碼庫一起使用,以下是我的一些想法: 好的: 解釋複雜程式碼:它非常適合分解棘手的程式碼片段或商業邏輯並正確解釋它們。 單元測試:非常擅長編寫單元測試並快速產生多個基於場景的測試案例。 程式碼片段:它可以輕鬆地為通用用例產生有用...
    程式設計 發佈於2024-11-07
  • 靜態類別或實例化類別:什麼時候應該選擇哪一個?
    靜態類別或實例化類別:什麼時候應該選擇哪一個?
    在靜態類別和實例化類別之間做出選擇:概述在PHP 中設計軟體應用程式時,開發人員經常面臨在使用靜態類別或實例化物件。這個決定可能會對程式的結構、效能和可測試性產生重大影響。 何時使用靜態類別靜態類別適用於物件不具備靜態類別的場景獨特的數據,只需要存取共享功能。例如,用於將 BB 程式碼轉換為 HTM...
    程式設計 發佈於2024-11-07
  • ⚠️ 在 JavaScript 中使用 `var` 的隱藏危險:為什麼是時候繼續前進了
    ⚠️ 在 JavaScript 中使用 `var` 的隱藏危險:為什麼是時候繼續前進了
    关键字 var 多年来一直是 JavaScript 中声明变量的默认方式。但是,它有一些怪癖和陷阱,可能会导致代码出现意外行为。现代替代方案(如 let 和 const)解决了许多此类问题,使它们成为大多数情况下声明变量的首选。 1️⃣ 提升:var 在不知不觉中声明变量! ?解释:...
    程式設計 發佈於2024-11-07
  • PDO::MYSQL_ATTR_INIT_COMMAND 需要「SET CHARACTER SET utf8」嗎?
    PDO::MYSQL_ATTR_INIT_COMMAND 需要「SET CHARACTER SET utf8」嗎?
    在有「PDO::MYSQL_ATTR_INIT_COMMAND」的 PDO 中「SET CHARACTER SET utf8」是否必要? 在PHP 和MySQL 中,「SET NAMES」 utf8」和「SET CHARACTER SET utf8」通常在使用UTF-8 編碼時使用。但是,當使用PD...
    程式設計 發佈於2024-11-07
  • 為什麼使用Password_Hash函數時哈希值會改變?
    為什麼使用Password_Hash函數時哈希值會改變?
    了解Password_Hash函數中不同的雜湊值在開發安全認證系統時,開發人員經常會遇到使用password_hash取得不同密碼哈希值的困惑功能。為了闡明此行為並確保正確的密碼驗證,讓我們分析此函數背後的機制。 密碼加鹽:有意的功能password_hash 函數有意產生唯一的鹽它對每個密碼進行哈...
    程式設計 發佈於2024-11-07
  • 為什麼與谷歌競爭並不瘋狂
    為什麼與谷歌競爭並不瘋狂
    大家好,我是 Antonio,Litlyx 的首席執行官,我們的對手是一些巨頭! Microsoft Clarity、Google Analytics、MixPanel...它們是分析領域的重要參與者。當人們聽到一家新創公司正在與如此知名的公司合作時,他們常常會感到驚訝。但讓我們看看為什麼與Goo...
    程式設計 發佈於2024-11-07
  • 如何在 Java Streams 中有效地將物件清單轉換為可選物件?
    如何在 Java Streams 中有效地將物件清單轉換為可選物件?
    使用Java 8 的可選和Stream::flatMap 變得簡潔使用Java 8 流時,將List 轉換為可選 並有效地提取第一個Other 值可能是一個挑戰。雖然 flatMap 通常需要返回流,但可選的 Stream() 的缺失使問題變得複雜。 Java 16 解決方案Java 16 引入了S...
    程式設計 發佈於2024-11-07
  • 避免前端開發失敗:編寫乾淨程式碼的行之有效的實踐
    避免前端開發失敗:編寫乾淨程式碼的行之有效的實踐
    介紹 您是否曾因看似無法理清或擴展的凌亂代碼而感到不知所措?如果你有,那麼你並不孤單。許多開發人員面臨著維護乾淨的程式碼庫的挑戰,這對於專案的長期成功和可擴展性至關重要。讓我們探索一些有效的策略來保持程式碼可管理性和專案順利運行。 了解基礎知識:什麼是乾淨程式碼?...
    程式設計 發佈於2024-11-07
  • 如何存取Python字典中的第一個和第N個鍵值對?
    如何存取Python字典中的第一個和第N個鍵值對?
    取得 Python 字典中的第一個條目使用數位索引(如顏色[0])對字典進行索引可能會導致 KeyError 異常。從 Python 3.7 開始,字典保留插入順序,使我們能夠像有序集合一樣使用它們。 取得第一個鍵和值要取得字典中的第一個鍵和值,我們可以使用以下方法:列表轉換:使用list(dict...
    程式設計 發佈於2024-11-07
  • 使用 cProfile 和 PyPy 模組優化 Python 程式碼:完整指南
    使用 cProfile 和 PyPy 模組優化 Python 程式碼:完整指南
    介绍 作为 Python 开发人员,我们通常先关注让代码正常运行,然后再担心优化它。然而,在处理大规模应用程序或性能关键型代码时,优化变得至关重要。在这篇文章中,我们将介绍两个可用于优化 Python 代码的强大工具:cProfile 模块和 PyPy 解释器。 在这篇文章的结尾,...
    程式設計 發佈於2024-11-07
  • 上週我學到了什麼(
    上週我學到了什麼(
    原生 JavaScript 中的反應性 – 使用代理模式在應用程式狀態變更時觸發事件。 (前端大師課程 - “你可能不需要框架”) throw new Error("Error!") 不能在三元中使用(至少不能用作'else' 部分。三元運算子的最後一部分必...
    程式設計 發佈於2024-11-07
  • 如何在 Linux 系統上將 Java 應用程式作為服務運行?
    如何在 Linux 系統上將 Java 應用程式作為服務運行?
    Linux 系統服務導航:將Java 應用程式作為服務運行在Linux 系統管理領域,將應用程式作為服務進行管理對於確保其可靠且受控的執行至關重要。本文深入探討了將 Java 伺服器應用程式配置為在 Linux 作業系統上作為服務運行的過程,為使用者提出的問題提供了全面的解決方案。 主要目標是創建一...
    程式設計 發佈於2024-11-07
  • 如何在不安裝 Angular CLI 的情況下建立 Angular 專案的特定版本
    如何在不安裝 Angular CLI 的情況下建立 Angular 專案的特定版本
    您是否使用 Angular 並需要使用不同的 Angular 版本設定項目?這是為特定版本建立 Angular 專案的簡單指南,無論是否使用 Angular CLI! 為什麼要使用特定的 Angular 版本? 在處理多個 Angular 專案時,有時您需要鎖定特定版本。也許您的專...
    程式設計 發佈於2024-11-07

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3