diff --git a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/OrderEvaluator.cs b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/OrderEvaluator.cs index 56afba2c..0979bff7 100644 --- a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/OrderEvaluator.cs +++ b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/OrderEvaluator.cs @@ -15,8 +15,8 @@ public IQueryable GetQuery(IQueryable query, ISpecification specific { if (specification.OrderExpressions != null) { - if (specification.OrderExpressions.Where(x => x.OrderType == OrderTypeEnum.OrderBy || - x.OrderType == OrderTypeEnum.OrderByDescending).Count() > 1) + if (specification.OrderExpressions.Count(x => x.OrderType == OrderTypeEnum.OrderBy + || x.OrderType == OrderTypeEnum.OrderByDescending) > 1) { throw new DuplicateOrderChainException(); } @@ -55,8 +55,8 @@ public IEnumerable Evaluate(IEnumerable query, ISpecification specif { if (specification.OrderExpressions != null) { - if (specification.OrderExpressions.Where(x => x.OrderType == OrderTypeEnum.OrderBy || - x.OrderType == OrderTypeEnum.OrderByDescending).Count() > 1) + if (specification.OrderExpressions.Count(x => x.OrderType == OrderTypeEnum.OrderBy + || x.OrderType == OrderTypeEnum.OrderByDescending) > 1) { throw new DuplicateOrderChainException(); } diff --git a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/SearchEvaluator.cs b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/SearchEvaluator.cs index 15d60250..df77ee79 100644 --- a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/SearchEvaluator.cs +++ b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/SearchEvaluator.cs @@ -13,8 +13,7 @@ public IQueryable GetQuery(IQueryable query, ISpecification specific { foreach (var searchCriteria in specification.SearchCriterias.GroupBy(x => x.SearchGroup)) { - var criterias = searchCriteria.Select(x => (x.Selector, x.SearchTerm)); - query = query.Search(criterias); + query = query.Search(searchCriteria); } return query; diff --git a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Extensions/SearchExtension.cs b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Extensions/SearchExtension.cs index c8c7edc3..ea9bca0f 100644 --- a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Extensions/SearchExtension.cs +++ b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Extensions/SearchExtension.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Data.Entity; using System.Linq; using System.Linq.Expressions; -using System.Data.Entity; namespace Ardalis.Specification.EntityFramework6 { @@ -20,15 +20,18 @@ public static class SearchExtension /// /// /// - public static IQueryable Search(this IQueryable source, IEnumerable<(Expression> selector, string searchTerm)> criterias) + public static IQueryable Search(this IQueryable source, IEnumerable> criterias) { Expression expr = null; var parameter = Expression.Parameter(typeof(T), "x"); - foreach (var (selector, searchTerm) in criterias) + foreach (var criteria in criterias) { - if (selector == null || string.IsNullOrEmpty(searchTerm)) + var (selector, searchTerm) = (criteria.Selector, criteria.SearchTerm); + if (string.IsNullOrEmpty(criteria.SearchTerm)) + { continue; + } var like = typeof(DbFunctions).GetMethod(nameof(DbFunctions.Like), new Type[] { typeof(string), typeof(string) }); diff --git a/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_GetBySpec.cs b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_GetBySpec.cs index 334613e9..3cccf5c6 100644 --- a/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_GetBySpec.cs +++ b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_GetBySpec.cs @@ -87,5 +87,18 @@ public async Task ReturnsUntrackedCompany_GivenCompanyByIdAsUntrackedSpec() result?.Name.Should().Be(CompanySeed.VALID_COMPANY_NAME); dbContext.Entry(result).State.Should().Be(EntityState.Detached); } + + [Fact] + public async Task ReturnsStoreWithCompanyAndCountryAndStoresForCompany_GivenStoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec() + { + var result = await storeRepository.GetBySpecAsync(new StoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec(StoreSeed.VALID_STORE_ID)); + + result.Should().NotBeNull(); + result.Name.Should().Be(StoreSeed.VALID_STORE_NAME); + result.Company.Should().NotBeNull(); + result.Company?.Country.Should().NotBeNull(); + result.Company?.Stores.Should().HaveCountGreaterOrEqualTo(2); + result.Company?.Stores?.Should().Match(x => x.Any(z => z.Products.Count > 0)); + } } } \ No newline at end of file diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/CachedReadConcurrentDictionary.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/CachedReadConcurrentDictionary.cs new file mode 100644 index 00000000..241ae4d4 --- /dev/null +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/CachedReadConcurrentDictionary.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Ardalis.Specification.EntityFrameworkCore +{ + /// + /// A thread-safe dictionary for read-heavy workloads. + /// + /// The key type. + /// The value type. + internal class CachedReadConcurrentDictionary : IDictionary + { + /// + /// 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() + { + this.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) + { + this.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) + { + this.comparer = comparer; + this.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) + { + this.comparer = comparer; + this.dictionary = new ConcurrentDictionary(collection, comparer); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + public IEnumerator> GetEnumerator() => this.GetReadDictionary().GetEnumerator(); + + /// + public void Add(KeyValuePair item) + { + ((IDictionary)this.dictionary).Add(item); + this.InvalidateCache(); + } + + /// + public void Clear() + { + this.dictionary.Clear(); + this.InvalidateCache(); + } + + /// + public bool Contains(KeyValuePair item) => this.GetReadDictionary().Contains(item); + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + this.GetReadDictionary().CopyTo(array, arrayIndex); + } + + /// + public bool Remove(KeyValuePair item) + { + var result = ((IDictionary)this.dictionary).Remove(item); + if (result) this.InvalidateCache(); + return result; + } + + /// + public int Count => this.GetReadDictionary().Count; + + /// + public bool IsReadOnly => false; + + /// + public void Add(TKey key, TValue value) + { + ((IDictionary)this.dictionary).Add(key, value); + this.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 (this.GetReadDictionary().TryGetValue(key, out var value)) + { + return value; + } + + value = this.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 (this.dictionary.TryAdd(key, value)) + { + this.InvalidateCache(); + return true; + } + + return false; + } + + /// + public bool ContainsKey(TKey key) => this.GetReadDictionary().ContainsKey(key); + + /// + public bool Remove(TKey key) + { + var result = ((IDictionary)this.dictionary).Remove(key); + if (result) this.InvalidateCache(); + return result; + } + + /// + public bool TryGetValue(TKey key, out TValue value) => this.GetReadDictionary().TryGetValue(key, out value); + + /// + public TValue this[TKey key] + { + get => this.GetReadDictionary()[key]; + set + { + this.dictionary[key] = value; + this.InvalidateCache(); + } + } + + /// + public ICollection Keys => this.GetReadDictionary().Keys; + + /// + public ICollection Values => this.GetReadDictionary().Values; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IDictionary GetReadDictionary() => this.readCache ?? this.GetWithoutCache(); + + private IDictionary GetWithoutCache() + { + // If the dictionary was recently modified or the cache is being recomputed, return the dictionary directly. + if (Interlocked.Increment(ref this.cacheMissReads) < CacheMissesBeforeCaching) + { + return this.dictionary; + } + + // Recompute the cache if too many cache misses have occurred. + this.cacheMissReads = 0; + return this.readCache = new Dictionary(this.dictionary, this.comparer); + } + + private void InvalidateCache() + { + this.cacheMissReads = 0; + this.readCache = null; + } + } +} diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs index efb68277..7c76fca9 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs @@ -1,17 +1,65 @@ -using Microsoft.EntityFrameworkCore; -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; namespace Ardalis.Specification.EntityFrameworkCore { public class IncludeEvaluator : IEvaluator { - private IncludeEvaluator() { } - public static IncludeEvaluator Instance { get; } = new IncludeEvaluator(); + private static readonly MethodInfo IncludeMethodInfo = typeof(EntityFrameworkQueryableExtensions) + .GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.Include)) + .Single(mi => mi.GetGenericArguments().Length == 2 + && mi.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>) + && mi.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>)); - public bool IsCriteriaEvaluator { get; } = false; + private static readonly MethodInfo ThenIncludeAfterReferenceMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetTypeInfo().GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude)) + .Single(mi => 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<>)); + + 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 CachedReadConcurrentDictionary<(Type EntityType, Type PropertyType, Type? PreviousPropertyType), Lazy>>(); + + private readonly bool cacheEnabled; + + private IncludeEvaluator(bool cacheEnabled) + { + this.cacheEnabled = cacheEnabled; + } + + /// + /// instance without any additional features. + /// + public static IncludeEvaluator Default { get; } = new IncludeEvaluator(false); + + /// + /// instance with caching to provide better performance. + /// + public static IncludeEvaluator Cached { get; } = new IncludeEvaluator(true); + + public bool IsCriteriaEvaluator => false; public IQueryable GetQuery(IQueryable query, ISpecification specification) where T : class { @@ -24,15 +72,112 @@ public IQueryable GetQuery(IQueryable query, ISpecification specific { if (includeInfo.Type == IncludeTypeEnum.Include) { - query = query.Include(includeInfo); + query = this.BuildInclude(query, includeInfo); } else if (includeInfo.Type == IncludeTypeEnum.ThenInclude) { - query = query.ThenInclude(includeInfo); + query = this.BuildThenInclude(query, includeInfo); } } return query; } + + private IQueryable BuildInclude(IQueryable query, IncludeExpressionInfo includeInfo) + { + _ = includeInfo ?? throw new ArgumentNullException(nameof(includeInfo)); + + if (!this.cacheEnabled) + { + return (IQueryable)IncludeMethodInfo.MakeGenericMethod(includeInfo.EntityType, includeInfo.PropertyType).Invoke(null, new object[] { query, includeInfo.LambdaExpression, }); + } + + var include = DelegatesCache.GetOrAdd((includeInfo.EntityType, includeInfo.PropertyType, null), CreateIncludeDelegate).Value; + + return (IQueryable)include(query, includeInfo.LambdaExpression); + } + + private IQueryable BuildThenInclude(IQueryable query, IncludeExpressionInfo includeInfo) + { + _ = includeInfo ?? throw new ArgumentNullException(nameof(includeInfo)); + _ = includeInfo.PreviousPropertyType ?? throw new ArgumentNullException(nameof(includeInfo.PreviousPropertyType)); + + if (!this.cacheEnabled) + { + return (IQueryable)(IsGenericEnumerable(includeInfo.PreviousPropertyType, out var previousPropertyType) + ? ThenIncludeAfterEnumerableMethodInfo + : ThenIncludeAfterReferenceMethodInfo).MakeGenericMethod(includeInfo.EntityType, previousPropertyType, includeInfo.PropertyType) + .Invoke(null, new object[] { query, includeInfo.LambdaExpression, }); + } + + var thenInclude = DelegatesCache.GetOrAdd((includeInfo.EntityType, includeInfo.PropertyType, includeInfo.PreviousPropertyType), CreateThenIncludeDelegate).Value; + + return (IQueryable)thenInclude(query, includeInfo.LambdaExpression); + } + + // (source, selector) => EntityFrameworkQueryableExtensions.Include((IQueryable)source, (Expression>)selector); + private static Lazy> CreateIncludeDelegate((Type EntityType, Type PropertyType, Type? PreviousPropertyType) cacheKey) + => new Lazy>(() => + { + 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 Lazy>(() => + { + MethodInfo 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<>)) + { + propertyType = type.GenericTypeArguments[0]; + + return true; + } + + propertyType = type; + + return false; + } } } diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SearchEvaluator.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SearchEvaluator.cs index 92ac8b00..42f697a9 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SearchEvaluator.cs +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SearchEvaluator.cs @@ -1,8 +1,4 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Linq; namespace Ardalis.Specification.EntityFrameworkCore { @@ -17,8 +13,7 @@ public IQueryable GetQuery(IQueryable query, ISpecification specific { foreach (var searchCriteria in specification.SearchCriterias.GroupBy(x => x.SearchGroup)) { - var criterias = searchCriteria.Select(x => (x.Selector, x.SearchTerm)); - query = query.Search(criterias); + query = query.Search(searchCriteria); } return query; diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs index b3bf50e3..bac4e813 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs @@ -1,8 +1,5 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; namespace Ardalis.Specification.EntityFrameworkCore { @@ -10,17 +7,25 @@ namespace Ardalis.Specification.EntityFrameworkCore 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. + /// + public static SpecificationEvaluator Cached { get; } = new SpecificationEvaluator(true); + private readonly List evaluators = new List(); - public SpecificationEvaluator() + public SpecificationEvaluator(bool cacheEnabled = false) { this.evaluators.AddRange(new IEvaluator[] { WhereEvaluator.Instance, SearchEvaluator.Instance, - IncludeEvaluator.Instance, + cacheEnabled ? IncludeEvaluator.Cached : IncludeEvaluator.Default, OrderEvaluator.Instance, PaginationEvaluator.Instance, AsNoTrackingEvaluator.Instance, @@ -30,6 +35,7 @@ public SpecificationEvaluator() #endif }); } + public SpecificationEvaluator(IEnumerable evaluators) { this.evaluators.AddRange(evaluators); diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Extensions/IncludeExtensions.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Extensions/IncludeExtensions.cs deleted file mode 100644 index 5a226e99..00000000 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Extensions/IncludeExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; - -namespace Ardalis.Specification.EntityFrameworkCore -{ - public static class IncludeExtensions - { - public static IQueryable Include(this IQueryable source, IncludeExpressionInfo info) - { - _ = info ?? throw new ArgumentNullException(nameof(info)); - - var queryExpr = Expression.Call( - typeof(EntityFrameworkQueryableExtensions), - "Include", - new Type[] { - info.EntityType, - info.PropertyType - }, - source.Expression, - info.LambdaExpression - ); - - return source.Provider.CreateQuery(queryExpr); - } - public static IQueryable ThenInclude(this IQueryable source, IncludeExpressionInfo info) - { - _ = info ?? throw new ArgumentNullException(nameof(info)); - _ = info.PreviousPropertyType ?? throw new ArgumentNullException(nameof(info.PreviousPropertyType)); - - var queryExpr = Expression.Call( - typeof(EntityFrameworkQueryableExtensions), - "ThenInclude", - new Type[] { - info.EntityType, - info.PreviousPropertyType, - info.PropertyType - }, - source.Expression, - info.LambdaExpression - ); - - return source.Provider.CreateQuery(queryExpr); - } - } -} diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Extensions/SearchExtension.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Extensions/SearchExtension.cs index 4bb5e472..9d30959c 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Extensions/SearchExtension.cs +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Extensions/SearchExtension.cs @@ -1,8 +1,8 @@ -using Microsoft.EntityFrameworkCore; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; namespace Ardalis.Specification.EntityFrameworkCore { @@ -20,27 +20,27 @@ public static class SearchExtension /// /// /// - public static IQueryable Search(this IQueryable source, IEnumerable<(Expression> selector, string searchTerm)> criterias) + public static IQueryable Search(this IQueryable source, IEnumerable> criterias) { Expression? expr = null; var parameter = Expression.Parameter(typeof(T), "x"); foreach (var criteria in criterias) { - if (criteria.selector == null || string.IsNullOrEmpty(criteria.searchTerm)) + if (string.IsNullOrEmpty(criteria.SearchTerm)) continue; var functions = Expression.Property(null, typeof(EF).GetProperty(nameof(EF.Functions))); var like = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new Type[] { functions.Type, typeof(string), typeof(string) }); - var propertySelector = ParameterReplacerVisitor.Replace(criteria.selector, criteria.selector.Parameters[0], parameter); + var propertySelector = ParameterReplacerVisitor.Replace(criteria.Selector, criteria.Selector.Parameters[0], parameter); var likeExpression = Expression.Call( null, like, functions, (propertySelector as LambdaExpression)?.Body, - Expression.Constant(criteria.searchTerm)); + Expression.Constant(criteria.SearchTerm)); expr = expr == null ? (Expression)likeExpression : Expression.OrElse(expr, likeExpression); } diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs index 59fbbfa9..fb4d5944 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs @@ -1,9 +1,9 @@ -using Microsoft.EntityFrameworkCore; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; namespace Ardalis.Specification.EntityFrameworkCore { diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/IntegrationTestBase.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/IntegrationTestBase.cs index 0b0fc40a..292be7e2 100644 --- a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/IntegrationTestBase.cs +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/IntegrationTestBase.cs @@ -1,23 +1,20 @@ using Ardalis.Specification.UnitTests.Fixture.Entities; -using System; -using System.Collections.Generic; -using System.Text; using Xunit; namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture { - public class IntegrationTestBase : IClassFixture + public abstract class IntegrationTestBase : IClassFixture { protected TestDbContext dbContext; protected Repository companyRepository; protected Repository storeRepository; - public IntegrationTestBase(SharedDatabaseFixture fixture) + protected IntegrationTestBase(SharedDatabaseFixture fixture, ISpecificationEvaluator specificationEvaluator) { dbContext = fixture.CreateContext(); - - companyRepository = new Repository(dbContext); - storeRepository = new Repository(dbContext); + + companyRepository = new Repository(dbContext, specificationEvaluator); + storeRepository = new Repository(dbContext, specificationEvaluator); } } } diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs index ba8a5b8c..a9286c77 100644 --- a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs @@ -1,18 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; - -namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture +namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture { /// public class Repository : RepositoryBase where T : class { protected readonly TestDbContext dbContext; - public Repository(TestDbContext dbContext) : base(dbContext) + public Repository(TestDbContext dbContext, ISpecificationEvaluator specificationEvaluator) : base(dbContext, specificationEvaluator) { this.dbContext = dbContext; } diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_AnyAsync.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_AnyAsync.cs index e8222e3b..bd2039b1 100644 --- a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_AnyAsync.cs +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_AnyAsync.cs @@ -1,18 +1,32 @@ -using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; +using System.Threading.Tasks; +using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; using Ardalis.Specification.UnitTests.Fixture.Entities.Seeds; using Ardalis.Specification.UnitTests.Fixture.Specs; using FluentAssertions; -using System.Threading.Tasks; using Xunit; namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests { - public class RepositoryOfT_AnyAsync : IntegrationTestBase + public class RepositoryOfT_AnyAsync : RepositoryOfT_AnyAsync_TestKit { - public RepositoryOfT_AnyAsync(SharedDatabaseFixture fixture) : base(fixture) { } + public RepositoryOfT_AnyAsync(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Default) + { + } + } + + public class RepositoryOfT_AnyAsync_Cached : RepositoryOfT_AnyAsync_TestKit + { + public RepositoryOfT_AnyAsync_Cached(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Cached) + { + } + } + + public abstract class RepositoryOfT_AnyAsync_TestKit : IntegrationTestBase + { + protected RepositoryOfT_AnyAsync_TestKit(SharedDatabaseFixture fixture, ISpecificationEvaluator specificationEvaluator) : base(fixture, specificationEvaluator) { } [Fact] - public async Task ReturnsTrueOnStoresRecords_WithoutSpec() + public virtual async Task ReturnsTrueOnStoresRecords_WithoutSpec() { var result = await storeRepository.AnyAsync(); @@ -20,7 +34,7 @@ public async Task ReturnsTrueOnStoresRecords_WithoutSpec() } [Fact] - public async Task ReturnsTrue_GivenStoreByIdSpecWithValidStore() + public virtual async Task ReturnsTrue_GivenStoreByIdSpecWithValidStore() { var result = await storeRepository.AnyAsync(new StoreByIdSpec(StoreSeed.VALID_STORE_ID)); @@ -28,7 +42,7 @@ public async Task ReturnsTrue_GivenStoreByIdSpecWithValidStore() } [Fact] - public async Task ReturnsTrue_GivenStoreByIdSpecWithInvalidStore() + public virtual async Task ReturnsTrue_GivenStoreByIdSpecWithInvalidStore() { var result = await storeRepository.AnyAsync(new StoreByIdSpec(0)); diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetById.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetById.cs index 65c817ca..90fa5c45 100644 --- a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetById.cs +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetById.cs @@ -1,22 +1,31 @@ -using FluentAssertions; +using System.Threading.Tasks; using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; using Ardalis.Specification.UnitTests.Fixture.Entities.Seeds; -using Ardalis.Specification.UnitTests.Fixture.Specs; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using FluentAssertions; using Xunit; namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests { - public class RepositoryOfT_GetById : IntegrationTestBase + public class RepositoryOfT_GetById : RepositoryOfT_GetById_TestKit { - public RepositoryOfT_GetById(SharedDatabaseFixture fixture) : base(fixture) { } + public RepositoryOfT_GetById(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Default) + { + } + } + + public class RepositoryOfT_GetById_Cached : RepositoryOfT_GetById_TestKit + { + public RepositoryOfT_GetById_Cached(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Cached) + { + } + } + + public abstract class RepositoryOfT_GetById_TestKit : IntegrationTestBase + { + protected RepositoryOfT_GetById_TestKit(SharedDatabaseFixture fixture, ISpecificationEvaluator specificationEvaluator) : base(fixture, specificationEvaluator) { } [Fact] - public async Task ReturnsStore_GivenId() + public virtual async Task ReturnsStore_GivenId() { var result = await storeRepository.GetByIdAsync(StoreSeed.VALID_STORE_ID); @@ -25,7 +34,7 @@ public async Task ReturnsStore_GivenId() } [Fact] - public async Task ReturnsStore_GivenGenericId() + public virtual async Task ReturnsStore_GivenGenericId() { var result = await storeRepository.GetByIdAsync(StoreSeed.VALID_STORE_ID); diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetBySpec.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetBySpec.cs index 51160ce3..d1443458 100644 --- a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetBySpec.cs +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_GetBySpec.cs @@ -1,22 +1,33 @@ -using FluentAssertions; +using System.Linq; +using System.Threading.Tasks; using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; using Ardalis.Specification.UnitTests.Fixture.Entities.Seeds; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; using Ardalis.Specification.UnitTests.Fixture.Specs; +using FluentAssertions; +using Xunit; namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests { - public class RepositoryOfT_GetBySpec : IntegrationTestBase + public class RepositoryOfT_GetBySpec : RepositoryOfT_GetBySpec_TestKit { - public RepositoryOfT_GetBySpec(SharedDatabaseFixture fixture) : base(fixture) { } + public RepositoryOfT_GetBySpec(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Default) + { + } + } + + public class RepositoryOfT_GetBySpec_Cached : RepositoryOfT_GetBySpec_TestKit + { + public RepositoryOfT_GetBySpec_Cached(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Cached) + { + } + } + + public abstract class RepositoryOfT_GetBySpec_TestKit : IntegrationTestBase + { + protected RepositoryOfT_GetBySpec_TestKit(SharedDatabaseFixture fixture, ISpecificationEvaluator specificationEvaluator ) : base(fixture, specificationEvaluator) { } [Fact] - public async Task ReturnsStoreWithProducts_GivenStoreByIdIncludeProductsSpec() + public virtual async Task ReturnsStoreWithProducts_GivenStoreByIdIncludeProductsSpec() { var result = await storeRepository.GetBySpecAsync(new StoreByIdIncludeProductsSpec(StoreSeed.VALID_STORE_ID)); @@ -26,7 +37,7 @@ public async Task ReturnsStoreWithProducts_GivenStoreByIdIncludeProductsSpec() } [Fact] - public async Task ReturnsStoreWithAddress_GivenStoreByIdIncludeAddressSpec() + public virtual async Task ReturnsStoreWithAddress_GivenStoreByIdIncludeAddressSpec() { var result = await storeRepository.GetBySpecAsync(new StoreByIdIncludeAddressSpec(StoreSeed.VALID_STORE_ID)); @@ -36,7 +47,7 @@ public async Task ReturnsStoreWithAddress_GivenStoreByIdIncludeAddressSpec() } [Fact] - public async Task ReturnsStoreWithAddressAndProduct_GivenStoreByIdIncludeAddressAndProductsSpec() + public virtual async Task ReturnsStoreWithAddressAndProduct_GivenStoreByIdIncludeAddressAndProductsSpec() { var result = await storeRepository.GetBySpecAsync(new StoreByIdIncludeAddressAndProductsSpec(StoreSeed.VALID_STORE_ID)); @@ -47,7 +58,7 @@ public async Task ReturnsStoreWithAddressAndProduct_GivenStoreByIdIncludeAddress } [Fact] - public async Task ReturnsStoreWithProducts_GivenStoreByIdIncludeProductsUsingStringSpec() + public virtual async Task ReturnsStoreWithProducts_GivenStoreByIdIncludeProductsUsingStringSpec() { var result = await storeRepository.GetBySpecAsync(new StoreByIdIncludeProductsUsingStringSpec(StoreSeed.VALID_STORE_ID)); @@ -57,7 +68,7 @@ public async Task ReturnsStoreWithProducts_GivenStoreByIdIncludeProductsUsingStr } [Fact] - public async Task ReturnsCompanyWithStoresAndAddress_GivenCompanyByIdIncludeStoresThenIncludeAddressSpec() + public virtual async Task ReturnsCompanyWithStoresAndAddress_GivenCompanyByIdIncludeStoresThenIncludeAddressSpec() { var result = await companyRepository.GetBySpecAsync(new CompanyByIdIncludeStoresThenIncludeAddressSpec(CompanySeed.VALID_COMPANY_ID)); @@ -68,7 +79,7 @@ public async Task ReturnsCompanyWithStoresAndAddress_GivenCompanyByIdIncludeStor } [Fact] - public async Task ReturnsCompanyWithStoresAndProducts_GivenCompanyByIdIncludeStoresThenIncludeProductsSpec() + public virtual async Task ReturnsCompanyWithStoresAndProducts_GivenCompanyByIdIncludeStoresThenIncludeProductsSpec() { var result = await companyRepository.GetBySpecAsync(new CompanyByIdIncludeStoresThenIncludeProductsSpec(CompanySeed.VALID_COMPANY_ID)); @@ -79,7 +90,7 @@ public async Task ReturnsCompanyWithStoresAndProducts_GivenCompanyByIdIncludeSto } [Fact] - public async Task ReturnsUntrackedCompany_GivenCompanyByIdAsUntrackedSpec() + public virtual async Task ReturnsUntrackedCompany_GivenCompanyByIdAsUntrackedSpec() { dbContext.ChangeTracker.Clear(); @@ -89,5 +100,18 @@ public async Task ReturnsUntrackedCompany_GivenCompanyByIdAsUntrackedSpec() result?.Name.Should().Be(CompanySeed.VALID_COMPANY_NAME); dbContext.Entry(result).State.Should().Be(Microsoft.EntityFrameworkCore.EntityState.Detached); } + + [Fact] + public virtual async Task ReturnsStoreWithCompanyAndCountryAndStoresForCompany_GivenStoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec() + { + var result = await storeRepository.GetBySpecAsync(new StoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec(StoreSeed.VALID_STORE_ID)); + + result.Should().NotBeNull(); + result!.Name.Should().Be(StoreSeed.VALID_STORE_NAME); + result.Company.Should().NotBeNull(); + result.Company!.Country.Should().NotBeNull(); + result.Company!.Stores.Should().HaveCountGreaterOrEqualTo(2); + result.Company?.Stores?.Should().Match(x => x.Any(z => z.Products.Count > 0)); + } } } diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_ListAsync.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_ListAsync.cs index 9aea13c5..3ca6cb88 100644 --- a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_ListAsync.cs +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_ListAsync.cs @@ -1,23 +1,33 @@ -using FluentAssertions; +using System.Linq; +using System.Threading.Tasks; using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; -using Ardalis.Specification.UnitTests.Fixture.Specs; -using Ardalis.Specification.UnitTests.Fixture.Entities; using Ardalis.Specification.UnitTests.Fixture.Entities.Seeds; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Ardalis.Specification.UnitTests.Fixture.Specs; +using FluentAssertions; using Xunit; namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests { - public class RepositoryOfT_ListAsync : IntegrationTestBase + public class RepositoryOfT_ListAsync : RepositoryOfT_ListAsync_TestKit + { + public RepositoryOfT_ListAsync(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Default) + { + } + } + + public class RepositoryOfT_ListAsync_Cached : RepositoryOfT_ListAsync_TestKit + { + public RepositoryOfT_ListAsync_Cached(SharedDatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Cached) + { + } + } + + public abstract class RepositoryOfT_ListAsync_TestKit : IntegrationTestBase { - public RepositoryOfT_ListAsync(SharedDatabaseFixture fixture) : base(fixture) { } + protected RepositoryOfT_ListAsync_TestKit(SharedDatabaseFixture fixture, ISpecificationEvaluator specificationEvaluator) : base(fixture, specificationEvaluator) { } [Fact] - public async Task ReturnsStoreWithProducts_GivenStoreIncludeProductsSpec() + public virtual async Task ReturnsStoreWithProducts_GivenStoreIncludeProductsSpec() { var result = await storeRepository.ListAsync(new StoreIncludeProductsSpec()); @@ -27,7 +37,7 @@ public async Task ReturnsStoreWithProducts_GivenStoreIncludeProductsSpec() } [Fact] - public async Task ReturnsStoreWithAddress_GivenStoreIncludeAddressSpec() + public virtual async Task ReturnsStoreWithAddress_GivenStoreIncludeAddressSpec() { var result = await storeRepository.ListAsync(new StoreIncludeAddressSpec()); @@ -37,7 +47,7 @@ public async Task ReturnsStoreWithAddress_GivenStoreIncludeAddressSpec() } [Fact] - public async Task ReturnsStoreWithAddressAndProduct_GivenStoreIncludeProductsThenStoreSpec() + public virtual async Task ReturnsStoreWithAddressAndProduct_GivenStoreIncludeProductsThenStoreSpec() { var result = await storeRepository.ListAsync(new StoreIncludeProductsThenStoreSpec()); @@ -48,7 +58,7 @@ public async Task ReturnsStoreWithAddressAndProduct_GivenStoreIncludeProductsThe } [Fact] - public async Task ReturnsCompanyWithStoreWithIdOne_GivenCompanyIncludeFilteredStoresSpec() + public virtual async Task ReturnsCompanyWithStoreWithIdOne_GivenCompanyIncludeFilteredStoresSpec() { var result = await companyRepository.ListAsync(new CompanyIncludeFilteredStoresSpec(1)); @@ -60,7 +70,7 @@ public async Task ReturnsCompanyWithStoreWithIdOne_GivenCompanyIncludeFilteredSt } [Fact] - public async Task ReturnsStoreWithIdFrom15To30_GivenStoresByIdListSpec() + public virtual async Task ReturnsStoreWithIdFrom15To30_GivenStoresByIdListSpec() { var ids = Enumerable.Range(15, 16); var spec = new StoresByIdListSpec(ids); @@ -73,7 +83,7 @@ public async Task ReturnsStoreWithIdFrom15To30_GivenStoresByIdListSpec() } [Fact] - public async Task ReturnsSecondPageOfStoreNames_GivenStoreNamesPaginatedSpec() + public virtual async Task ReturnsSecondPageOfStoreNames_GivenStoreNamesPaginatedSpec() { int take = 10; // pagesize 10 int skip = (2 - 1) * 10; // page 2 @@ -88,7 +98,7 @@ public async Task ReturnsSecondPageOfStoreNames_GivenStoreNamesPaginatedSpec() } [Fact] - public async Task ReturnsSecondPageOfStores_GivenStoresPaginatedSpec() + public virtual async Task ReturnsSecondPageOfStores_GivenStoresPaginatedSpec() { int take = 10; // pagesize 10 int skip = (2 - 1) * 10; // page 2 @@ -103,7 +113,7 @@ public async Task ReturnsSecondPageOfStores_GivenStoresPaginatedSpec() } [Fact] - public async Task ReturnsOrderStoresByNameDescForCompanyWithId2_GivenStoresByCompanyOrderedDescByNameSpec() + public virtual async Task ReturnsOrderStoresByNameDescForCompanyWithId2_GivenStoresByCompanyOrderedDescByNameSpec() { var spec = new StoresByCompanyOrderedDescByNameSpec(2); @@ -114,7 +124,7 @@ public async Task ReturnsOrderStoresByNameDescForCompanyWithId2_GivenStoresByCom } [Fact] - public async Task ReturnsOrderStoresByNameDescThenByIdForCompanyWithId2_GivenStoresByCompanyOrderedDescByNameThenByIdSpec() + public virtual async Task ReturnsOrderStoresByNameDescThenByIdForCompanyWithId2_GivenStoresByCompanyOrderedDescByNameThenByIdSpec() { var spec = new StoresByCompanyOrderedDescByNameThenByIdSpec(2); @@ -125,7 +135,7 @@ public async Task ReturnsOrderStoresByNameDescThenByIdForCompanyWithId2_GivenSto } [Fact] - public async Task ReturnsSecondPageOfStoresForCompanyWithId2_GivenStoresByCompanyPaginatedOrderedDescByNameSpec() + public virtual async Task ReturnsSecondPageOfStoresForCompanyWithId2_GivenStoresByCompanyPaginatedOrderedDescByNameSpec() { int take = 10; // pagesize 10 int skip = (2 - 1) * 10; // page 2 @@ -140,7 +150,7 @@ public async Task ReturnsSecondPageOfStoresForCompanyWithId2_GivenStoresByCompan } [Fact] - public async Task ReturnsSecondPageOfStoresForCompanyWithId2_GivenStoresByCompanyPaginatedSpec() + public virtual async Task ReturnsSecondPageOfStoresForCompanyWithId2_GivenStoresByCompanyPaginatedSpec() { int take = 10; // pagesize 10 int skip = (2 - 1) * 10; // page 2 @@ -155,7 +165,7 @@ public async Task ReturnsSecondPageOfStoresForCompanyWithId2_GivenStoresByCompan } [Fact] - public async Task ReturnsOrderedStores_GivenStoresOrderedSpecByName() + public virtual async Task ReturnsOrderedStores_GivenStoresOrderedSpecByName() { var spec = new StoresOrderedSpecByName(); @@ -166,7 +176,7 @@ public async Task ReturnsOrderedStores_GivenStoresOrderedSpecByName() } [Fact] - public async Task ReturnsOrderedStores_GivenStoresOrderedDescendingByNameSpec() + public virtual async Task ReturnsOrderedStores_GivenStoresOrderedDescendingByNameSpec() { var spec = new StoresOrderedDescendingByNameSpec(); @@ -177,7 +187,7 @@ public async Task ReturnsOrderedStores_GivenStoresOrderedDescendingByNameSpec() } [Fact] - public async Task ReturnsStoreContainingCity1_GivenStoreIncludeProductsSpec() + public virtual async Task ReturnsStoreContainingCity1_GivenStoreIncludeProductsSpec() { var result = await storeRepository.ListAsync(new StoreSearchByNameOrCitySpec(StoreSeed.VALID_Search_City_Key)); diff --git a/Specification/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs b/Specification/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs index 3541b16f..534061fb 100644 --- a/Specification/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs +++ b/Specification/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; -using System.Text; namespace Ardalis.Specification { @@ -26,7 +25,7 @@ public static IIncludableSpecificationBuilder ThenInclude> thenIncludeExpression) where TEntity : class { - var info = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(TPreviousProperty)); + var info = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(IEnumerable)); ((List)previousBuilder.Specification.IncludeExpressions).Add(info); diff --git a/Specification/tests/Ardalis.Specification.UnitTests/BuilderTests/IncludableBuilderExtensions_ThenInclude.cs b/Specification/tests/Ardalis.Specification.UnitTests/BuilderTests/IncludableBuilderExtensions_ThenInclude.cs index 5976a7de..0c7dcaaf 100644 --- a/Specification/tests/Ardalis.Specification.UnitTests/BuilderTests/IncludableBuilderExtensions_ThenInclude.cs +++ b/Specification/tests/Ardalis.Specification.UnitTests/BuilderTests/IncludableBuilderExtensions_ThenInclude.cs @@ -1,10 +1,8 @@ -using FluentAssertions; +using System.Collections.Generic; +using System.Linq; using Ardalis.Specification.UnitTests.Fixture.Entities; using Ardalis.Specification.UnitTests.Fixture.Specs; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using FluentAssertions; using Xunit; namespace Ardalis.Specification.UnitTests @@ -23,5 +21,17 @@ public void AppendIncludeExpressionInfoToListWithTypeThenInclude_GivenThenInclud includeExpressions[1].Type.Should().Be(IncludeTypeEnum.ThenInclude); } + + [Fact] + public void ThenInclude_Append_IncludeExpressionInfo_With_EnumerablePreviousPropertyType() + { + var spec = new StoreIncludeCompanyThenStoresSpec(); + + var includeExpressions = spec.IncludeExpressions.ToList(); + + includeExpressions.Should().HaveCount(3); + + includeExpressions[2].PreviousPropertyType.Should().Be(typeof(IEnumerable)); + } } } diff --git a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Entities/Store.cs b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Entities/Store.cs index 9bd006ed..f8a58579 100644 --- a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Entities/Store.cs +++ b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Entities/Store.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; namespace Ardalis.Specification.UnitTests.Fixture.Entities { diff --git a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/StoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec.cs b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/StoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec.cs new file mode 100644 index 00000000..af181b72 --- /dev/null +++ b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/StoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec.cs @@ -0,0 +1,14 @@ +using Ardalis.Specification.UnitTests.Fixture.Entities; + +namespace Ardalis.Specification.UnitTests.Fixture.Specs +{ + public class StoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec : Specification, ISingleResultSpecification + { + public StoreByIdIncludeCompanyAndCountryAndStoresForCompanySpec(int id) + { + Query.Where(x => x.Id == id); + Query.Include(x => x.Company).ThenInclude(x => x!.Country); + Query.Include(x => x.Company).ThenInclude(x => x!.Stores).ThenInclude(x => x.Products); + } + } +} diff --git a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/SpecsForIncludeTests/StoreIncludeCompanyThenStoresSpec.cs b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/SpecsForIncludeTests/StoreIncludeCompanyThenStoresSpec.cs index a9a417c1..84772a43 100644 --- a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/SpecsForIncludeTests/StoreIncludeCompanyThenStoresSpec.cs +++ b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/SpecsForIncludeTests/StoreIncludeCompanyThenStoresSpec.cs @@ -1,7 +1,4 @@ using Ardalis.Specification.UnitTests.Fixture.Entities; -using System; -using System.Collections.Generic; -using System.Text; namespace Ardalis.Specification.UnitTests.Fixture.Specs { @@ -10,7 +7,8 @@ public class StoreIncludeCompanyThenStoresSpec : Specification public StoreIncludeCompanyThenStoresSpec() { Query.Include(x => x.Company) - .ThenInclude(x=>x!.Stores); + .ThenInclude(x => x!.Stores) + .ThenInclude(x => x.Products); } } }