diff --git a/src/Ardalis.Specification.EntityFrameworkCore/CachedReadConcurrentDictionary.cs b/src/Ardalis.Specification.EntityFrameworkCore/CachedReadConcurrentDictionary.cs
deleted file mode 100644
index f493d7cd..00000000
--- a/src/Ardalis.Specification.EntityFrameworkCore/CachedReadConcurrentDictionary.cs
+++ /dev/null
@@ -1,223 +0,0 @@
-using System.Collections;
-using System.Collections.Concurrent;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-
-namespace Ardalis.Specification.EntityFrameworkCore;
-
-///
-/// A thread-safe dictionary for read-heavy workloads.
-///
-/// The key type.
-/// The value type.
-internal class CachedReadConcurrentDictionary : IDictionary where TKey : notnull
-{
- ///
- /// The number of cache misses which are tolerated before the cache is regenerated.
- ///
- private const int _cacheMissesBeforeCaching = 10;
- private readonly ConcurrentDictionary _dictionary;
- private readonly IEqualityComparer? _comparer;
-
- ///
- /// Approximate number of reads which did not hit the cache since it was last invalidated.
- /// This is used as a heuristic that the dictionary is not being modified frequently with respect to the read volume.
- ///
- private int _cacheMissReads;
-
- ///
- /// Cached version of .
- ///
- private Dictionary? _readCache;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public CachedReadConcurrentDictionary()
- {
- _dictionary = new ConcurrentDictionary();
- }
-
- ///
- /// Initializes a new instance of the class
- /// that contains elements copied from the specified collection.
- ///
- ///
- /// The whose elements are copied to the new instance.
- ///
- public CachedReadConcurrentDictionary(IEnumerable> collection)
- {
- _dictionary = new ConcurrentDictionary(collection);
- }
-
- ///
- /// Initializes a new instance of the class
- /// that contains elements copied from the specified collection and uses the specified
- /// .
- ///
- ///
- /// The implementation to use when comparing keys.
- ///
- public CachedReadConcurrentDictionary(IEqualityComparer comparer)
- {
- _comparer = comparer;
- _dictionary = new ConcurrentDictionary(comparer);
- }
-
- ///
- /// Initializes a new instance of the
- /// class that contains elements copied from the specified collection and uses the specified
- /// .
- ///
- ///
- /// The whose elements are copied to the new instance.
- ///
- ///
- /// The implementation to use when comparing keys.
- ///
- public CachedReadConcurrentDictionary(IEnumerable> collection, IEqualityComparer comparer)
- {
- _comparer = comparer;
- _dictionary = new ConcurrentDictionary(collection, comparer);
- }
-
- ///
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
- ///
- public IEnumerator> GetEnumerator() => GetReadDictionary().GetEnumerator();
-
- ///
- public void Add(KeyValuePair item)
- {
- ((IDictionary)_dictionary).Add(item);
- InvalidateCache();
- }
-
- ///
- public void Clear()
- {
- _dictionary.Clear();
- InvalidateCache();
- }
-
- ///
- public bool Contains(KeyValuePair item) => GetReadDictionary().Contains(item);
-
- ///
- public void CopyTo(KeyValuePair[] array, int arrayIndex)
- {
- GetReadDictionary().CopyTo(array, arrayIndex);
- }
-
- ///
- public bool Remove(KeyValuePair item)
- {
- var result = ((IDictionary)_dictionary).Remove(item);
- if (result) InvalidateCache();
- return result;
- }
-
- ///
- public int Count => GetReadDictionary().Count;
-
- ///
- public bool IsReadOnly => false;
-
- ///
- public void Add(TKey key, TValue value)
- {
- ((IDictionary)_dictionary).Add(key, value);
- InvalidateCache();
- }
-
- ///
- /// Adds a key/value pair to the if the key does not exist.
- ///
- /// The key of the element to add.
- /// The function used to generate a value for the key
- /// The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value if the key was not in the dictionary.
- public TValue GetOrAdd(TKey key, Func valueFactory)
- {
- if (GetReadDictionary().TryGetValue(key, out var value))
- {
- return value;
- }
-
- value = _dictionary.GetOrAdd(key, valueFactory);
- InvalidateCache();
-
- return value;
- }
-
- ///
- /// Attempts to add the specified key and value.
- ///
- /// The key of the element to add.
- /// The value of the element to add. The value can be a null reference (Nothing
- /// in Visual Basic) for reference types.
- /// true if the key/value pair was added successfully; otherwise, false.
- public bool TryAdd(TKey key, TValue value)
- {
- if (_dictionary.TryAdd(key, value))
- {
- InvalidateCache();
- return true;
- }
-
- return false;
- }
-
- ///
- public bool ContainsKey(TKey key) => GetReadDictionary().ContainsKey(key);
-
- ///
- public bool Remove(TKey key)
- {
- var result = ((IDictionary)_dictionary).Remove(key);
- if (result) InvalidateCache();
- return result;
- }
-
- ///
- public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => GetReadDictionary().TryGetValue(key, out value);
-
- ///
- public TValue this[TKey key]
- {
- get => GetReadDictionary()[key];
- set
- {
- _dictionary[key] = value;
- InvalidateCache();
- }
- }
-
- ///
- public ICollection Keys => GetReadDictionary().Keys;
-
- ///
- public ICollection Values => GetReadDictionary().Values;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private IDictionary GetReadDictionary() => _readCache ?? GetWithoutCache();
-
- private IDictionary GetWithoutCache()
- {
- // If the dictionary was recently modified or the cache is being recomputed, return the dictionary directly.
- if (Interlocked.Increment(ref _cacheMissReads) < _cacheMissesBeforeCaching)
- {
- return _dictionary;
- }
-
- // Recompute the cache if too many cache misses have occurred.
- _cacheMissReads = 0;
- return _readCache = new Dictionary(_dictionary, _comparer);
- }
-
- private void InvalidateCache()
- {
- _cacheMissReads = 0;
- _readCache = null;
- }
-}
diff --git a/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs
index e9accc4f..ce8b0ba4 100644
--- a/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs
+++ b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs
@@ -1,4 +1,7 @@
using Microsoft.EntityFrameworkCore.Query;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Diagnostics;
using System.Reflection;
namespace Ardalis.Specification.EntityFrameworkCore;
@@ -7,14 +10,14 @@ public class IncludeEvaluator : IEvaluator
{
private static readonly MethodInfo _includeMethodInfo = typeof(EntityFrameworkQueryableExtensions)
.GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.Include))
- .Single(mi => mi.GetGenericArguments().Length == 2
+ .Single(mi => mi.IsPublic && mi.GetGenericArguments().Length == 2
&& mi.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
&& mi.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>));
private static readonly MethodInfo _thenIncludeAfterReferenceMethodInfo
= typeof(EntityFrameworkQueryableExtensions)
.GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude))
- .Single(mi => mi.GetGenericArguments().Length == 3
+ .Single(mi => mi.IsPublic && mi.GetGenericArguments().Length == 3
&& mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter
&& mi.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IIncludableQueryable<,>)
&& mi.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>));
@@ -22,165 +25,92 @@ private static readonly MethodInfo _thenIncludeAfterReferenceMethodInfo
private static readonly MethodInfo _thenIncludeAfterEnumerableMethodInfo
= typeof(EntityFrameworkQueryableExtensions)
.GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude))
- .Where(mi => mi.GetGenericArguments().Length == 3)
- .Single(
- mi =>
- {
- var typeInfo = mi.GetParameters()[0].ParameterType.GenericTypeArguments[1];
-
- return typeInfo.IsGenericType
- && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>)
- && mi.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IIncludableQueryable<,>)
- && mi.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>);
- });
-
- private static readonly CachedReadConcurrentDictionary<(Type EntityType, Type PropertyType, Type? PreviousPropertyType), Lazy>> _delegatesCache = new();
-
- private readonly bool _cacheEnabled;
-
- private IncludeEvaluator(bool cacheEnabled)
- {
- _cacheEnabled = cacheEnabled;
- }
+ .Single(mi => mi.IsPublic && mi.GetGenericArguments().Length == 3
+ && !mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter
+ && mi.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IIncludableQueryable<,>)
+ && mi.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>));
- ///
- /// instance without any additional features.
- ///
- public static IncludeEvaluator Default { get; } = new IncludeEvaluator(false);
+ private readonly record struct CacheKey(Type EntityType, Type PropertyType, Type? PreviousReturnType);
+ private static readonly ConcurrentDictionary> _cache = new();
- ///
- /// instance with caching to provide better performance.
- ///
- public static IncludeEvaluator Cached { get; } = new IncludeEvaluator(true);
+ private IncludeEvaluator() { }
+ public static IncludeEvaluator Instance = new();
public bool IsCriteriaEvaluator => false;
+ ///
public IQueryable GetQuery(IQueryable query, ISpecification specification) where T : class
{
- foreach (var includeString in specification.IncludeStrings)
+ Type? previousReturnType = null;
+ foreach (var includeExpression in specification.IncludeExpressions)
{
- query = query.Include(includeString);
- }
+ var lambdaExpr = includeExpression.LambdaExpression;
- foreach (var includeInfo in specification.IncludeExpressions)
- {
- if (includeInfo.Type == IncludeTypeEnum.Include)
+ if (includeExpression.Type == IncludeTypeEnum.Include)
{
- query = BuildInclude(query, includeInfo);
+ var key = new CacheKey(typeof(T), lambdaExpr.ReturnType, null);
+ previousReturnType = lambdaExpr.ReturnType;
+ var include = _cache.GetOrAdd(key, CreateIncludeDelegate);
+ query = (IQueryable)include(query, lambdaExpr);
}
- else if (includeInfo.Type == IncludeTypeEnum.ThenInclude)
+ else if (includeExpression.Type == IncludeTypeEnum.ThenInclude)
{
- query = BuildThenInclude(query, includeInfo);
+ var key = new CacheKey(typeof(T), lambdaExpr.ReturnType, previousReturnType);
+ previousReturnType = lambdaExpr.ReturnType;
+ var include = _cache.GetOrAdd(key, CreateThenIncludeDelegate);
+ query = (IQueryable)include(query, lambdaExpr);
}
}
return query;
}
- private IQueryable BuildInclude(IQueryable query, IncludeExpressionInfo includeInfo)
+ private static Func CreateIncludeDelegate(CacheKey cacheKey)
{
- _ = includeInfo ?? throw new ArgumentNullException(nameof(includeInfo));
-
- if (!_cacheEnabled)
- {
- var result = _includeMethodInfo.MakeGenericMethod(includeInfo.EntityType, includeInfo.PropertyType).Invoke(null, new object[] { query, includeInfo.LambdaExpression });
-
- _ = result ?? throw new TargetException();
-
- return (IQueryable)result;
- }
+ var includeMethod = _includeMethodInfo.MakeGenericMethod(cacheKey.EntityType, cacheKey.PropertyType);
+ var sourceParameter = Expression.Parameter(typeof(IQueryable));
+ var selectorParameter = Expression.Parameter(typeof(LambdaExpression));
- var include = _delegatesCache.GetOrAdd((includeInfo.EntityType, includeInfo.PropertyType, null), CreateIncludeDelegate).Value;
+ var call = Expression.Call(
+ includeMethod,
+ Expression.Convert(sourceParameter, typeof(IQueryable<>).MakeGenericType(cacheKey.EntityType)),
+ Expression.Convert(selectorParameter, typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(cacheKey.EntityType, cacheKey.PropertyType))));
- return (IQueryable)include(query, includeInfo.LambdaExpression);
+ var lambda = Expression.Lambda>(call, sourceParameter, selectorParameter);
+ return lambda.Compile();
}
- private IQueryable BuildThenInclude(IQueryable query, IncludeExpressionInfo includeInfo)
+ private static Func CreateThenIncludeDelegate(CacheKey cacheKey)
{
- _ = includeInfo ?? throw new ArgumentNullException(nameof(includeInfo));
- _ = includeInfo.PreviousPropertyType ?? throw new ArgumentNullException(nameof(includeInfo.PreviousPropertyType));
+ Debug.Assert(cacheKey.PreviousReturnType is not null);
- if (!_cacheEnabled)
- {
- var result = (IsGenericEnumerable(includeInfo.PreviousPropertyType, out var previousPropertyType)
- ? _thenIncludeAfterEnumerableMethodInfo
- : _thenIncludeAfterReferenceMethodInfo).MakeGenericMethod(includeInfo.EntityType, previousPropertyType, includeInfo.PropertyType)
- .Invoke(null, new object[] { query, includeInfo.LambdaExpression, });
-
- _ = result ?? throw new TargetException();
+ var thenIncludeInfo = IsGenericEnumerable(cacheKey.PreviousReturnType, out var previousPropertyType)
+ ? _thenIncludeAfterEnumerableMethodInfo
+ : _thenIncludeAfterReferenceMethodInfo;
- return (IQueryable)result;
- }
+ var thenIncludeMethod = thenIncludeInfo.MakeGenericMethod(cacheKey.EntityType, previousPropertyType, cacheKey.PropertyType);
+ var sourceParameter = Expression.Parameter(typeof(IQueryable));
+ var selectorParameter = Expression.Parameter(typeof(LambdaExpression));
- var thenInclude = _delegatesCache.GetOrAdd((includeInfo.EntityType, includeInfo.PropertyType, includeInfo.PreviousPropertyType), CreateThenIncludeDelegate).Value;
+ var call = Expression.Call(
+ thenIncludeMethod,
+ // We must pass cacheKey.PreviousReturnType. It must be exact type, not the generic type argument
+ Expression.Convert(sourceParameter, typeof(IIncludableQueryable<,>).MakeGenericType(cacheKey.EntityType, cacheKey.PreviousReturnType)),
+ Expression.Convert(selectorParameter, typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(previousPropertyType, cacheKey.PropertyType))));
- return (IQueryable)thenInclude(query, includeInfo.LambdaExpression);
+ var lambda = Expression.Lambda>(call, sourceParameter, selectorParameter);
+ return lambda.Compile();
}
- // (source, selector) => EntityFrameworkQueryableExtensions.Include((IQueryable)source, (Expression>)selector);
- private static Lazy> CreateIncludeDelegate((Type EntityType, Type PropertyType, Type? PreviousPropertyType) cacheKey)
- => new(() =>
- {
- var concreteInclude = _includeMethodInfo.MakeGenericMethod(cacheKey.EntityType, cacheKey.PropertyType);
- var sourceParameter = Expression.Parameter(typeof(IQueryable));
- var selectorParameter = Expression.Parameter(typeof(LambdaExpression));
-
- var call = Expression.Call(
- concreteInclude,
- Expression.Convert(sourceParameter, typeof(IQueryable<>).MakeGenericType(cacheKey.EntityType)),
- Expression.Convert(selectorParameter, typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(cacheKey.EntityType, cacheKey.PropertyType))));
-
- var lambda = Expression.Lambda>(call, sourceParameter, selectorParameter);
-
- return lambda.Compile();
- });
-
- // ((source, selector) =>
- // EntityFrameworkQueryableExtensions.ThenInclude(
- // (IIncludableQueryable)source,
- // (Expression>)selector);
- // (source, selector) =>
- // EntityFrameworkQueryableExtensions.ThenInclude(
- // (IIncludableQueryable>)source,
- // (Expression>)selector);
- private static Lazy> CreateThenIncludeDelegate((Type EntityType, Type PropertyType, Type? PreviousPropertyType) cacheKey)
- => new(() =>
- {
- _ = cacheKey.PreviousPropertyType ?? throw new ArgumentNullException(nameof(cacheKey.PreviousPropertyType));
-
- var thenIncludeInfo = _thenIncludeAfterReferenceMethodInfo;
- if (IsGenericEnumerable(cacheKey.PreviousPropertyType, out var previousPropertyType))
- {
- thenIncludeInfo = _thenIncludeAfterEnumerableMethodInfo;
- }
-
- var concreteThenInclude = thenIncludeInfo.MakeGenericMethod(cacheKey.EntityType, previousPropertyType, cacheKey.PropertyType);
- var sourceParameter = Expression.Parameter(typeof(IQueryable));
- var selectorParameter = Expression.Parameter(typeof(LambdaExpression));
-
- var call = Expression.Call(
- concreteThenInclude,
- Expression.Convert(
- sourceParameter,
- typeof(IIncludableQueryable<,>).MakeGenericType(cacheKey.EntityType, cacheKey.PreviousPropertyType)), // cacheKey.PreviousPropertyType must be exact type, not generic type argument
- Expression.Convert(selectorParameter, typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(previousPropertyType, cacheKey.PropertyType))));
-
- var lambda = Expression.Lambda>(call, sourceParameter, selectorParameter);
-
- return lambda.Compile();
- });
-
private static bool IsGenericEnumerable(Type type, out Type propertyType)
{
- if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ if (type.IsGenericType && typeof(IEnumerable).IsAssignableFrom(type))
{
propertyType = type.GenericTypeArguments[0];
-
return true;
}
propertyType = type;
-
return false;
}
}
diff --git a/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs
new file mode 100644
index 00000000..72c57354
--- /dev/null
+++ b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs
@@ -0,0 +1,20 @@
+namespace Ardalis.Specification.EntityFrameworkCore;
+
+public sealed class IncludeStringEvaluator : IEvaluator
+{
+ private IncludeStringEvaluator() { }
+ public static IncludeStringEvaluator Instance { get; } = new();
+
+ public bool IsCriteriaEvaluator => false;
+
+ ///
+ public IQueryable GetQuery(IQueryable query, ISpecification specification) where T : class
+ {
+ foreach (var includeString in specification.IncludeStrings)
+ {
+ query = query.Include(includeString);
+ }
+
+ return query;
+ }
+}
diff --git a/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs
index 5434ac11..c85fde7e 100644
--- a/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs
+++ b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs
@@ -4,25 +4,22 @@
public class SpecificationEvaluator : ISpecificationEvaluator
{
// Will use singleton for default configuration. Yet, it can be instantiated if necessary, with default or provided evaluators.
- ///
- /// instance with default evaluators and without any additional features enabled.
- ///
- public static SpecificationEvaluator Default { get; } = new SpecificationEvaluator();
///
- /// instance with default evaluators and enabled caching.
+ /// instance with default evaluators..
///
- public static SpecificationEvaluator Cached { get; } = new SpecificationEvaluator(true);
+ public static SpecificationEvaluator Default { get; } = new SpecificationEvaluator();
protected List Evaluators { get; } = new List();
- public SpecificationEvaluator(bool cacheEnabled = false)
+ public SpecificationEvaluator()
{
Evaluators.AddRange(new IEvaluator[]
{
WhereEvaluator.Instance,
SearchEvaluator.Instance,
- cacheEnabled ? IncludeEvaluator.Cached : IncludeEvaluator.Default,
+ IncludeStringEvaluator.Instance,
+ IncludeEvaluator.Instance,
OrderEvaluator.Instance,
PaginationEvaluator.Instance,
AsNoTrackingEvaluator.Instance,
diff --git a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs
index 5d9f6749..b7214f15 100644
--- a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs
+++ b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs
@@ -3,7 +3,7 @@
[Collection("SharedCollection")]
public class IncludeEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
{
- private static readonly IncludeEvaluator _evaluator = IncludeEvaluator.Default;
+ private static readonly IncludeEvaluator _evaluator = IncludeEvaluator.Instance;
[Fact]
public void QueriesMatch_GivenIncludeExpressions()
diff --git a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeStringEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeStringEvaluatorTests.cs
index e217c271..b8b4592a 100644
--- a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeStringEvaluatorTests.cs
+++ b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeStringEvaluatorTests.cs
@@ -1,45 +1,45 @@
-//namespace Tests.Evaluators;
-
-//[Collection("SharedCollection")]
-//public class IncludeStringEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
-//{
-// private static readonly IncludeStringEvaluator _evaluator = IncludeStringEvaluator.Instance;
-
-// [Fact]
-// public void QueriesMatch_GivenIncludeString()
-// {
-// var spec = new Specification();
-// spec.Query
-// .Include(nameof(Address));
-
-// var actual = _evaluator
-// .Evaluate(DbContext.Stores, spec)
-// .ToQueryString();
-
-// var expected = DbContext.Stores
-// .Include(nameof(Address))
-// .ToQueryString();
-
-// actual.Should().Be(expected);
-// }
-
-// [Fact]
-// public void QueriesMatch_GivenMultipleIncludeStrings()
-// {
-// var spec = new Specification();
-// spec.Query
-// .Include(nameof(Address))
-// .Include($"{nameof(Company)}.{nameof(Country)}");
-
-// var actual = _evaluator
-// .Evaluate(DbContext.Stores, spec)
-// .ToQueryString();
-
-// var expected = DbContext.Stores
-// .Include(nameof(Address))
-// .Include($"{nameof(Company)}.{nameof(Country)}")
-// .ToQueryString();
-
-// actual.Should().Be(expected);
-// }
-//}
+namespace Tests.Evaluators;
+
+[Collection("SharedCollection")]
+public class IncludeStringEvaluatorTests(TestFactory factory) : IntegrationTest(factory)
+{
+ private static readonly IncludeStringEvaluator _evaluator = IncludeStringEvaluator.Instance;
+
+ [Fact]
+ public void QueriesMatch_GivenIncludeString()
+ {
+ var spec = new Specification();
+ spec.Query
+ .Include(nameof(Address));
+
+ var actual = _evaluator
+ .GetQuery(DbContext.Stores, spec)
+ .ToQueryString();
+
+ var expected = DbContext.Stores
+ .Include(nameof(Address))
+ .ToQueryString();
+
+ actual.Should().Be(expected);
+ }
+
+ [Fact]
+ public void QueriesMatch_GivenMultipleIncludeStrings()
+ {
+ var spec = new Specification();
+ spec.Query
+ .Include(nameof(Address))
+ .Include($"{nameof(Company)}.{nameof(Country)}");
+
+ var actual = _evaluator
+ .GetQuery(DbContext.Stores, spec)
+ .ToQueryString();
+
+ var expected = DbContext.Stores
+ .Include(nameof(Address))
+ .Include($"{nameof(Company)}.{nameof(Country)}")
+ .ToQueryString();
+
+ actual.Should().Be(expected);
+ }
+}
diff --git a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs
index 3719296b..d99139d1 100644
--- a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs
+++ b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/SpecificationEvaluatorTests.cs
@@ -75,8 +75,7 @@ public void GivenFullQuery()
.IgnoreQueryFilters();
var actual = _evaluator.GetQuery(DbContext.Stores, spec)
- .ToQueryString()
- .Replace("__criteria_SearchTerm_", "__Format_");
+ .ToQueryString();
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
@@ -129,8 +128,7 @@ public void GivenExpressionsInRandomOrder()
.IgnoreQueryFilters();
var actual = _evaluator.GetQuery(DbContext.Stores, spec)
- .ToQueryString()
- .Replace("__criteria_SearchTerm_", "__Format_");
+ .ToQueryString();
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
@@ -184,8 +182,7 @@ public void GivenFullQueryWithSelect()
spec.Query.Select(x => x.Name);
var actual = _evaluator.GetQuery(DbContext.Stores, spec)
- .ToQueryString()
- .Replace("__criteria_SearchTerm_", "__Format_");
+ .ToQueryString();
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
@@ -241,8 +238,7 @@ public void GivenFullQueryWithSelect()
// spec.Query.SelectMany(x => x.Products.Select(x => x.Name));
// var actual = _evaluator.GetQuery(DbContext.Stores, spec)
- // .ToQueryString()
- // .Replace("__criteria_SearchTerm_", "__Format_");
+ // .ToQueryString();
// // The expression in the spec are applied in a predefined order.
// var expected = DbContext.Stores
diff --git a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Extensions/Extensions_WithSpecification.cs b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Extensions/Extensions_WithSpecification.cs
index 6787998d..39d08ea1 100644
--- a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Extensions/Extensions_WithSpecification.cs
+++ b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Extensions/Extensions_WithSpecification.cs
@@ -34,8 +34,7 @@ public void QueriesMatch_GivenFullQuery()
var actual = DbContext.Stores
.WithSpecification(spec)
- .ToQueryString()
- .Replace("__criteria_SearchTerm_", "__Format_");
+ .ToQueryString();
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
@@ -90,8 +89,7 @@ public void QueriesMatch_GivenFullQueryWithSelect()
var actual = DbContext.Stores
.WithSpecification(spec)
- .ToQueryString()
- .Replace("__criteria_SearchTerm_", "__Format_");
+ .ToQueryString();
// The expression in the spec are applied in a predefined order.
var expected = DbContext.Stores
@@ -148,8 +146,7 @@ public void QueriesMatch_GivenFullQueryWithSelect()
// var actual = DbContext.Stores
// .WithSpecification(spec)
- // .ToQueryString()
- // .Replace("__criteria_SearchTerm_", "__Format_");
+ // .ToQueryString();
// // The expression in the spec are applied in a predefined order.
// var expected = DbContext.Stores