diff --git a/src/Ardalis.Specification.EntityFramework6/Ardalis.Specification.EntityFramework6.csproj b/src/Ardalis.Specification.EntityFramework6/Ardalis.Specification.EntityFramework6.csproj index 70c042be..fcdd91d5 100644 --- a/src/Ardalis.Specification.EntityFramework6/Ardalis.Specification.EntityFramework6.csproj +++ b/src/Ardalis.Specification.EntityFramework6/Ardalis.Specification.EntityFramework6.csproj @@ -11,7 +11,7 @@ EF6 plugin package to Ardalis.Specification containing EF6 evaluator and abstract repository. EF6 plugin package to Ardalis.Specification containing EF6 evaluator and abstract repository. - 9.0.1 + 9.1.0 spec;specification;repository;ddd;ef;ef6;entity framework The change log and breaking changes are listed here. diff --git a/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj b/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj index 8cc3c83d..236fc65d 100644 --- a/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj +++ b/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj @@ -10,7 +10,7 @@ EF Core plugin package to Ardalis.Specification containing EF Core evaluator and abstract repository. EF Core plugin package to Ardalis.Specification containing EF Core evaluator and abstract repository. - 9.0.1 + 9.1.0 spec;specification;repository;ddd;ef;ef core;entity framework;entity framework core The change log and breaking changes are listed here. diff --git a/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs index ce8b0ba4..87925228 100644 --- a/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs +++ b/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/IncludeEvaluator.cs @@ -41,7 +41,6 @@ private IncludeEvaluator() { } /// public IQueryable GetQuery(IQueryable query, ISpecification specification) where T : class { - Type? previousReturnType = null; foreach (var includeExpression in specification.IncludeExpressions) { var lambdaExpr = includeExpression.LambdaExpression; @@ -49,14 +48,12 @@ public IQueryable GetQuery(IQueryable query, ISpecification specific if (includeExpression.Type == IncludeTypeEnum.Include) { 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 (includeExpression.Type == IncludeTypeEnum.ThenInclude) { - var key = new CacheKey(typeof(T), lambdaExpr.ReturnType, previousReturnType); - previousReturnType = lambdaExpr.ReturnType; + var key = new CacheKey(typeof(T), lambdaExpr.ReturnType, includeExpression.PreviousPropertyType); var include = _cache.GetOrAdd(key, CreateThenIncludeDelegate); query = (IQueryable)include(query, lambdaExpr); } @@ -104,7 +101,7 @@ private static Func CreateThenIncludeD private static bool IsGenericEnumerable(Type type, out Type propertyType) { - if (type.IsGenericType && typeof(IEnumerable).IsAssignableFrom(type)) + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { propertyType = type.GenericTypeArguments[0]; return true; diff --git a/src/Ardalis.Specification/Ardalis.Specification.csproj b/src/Ardalis.Specification/Ardalis.Specification.csproj index 6037ca8c..3e217dc0 100644 --- a/src/Ardalis.Specification/Ardalis.Specification.csproj +++ b/src/Ardalis.Specification/Ardalis.Specification.csproj @@ -10,7 +10,7 @@ A simple package with a base Specification class, for use in creating queries that work with Repository types. A simple package with a base Specification class, for use in creating queries that work with Repository types. - 9.0.1 + 9.1.0 spec;specification;repository;ddd The change log and breaking changes are listed here. diff --git a/src/Ardalis.Specification/Builders/Builder_Include.cs b/src/Ardalis.Specification/Builders/Builder_Include.cs index ee7ec96e..918ef314 100644 --- a/src/Ardalis.Specification/Builders/Builder_Include.cs +++ b/src/Ardalis.Specification/Builders/Builder_Include.cs @@ -101,7 +101,7 @@ public static IIncludableSpecificationBuilder Include Include ThenI { if (condition && !Specification.IsChainDiscarded) { - var expr = new IncludeExpressionInfo(navigationSelector, IncludeTypeEnum.ThenInclude); + var expr = new IncludeExpressionInfo(navigationSelector, typeof(TPreviousProperty)); builder.Specification.Add(expr); } else @@ -228,7 +228,7 @@ public static IIncludableSpecificationBuilder ThenInclude.IsChainDiscarded) { - var expr = new IncludeExpressionInfo(navigationSelector, IncludeTypeEnum.ThenInclude); + var expr = new IncludeExpressionInfo(navigationSelector, typeof(TPreviousProperty)); builder.Specification.Add(expr); } else @@ -275,7 +275,7 @@ public static IIncludableSpecificationBuilder ThenI { if (condition && !Specification.IsChainDiscarded) { - var expr = new IncludeExpressionInfo(navigationSelector, IncludeTypeEnum.ThenInclude); + var expr = new IncludeExpressionInfo(navigationSelector, typeof(IEnumerable)); builder.Specification.Add(expr); } else @@ -320,7 +320,7 @@ public static IIncludableSpecificationBuilder ThenInclude.IsChainDiscarded) { - var expr = new IncludeExpressionInfo(navigationSelector, IncludeTypeEnum.ThenInclude); + var expr = new IncludeExpressionInfo(navigationSelector, typeof(IEnumerable)); builder.Specification.Add(expr); } else diff --git a/src/Ardalis.Specification/Expressions/IncludeExpressionInfo.cs b/src/Ardalis.Specification/Expressions/IncludeExpressionInfo.cs index 0a5db08b..12816151 100644 --- a/src/Ardalis.Specification/Expressions/IncludeExpressionInfo.cs +++ b/src/Ardalis.Specification/Expressions/IncludeExpressionInfo.cs @@ -11,16 +11,32 @@ public class IncludeExpressionInfo /// public LambdaExpression LambdaExpression { get; } + /// + /// The type of the previously included entity. + /// + public Type? PreviousPropertyType { get; } + /// /// The include type. /// public IncludeTypeEnum Type { get; } - public IncludeExpressionInfo(LambdaExpression expression, IncludeTypeEnum includeType) + public IncludeExpressionInfo(LambdaExpression expression) + { + _ = expression ?? throw new ArgumentNullException(nameof(expression)); + + LambdaExpression = expression; + PreviousPropertyType = null; + Type = IncludeTypeEnum.Include; + } + + public IncludeExpressionInfo(LambdaExpression expression, Type previousPropertyType) { _ = expression ?? throw new ArgumentNullException(nameof(expression)); + _ = previousPropertyType ?? throw new ArgumentNullException(nameof(previousPropertyType)); LambdaExpression = expression; - Type = includeType; + PreviousPropertyType = previousPropertyType; + Type = IncludeTypeEnum.ThenInclude; } } diff --git a/src/Ardalis.Specification/ISpecification.cs b/src/Ardalis.Specification/ISpecification.cs index 5471a7aa..9c6a3d78 100644 --- a/src/Ardalis.Specification/ISpecification.cs +++ b/src/Ardalis.Specification/ISpecification.cs @@ -157,4 +157,6 @@ public interface ISpecification /// The entity to be validated /// bool IsSatisfiedBy(T entity); + + internal void CopyTo(Specification otherSpec); } diff --git a/src/Ardalis.Specification/Specification.cs b/src/Ardalis.Specification/Specification.cs index 882b43fa..8536a007 100644 --- a/src/Ardalis.Specification/Specification.cs +++ b/src/Ardalis.Specification/Specification.cs @@ -153,4 +153,52 @@ public virtual bool IsSatisfiedBy(T entity) var validator = Validator; return validator.IsValid(entity, this); } + + void ISpecification.CopyTo(Specification otherSpec) + { + otherSpec.PostProcessingAction = PostProcessingAction; + otherSpec.QueryTag = QueryTag; + otherSpec.CacheKey = CacheKey; + otherSpec.Take = Take; + otherSpec.Skip = Skip; + otherSpec.IgnoreQueryFilters = IgnoreQueryFilters; + otherSpec.IgnoreAutoIncludes = IgnoreAutoIncludes; + otherSpec.AsSplitQuery = AsSplitQuery; + otherSpec.AsNoTracking = AsNoTracking; + otherSpec.AsTracking = AsTracking; + otherSpec.AsNoTrackingWithIdentityResolution = AsNoTrackingWithIdentityResolution; + + // The expression containers are immutable, having the same instance is fine. + // We'll just create new collections. + + if (_whereExpressions is not null) + { + otherSpec._whereExpressions = _whereExpressions.ToList(); + } + + if (_includeExpressions is not null) + { + otherSpec._includeExpressions = _includeExpressions.ToList(); + } + + if (_includeStrings is not null) + { + otherSpec._includeStrings = _includeStrings.ToList(); + } + + if (_orderExpressions is not null) + { + otherSpec._orderExpressions = _orderExpressions.ToList(); + } + + if (_searchExpressions is not null) + { + otherSpec._searchExpressions = _searchExpressions.ToList(); + } + + if (_items is not null) + { + otherSpec._items = new Dictionary(_items); + } + } } diff --git a/src/Ardalis.Specification/SpecificationExtensions.cs b/src/Ardalis.Specification/SpecificationExtensions.cs new file mode 100644 index 00000000..7b12b571 --- /dev/null +++ b/src/Ardalis.Specification/SpecificationExtensions.cs @@ -0,0 +1,14 @@ +namespace Ardalis.Specification; + +public static class SpecificationExtensions +{ + public static Specification WithProjectionOf(this ISpecification source, ISpecification projectionSpec) + { + var newSpec = new Specification(); + source.CopyTo(newSpec); + newSpec.Selector = projectionSpec.Selector; + newSpec.SelectorMany = projectionSpec.SelectorMany; + newSpec.PostProcessingAction = projectionSpec.PostProcessingAction; + return newSpec; + } +} diff --git a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs index b7214f15..57c5e90b 100644 --- a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs +++ b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Evaluators/IncludeEvaluatorTests.cs @@ -29,6 +29,26 @@ public void QueriesMatch_GivenIncludeExpressions() actual.Should().Be(expected); } + [Fact] + public void QueriesMatch_GivenInheritanceModel() + { + var spec = new Specification(); + spec.Query + .Include(x => x.BarChildren) + .ThenInclude(x => (x as BarDerived)!.BarDerivedInfo); + + var actual = _evaluator + .GetQuery(DbContext.Bars, spec) + .ToQueryString(); + + var expected = DbContext.Bars + .Include(x => x.BarChildren) + .ThenInclude(x => (x as BarDerived)!.BarDerivedInfo) + .ToQueryString(); + + actual.Should().Be(expected); + } + [Fact] public void QueriesMatch_GivenThenIncludeWithVariousNavigationCollectionTypes() { diff --git a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Fixture/Data/Bar.cs b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Fixture/Data/Bar.cs new file mode 100644 index 00000000..2aba8c99 --- /dev/null +++ b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Fixture/Data/Bar.cs @@ -0,0 +1,31 @@ +namespace Tests.Fixture; + +public class Bar +{ + public int Id { get; set; } + public string? Dummy { get; set; } + + private readonly List _barChildren = []; + public IReadOnlyCollection BarChildren => _barChildren.AsReadOnly(); +} + +public class BarChild +{ + public int Id { get; set; } + public string? Dummy { get; set; } + + public int BarId { get; set; } + public Bar Bar { get; set; } = default!; +} + +public class BarDerived : BarChild +{ + public int BarDerivedInfoId { get; set; } + public BarDerivedInfo BarDerivedInfo { get; set; } = default!; +} + +public class BarDerivedInfo +{ + public int Id { get; set; } + public string? Name { get; set; } +} diff --git a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Fixture/TestDbContext.cs b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Fixture/TestDbContext.cs index 7595188c..1313beb8 100644 --- a/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Fixture/TestDbContext.cs +++ b/tests/Ardalis.Specification.EntityFrameworkCore.Tests/Fixture/TestDbContext.cs @@ -2,6 +2,7 @@ public class TestDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Bars => Set(); public DbSet Foos => Set(); public DbSet Countries => Set(); public DbSet Companies => Set(); @@ -23,5 +24,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasQueryFilter(x => !x.IsDeleted); + + modelBuilder.Entity() + .HasBaseType(); } } diff --git a/tests/Ardalis.Specification.Tests/Expressions/IncludeExpressionInfoTests.cs b/tests/Ardalis.Specification.Tests/Expressions/IncludeExpressionInfoTests.cs index 9ecab725..949ac89a 100644 --- a/tests/Ardalis.Specification.Tests/Expressions/IncludeExpressionInfoTests.cs +++ b/tests/Ardalis.Specification.Tests/Expressions/IncludeExpressionInfoTests.cs @@ -9,18 +9,44 @@ public record City(int Id); [Fact] public void Constructor_ThrowsArgumentNullException_GivenNullForLambdaExpression() { - var sut = () => new IncludeExpressionInfo(null!, IncludeTypeEnum.Include); + var sut = () => new IncludeExpressionInfo(null!); sut.Should().Throw().WithParameterName("expression"); + + + sut = () => new IncludeExpressionInfo(null!, typeof(Customer)); + + sut.Should().Throw().WithParameterName("expression"); + } + + [Fact] + public void Constructor_ThrowsArgumentNullException_GivenNullForPreviousPropertyType() + { + Expression> expr = x => x.Address; + var sut = () => new IncludeExpressionInfo(expr, null!); + + sut.Should().Throw().WithParameterName("previousPropertyType"); } [Fact] public void Constructor_GivenIncludeExpression() { Expression> expr = x => x.Address; - var sut = new IncludeExpressionInfo(expr, IncludeTypeEnum.Include); + var sut = new IncludeExpressionInfo(expr); sut.Type.Should().Be(IncludeTypeEnum.Include); sut.LambdaExpression.Should().Be(expr); } + + [Fact] + public void Constructor_GivenThenIncludeExpressionAndPreviousPropertyType() + { + Expression> expr = x => x.City; + var previousPropertyType = typeof(Customer); + var sut = new IncludeExpressionInfo(expr, previousPropertyType); + + sut.Type.Should().Be(IncludeTypeEnum.ThenInclude); + sut.LambdaExpression.Should().Be(expr); + sut.PreviousPropertyType.Should().Be(previousPropertyType); + } } diff --git a/tests/Ardalis.Specification.Tests/SpecificationExtensionsTests.cs b/tests/Ardalis.Specification.Tests/SpecificationExtensionsTests.cs new file mode 100644 index 00000000..0780aa8d --- /dev/null +++ b/tests/Ardalis.Specification.Tests/SpecificationExtensionsTests.cs @@ -0,0 +1,68 @@ +namespace Tests; + +public class SpecificationExtensionsTests +{ + private record Address(int Id, string Street); + private record Person(int Id, string Name, List Names, Address Address); + + [Fact] + public void WithProjectionOf_ReturnsCopyWithProjection() + { + var spec = new Specification(); + spec.Items.Add("test", "test"); + spec.Query + .Where(x => x.Name == "test") + .Include(x => x.Address) + .Include("Address") + .OrderBy(x => x.Id) + .Search(x => x.Name, "test") + .Take(2) + .Skip(3) + .WithCacheKey("testKey") + .IgnoreQueryFilters() + .IgnoreQueryFilters() + .AsSplitQuery() + .AsNoTracking() + .TagWith("testQuery") + .PostProcessingAction(x => x.Where(x => x.Id > 0)); + + var projectionSpec = new Specification(); + projectionSpec.Query.Select(x => x.Name); + projectionSpec.Query.SelectMany(x => x.Names); + projectionSpec.Query.PostProcessingAction(x => x.Select(x => x + "A")); + + var newSpec = spec.WithProjectionOf(projectionSpec); + + newSpec.Items.Should().NotBeSameAs(spec.Items); + newSpec.Items.Should().BeEquivalentTo(spec.Items); + + newSpec.WhereExpressions.Should().NotBeSameAs(spec.WhereExpressions); + newSpec.WhereExpressions.Should().Equal(spec.WhereExpressions); + + newSpec.IncludeExpressions.Should().NotBeSameAs(spec.IncludeExpressions); + newSpec.IncludeExpressions.Should().Equal(spec.IncludeExpressions); + + newSpec.IncludeStrings.Should().NotBeSameAs(spec.IncludeStrings); + newSpec.IncludeStrings.Should().Equal(spec.IncludeStrings); + + newSpec.OrderExpressions.Should().NotBeSameAs(spec.OrderExpressions); + newSpec.OrderExpressions.Should().Equal(spec.OrderExpressions); + + newSpec.SearchCriterias.Should().NotBeSameAs(spec.SearchCriterias); + newSpec.SearchCriterias.Should().Equal(spec.SearchCriterias); + + newSpec.Take.Should().Be(spec.Take); + newSpec.Skip.Should().Be(spec.Skip); + newSpec.CacheKey.Should().Be(spec.CacheKey); + newSpec.IgnoreQueryFilters.Should().Be(spec.IgnoreQueryFilters); + newSpec.IgnoreAutoIncludes.Should().Be(spec.IgnoreAutoIncludes); + newSpec.AsSplitQuery.Should().Be(spec.AsSplitQuery); + newSpec.AsNoTracking.Should().Be(spec.AsNoTracking); + newSpec.AsNoTrackingWithIdentityResolution.Should().Be(spec.AsNoTrackingWithIdentityResolution); + newSpec.AsTracking.Should().Be(spec.AsTracking); + newSpec.QueryTag.Should().Be(spec.QueryTag); + + newSpec.PostProcessingAction.Should().BeSameAs(projectionSpec.PostProcessingAction); + ((Specification)newSpec).PostProcessingAction.Should().BeSameAs(spec.PostProcessingAction); + } +}