Heim  >  Artikel  >  Backend-Entwicklung  >  Erweitern von Ardalis.Specification für NHibernate mit Linq, Criteria API und Query Over

Erweitern von Ardalis.Specification für NHibernate mit Linq, Criteria API und Query Over

DDD
DDDOriginal
2024-09-18 11:52:24302Durchsuche

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

Ardalis.Specification ist eine leistungsstarke Bibliothek, die das Spezifikationsmuster zum Abfragen von Datenbanken ermöglicht. Sie wurde hauptsächlich für Entity Framework Core entwickelt. Hier zeige ich jedoch, wie Sie Ardalis.Specification für die Verwendung erweitern können NHibernate auch als ORM.

In diesem Blogbeitrag wird davon ausgegangen, dass Sie über Erfahrung mit Ardalis.Specification verfügen und es in einem Projekt mit NHibernate verwenden möchten. Wenn Sie mit Ardalis.Specification noch nicht vertraut sind, lesen Sie die Dokumentation, um mehr zu erfahren.

Erstens gibt es in NHibernate drei verschiedene integrierte Möglichkeiten, Abfragen durchzuführen

  • Linq zur Abfrage (mit IQueryable)
  • Kriterien-API
  • Abfrage beendet

Ich werde durchgehen, wie Sie Ardalis.Specification erweitern können, um alle drei Möglichkeiten zu verarbeiten, aber da Linq to Query auch mit IQueryable wie Entity Framework Core funktioniert, werde ich zuerst diese Option durchgehen.

Linq zur Abfrage

Es gibt eine kleine Nuance zwischen Entity Framework Core und NHIbernate, wenn es um die Erstellung von Join-Beziehungen geht. In Entity Framework Core haben wir Erweiterungsmethoden für IQueryable: Include und ThenInclude (dies sind auch die Methodennamen, die in Ardalis.Specification verwendet werden).

Fetch, FetchMany, ThenFetch und ThenFetchMany sind NHibernate-spezifische Methoden auf IQueryable, die Verknüpfungen durchführen. Der IEvaluator bietet uns die Erweiterbarkeit, die wir benötigen, um die richtige Erweiterungsmethode aufzurufen, wenn wir mit NHibernate arbeiten.

Fügen Sie eine Implementierung von IEvaluator wie folgt hinzu:

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;
    }
}

Als nächstes müssen wir ISpecificationEvaluator für die Verwendung unseres FetchEvaluator (und anderer Evaluatoren) konfigurieren. Wir fügen wie folgt eine Implementierung ISpecificationEvaluator hinzu, wobei die Evaluatoren im Konstruktor konfiguriert sind. WhereEvaluator, OrderEvaluator und PaginationEvaluator sind alle in der Ardalis.Specification enthalten und funktionieren auch gut mit 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;
    }
}

Jetzt können wir in unserem Repository einen Verweis auf LinqToQuerySpecificationEvaluator erstellen, der etwa so aussehen könnte:

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

Das ist es. Wir können jetzt Linq to Query in unseren Spezifikationen verwenden, genau wie wir es normalerweise mit Ardalis tun.Specification:

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

Nachdem wir uns nun mit Linq-basierten Abfragen befasst haben, gehen wir zum Umgang mit Criteria API und Query Over über, die einen anderen Ansatz erfordern.

Mischen von Linq, Criteria und Query Over in NHibernate

Da Criteria API und Query Over über eine eigene Implementierung zum Generieren von SQL verfügen und nicht IQueryable verwenden, sind sie nicht mit der IEvaluator-Schnittstelle kompatibel. Meine Lösung besteht darin, in diesem Fall die Verwendung der IEvaluator-Schnittstelle für diese Methoden zu vermeiden und mich stattdessen auf die Vorteile des Spezifikationsmusters zu konzentrieren. Aber ich möchte auch mischen können
Linq to Query, Criteria und Query Over in meiner Lösung (wenn Sie nur eine dieser Implementierungen benötigen, können Sie sich für Ihre besten Anforderungen entscheiden).

Dazu füge ich vier neue Klassen hinzu, die Specification oder Specification erben

HINWEIS: Die Assembly, in der Sie diese Klassen definieren, benötigt einen Verweis auf NHibernate, da wir Aktionen für Kriterien und ein QueryOver definieren, das in NHibernate zu finden ist

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;
}

Dann können wir den Mustervergleich in unserem Repository verwenden, um die Art und Weise zu ändern, wie wir Abfragen mit NHibernate durchführen

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

Die obige Apply()-Methode ist eine Erweiterungsmethode, die die Abfrage auf eine einzelne Zeile vereinfacht:

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);
    }
}

Beispiel für eine Kriterienspezifikation

HINWEIS: Die Assembly, in der Sie diese Klassen definieren, benötigt einen Verweis auf NHibernate, da wir Aktionen für Kriterien definieren, die in NHibernate zu finden sind

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

Beispiel für eine Abfrage nach einer Spezifikation

HINWEIS: Die Assembly, in der Sie diese Klassen definieren, benötigt einen Verweis auf NHibernate, da wir Aktionen für ein QueryOver definieren, das in NHibernate zu finden ist

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

Durch die Erweiterung von Ardalis.Specification für NHibernate erschließen wir die Möglichkeit, Linq to Query, Criteria API und Query Over zu verwenden – alles innerhalb eines einzigen Repository-Musters. Dieser Ansatz bietet eine äußerst anpassungsfähige und leistungsstarke Lösung für NHibernate-Benutzer

Das obige ist der detaillierte Inhalt vonErweitern von Ardalis.Specification für NHibernate mit Linq, Criteria API und Query Over. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn