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