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