”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 使用 Linq、Criteria API 和 Query Over 扩展 NHibernate 的 Ardalis.Specification

使用 Linq、Criteria API 和 Query Over 扩展 NHibernate 的 Ardalis.Specification

发布于2024-11-07
浏览:674

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]删除
最新教程 更多>
  • Java中Lambda表达式为何需要“final”或“有效final”变量?
    Java中Lambda表达式为何需要“final”或“有效final”变量?
    Lambda Expressions Require "Final" or "Effectively Final" VariablesThe error message "Variable used in lambda expression shou...
    编程 发布于2025-07-01
  • 如何使用不同数量列的联合数据库表?
    如何使用不同数量列的联合数据库表?
    合并列数不同的表 当尝试合并列数不同的数据库表时,可能会遇到挑战。一种直接的方法是在列数较少的表中,为缺失的列追加空值。 例如,考虑两个表,表 A 和表 B,其中表 A 的列数多于表 B。为了合并这些表,同时处理表 B 中缺失的列,请按照以下步骤操作: 确定表 B 中缺失的列,并将它们添加到表的末...
    编程 发布于2025-07-01
  • `console.log`显示修改后对象值异常的原因
    `console.log`显示修改后对象值异常的原因
    foo = [{id:1},{id:2},{id:3},{id:4},{id:id:5},],]; console.log('foo1',foo,foo.length); foo.splice(2,1); console.log('foo2', foo, foo....
    编程 发布于2025-07-01
  • 如何使用替换指令在GO MOD中解析模块路径差异?
    如何使用替换指令在GO MOD中解析模块路径差异?
    在使用GO MOD时,在GO MOD 中克服模块路径差异时,可能会遇到冲突,其中3个Party Package将另一个PAXPANCE带有导入式套件之间的另一个软件包,并在导入式套件之间导入另一个软件包。如回声消息所证明的那样: go.etcd.io/bbolt [&&&&&&&&&&&&&&&&...
    编程 发布于2025-07-01
  • 如何在Java字符串中有效替换多个子字符串?
    如何在Java字符串中有效替换多个子字符串?
    在java 中有效地替换多个substring,需要在需要替换一个字符串中的多个substring的情况下,很容易求助于重复应用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    编程 发布于2025-07-01
  • 如何解决AppEngine中“无法猜测文件类型,使用application/octet-stream...”错误?
    如何解决AppEngine中“无法猜测文件类型,使用application/octet-stream...”错误?
    appEngine静态文件mime type override ,静态文件处理程序有时可以覆盖正确的mime类型,在错误消息中导致错误消息:“无法猜测mimeType for for file for file for [File]。 application/application/octet...
    编程 发布于2025-07-01
  • 查找当前执行JavaScript的脚本元素方法
    查找当前执行JavaScript的脚本元素方法
    如何引用当前执行脚本的脚本元素在某些方案中理解问题在某些方案中,开发人员可能需要将其他脚本动态加载其他脚本。但是,如果Head Element尚未完全渲染,则使用document.getElementsbytagname('head')[0] .appendChild(v)的常规方...
    编程 发布于2025-07-01
  • 如何处理PHP文件系统功能中的UTF-8文件名?
    如何处理PHP文件系统功能中的UTF-8文件名?
    在PHP的Filesystem functions中处理UTF-8 FileNames 在使用PHP的MKDIR函数中含有UTF-8字符的文件很多flusf-8字符时,您可能会在Windows Explorer中遇到comploreer grounder grounder grounder gro...
    编程 发布于2025-07-01
  • 如何在GO编译器中自定义编译优化?
    如何在GO编译器中自定义编译优化?
    在GO编译器中自定义编译优化 GO中的默认编译过程遵循特定的优化策略。 However, users may need to adjust these optimizations for specific requirements.Optimization Control in Go Compi...
    编程 发布于2025-07-01
  • Async Void vs. Async Task在ASP.NET中:为什么Async Void方法有时会抛出异常?
    Async Void vs. Async Task在ASP.NET中:为什么Async Void方法有时会抛出异常?
    在ASP.NET async void void async void void void void void的设计无需返回asynchroncon而无需返回任务对象。他们在执行过程中增加未偿还操作的计数,并在完成后减少。在某些情况下,这种行为可能是有益的,例如未期望或明确预期操作结果的火灾和...
    编程 发布于2025-07-01
  • 如何在Chrome中居中选择框文本?
    如何在Chrome中居中选择框文本?
    选择框的文本对齐:局部chrome-inly-ly-ly-lyly solument 您可能希望将文本中心集中在选择框中,以获取优化的原因或提高可访问性。但是,在CSS中的选择元素中手动添加一个文本 - 对属性可能无法正常工作。初始尝试 state)</option> < op...
    编程 发布于2025-07-01
  • 将图片浮动到底部右侧并环绕文字的技巧
    将图片浮动到底部右侧并环绕文字的技巧
    在Web设计中围绕在Web设计中,有时可以将图像浮动到页面右下角,从而使文本围绕它缠绕。这可以在有效地展示图像的同时创建一个吸引人的视觉效果。 css位置在右下角,使用css float and clear properties: img { 浮点:对; ...
    编程 发布于2025-07-01
  • 如何干净地删除匿名JavaScript事件处理程序?
    如何干净地删除匿名JavaScript事件处理程序?
    删除匿名事件侦听器将匿名事件侦听器添加到元素中会提供灵活性和简单性,但是当需要删除它们时,可以构成挑战,而无需替换元素本身就可以替换一个问题。 element? element.addeventlistener(event,function(){/在这里工作/},false); 要解决此问题,请考...
    编程 发布于2025-07-01
  • 如何从PHP中的数组中提取随机元素?
    如何从PHP中的数组中提取随机元素?
    从阵列中的随机选择,可以轻松从数组中获取随机项目。考虑以下数组:; 从此数组中检索一个随机项目,利用array_rand( array_rand()函数从数组返回一个随机键。通过将$项目数组索引使用此键,我们可以从数组中访问一个随机元素。这种方法为选择随机项目提供了一种直接且可靠的方法。
    编程 发布于2025-07-01

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3