From 5debf9c30f6522c955540a48d15f9e416237fd0b Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sun, 15 Jun 2025 15:24:10 +0200 Subject: [PATCH 1/5] Add AsNoTrackingEvaluator tests. --- .../Evaluators/AsNoTrackingEvaluatorTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/AsNoTrackingEvaluatorTests.cs diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/AsNoTrackingEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/AsNoTrackingEvaluatorTests.cs new file mode 100644 index 00000000..669440cc --- /dev/null +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/AsNoTrackingEvaluatorTests.cs @@ -0,0 +1,27 @@ +using Tests.FixtureNew; + +namespace Tests.Evaluators; + +[Collection("SharedCollection")] +public class AsNoTrackingEvaluatorTests(TestFactory factory) : IntegrationTest(factory) +{ + private static readonly AsNoTrackingEvaluator _evaluator = AsNoTrackingEvaluator.Instance; + + [Fact] + public void Applies_GivenAsNoTracking() + { + var spec = new Specification(); + spec.Query.AsNoTracking(); + + var actual = _evaluator.GetQuery(DbContext.Countries, spec) + .Expression + .ToString(); + + var expected = DbContext.Countries + .AsNoTracking() + .AsQueryable().Expression + .ToString(); + + actual.Should().Be(expected); + } +} From d7794566b12834adc2b4d6b006f4615d835bac3f Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sun, 15 Jun 2025 15:25:12 +0200 Subject: [PATCH 2/5] Add IncludeEvaluator tests. --- .../Evaluators/IncludeEvaluatorTests.cs | 114 ++++++++++++++++++ .../FixtureNew/IntegrationTest.cs | 22 ++++ 2 files changed, 136 insertions(+) create mode 100644 tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs new file mode 100644 index 00000000..d666298e --- /dev/null +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs @@ -0,0 +1,114 @@ +using System.Data.Entity; +using Tests.FixtureNew; + +namespace Tests.Evaluators; + +[Collection("SharedCollection")] +public class IncludeEvaluatorTests(TestFactory factory) : IntegrationTest(factory) +{ + private static readonly IncludeEvaluator _evaluator = IncludeEvaluator.Instance; + + [Fact] + public void QueriesMatch_GivenNoIncludeExpression() + { + var spec = new Specification(); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies.AsQueryable(); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + [Fact] + public void QueriesMatch_GivenNoIncludeExpression_WithAutoOneToOne() + { + var spec = new Specification(); + + var actual = _evaluator.GetQuery(DbContext.Stores, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Stores.AsQueryable(); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + [Fact] + public void QueriesMatch_GivenSingleIncludeExpression_WithReferenceNavigation() + { + var spec = new Specification(); + spec.Query + .Include(x => x.Country); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies + .Include(x => x.Country); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + [Fact] + public void QueriesMatch_GivenSingleIncludeExpression_WithCollectionNavigation() + { + var spec = new Specification(); + spec.Query + .Include(x => x.Stores); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies + .Include(x => x.Stores); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + [Fact] + public void QueriesMatch_GivenMultipleIncludeExpression() + { + var spec = new Specification(); + spec.Query + .Include(x => x.Country) + .Include(x => x.Stores); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies + .Include(x => x.Country) + .Include(x => x.Stores); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + // TODO: Found out that EF6 include evaluator fails for multiple include chains. [Fati Iseni, 15/06/2025] + //[Fact] + //public void QueriesMatch_GivenThenIncludeExpression() + //{ + // var spec = new Specification(); + // spec.Query + // .Include(x => x.Products) + // .ThenInclude(x => x.Images) + // .Include(x => x.Company) + // .ThenInclude(x => x.Country); + + // var actual = _evaluator.GetQuery(DbContext.Stores, spec); + // var actualSql = GetQueryString(DbContext, actual); + + // // EF6 doe't support ThenInclude, it uses string-based includes + // var expected = DbContext.Stores + // .Include($"{nameof(Store.Products)}.{nameof(Product.Images)}") + // .Include($"{nameof(Store.Company)}.{nameof(Company.Country)}"); + // var expectedSql = GetQueryString(DbContext, expected); + + // actualSql.Should().Be(expectedSql); + //} +} diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/IntegrationTest.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/IntegrationTest.cs index e669deb8..b51cd64b 100644 --- a/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/IntegrationTest.cs +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/IntegrationTest.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.IO; namespace Tests.FixtureNew; @@ -12,9 +13,30 @@ public IntegrationTest(TestFactory testFactory) _testFactory = testFactory; } + public static string GetQueryString(TestDbContext dbContext, IQueryable queryable) + { + // The EF6 doesn't support ToQueryString, so we need to log the SQL manually + var writer = new StringWriter(); + dbContext.Database.Log = writer.Write; + _ = queryable.ToList(); // Execute the query to log the SQL + var sql = writer.ToString(); + + // Remove metadata lines (connection open/close, timestamps, execution comments) + var filteredLines = sql.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries) + .Where(line => + !line.StartsWith("Opened connection") && + !line.StartsWith("Closed connection") && + !line.StartsWith("-- Executing") && + !line.StartsWith("-- Completed") + ); + return string.Join(Environment.NewLine, filteredLines).Trim(); + } + public Task InitializeAsync() { DbContext = new TestDbContext(_testFactory.ConnectionString); + // On first access, there are additional queries and is skewing our tests. + _ = DbContext.Countries.Any(); return Task.CompletedTask; } From dbe24641b1a403b08fc83cd1957491e2ecd63167 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sun, 15 Jun 2025 15:42:26 +0200 Subject: [PATCH 3/5] Add OrderEvaluator tests --- .../Evaluators/OrderEvaluatorTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/OrderEvaluatorTests.cs diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/OrderEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/OrderEvaluatorTests.cs new file mode 100644 index 00000000..af0c7eb4 --- /dev/null +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/OrderEvaluatorTests.cs @@ -0,0 +1,46 @@ +using Tests.FixtureNew; + +namespace Tests.Evaluators; + +[Collection("SharedCollection")] +public class OrderEvaluatorTests(TestFactory factory) : IntegrationTest(factory) +{ + private static readonly Ardalis.Specification.EntityFramework6.OrderEvaluator _evaluator = + Ardalis.Specification.EntityFramework6.OrderEvaluator.Instance; + + [Fact] + public void QueriesMatch_GivenOrder() + { + var spec = new Specification(); + spec.Query + .OrderBy(x => x.Id); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies + .OrderBy(x => x.Id); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + [Fact] + public void QueriesMatch_GivenOrderChain() + { + var spec = new Specification(); + spec.Query + .OrderBy(x => x.Id) + .ThenBy(x => x.Name); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies + .OrderBy(x => x.Id) + .ThenBy(x => x.Name); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } +} From f8b3e9d2fe69259b258a9f7cab5f4ec4d8f8ebad Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sun, 15 Jun 2025 15:42:37 +0200 Subject: [PATCH 4/5] Add SearchEvaluator tests. --- .../Evaluators/SearchEvaluatorTests.cs | 77 +++++++++++++++++++ .../FixtureNew/Data/Address.cs | 4 +- .../FixtureNew/Data/Company.cs | 4 +- 3 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs new file mode 100644 index 00000000..21864d65 --- /dev/null +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs @@ -0,0 +1,77 @@ +using System.Data.Entity; +using Tests.FixtureNew; + +namespace Tests.Evaluators; + +[Collection("SharedCollection")] +public class SearchEvaluatorTests(TestFactory factory) : IntegrationTest(factory) +{ + private static readonly SearchEvaluator _evaluator = SearchEvaluator.Instance; + + [Fact] + public void QueriesMatch_GivenNoSearch() + { + var spec = new Specification(); + spec.Query + .Where(x => x.Id > 0); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies.AsQueryable(); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + //// TODO: Not producing the same query, it's not parameterized and no null check [Fati Iseni, 15/06/2025] + //[Fact] + //public void QueriesMatch_GivenSingleSearch() + //{ + // var storeTerm = "ab1"; + + // var spec = new Specification(); + // spec.Query + // .Where(x => x.Id > 0) + // .Search(x => x.Name, $"%{storeTerm}%"); + + // var actual = _evaluator.GetQuery(DbContext.Companies, spec); + // var actualSql = GetQueryString(DbContext, actual); + + // var expected = DbContext.Companies + // .Where(x => DbFunctions.Like(x.Name, "%" + storeTerm + "%")); + // var expectedSql = GetQueryString(DbContext, expected); + + // actualSql.Should().Be(expectedSql); + //} + + //// TODO: Not producing the same query, it's not parameterized and no null check [Fati Iseni, 15/06/2025] + //[Fact] + //public void QueriesMatch_GivenMultipleSearch() + //{ + // var storeTerm = "ab1"; + // var companyTerm = "ab2"; + // var countryTerm = "ab3"; + // var streetTerm = "ab4"; + + // var spec = new Specification(); + // spec.Query + // .Where(x => x.Id > 0) + // .Search(x => x.Name, $"%{storeTerm}%") + // .Search(x => x.Company.Name, $"%{companyTerm}%") + // .Search(x => x.Company.Country.Name, $"%{countryTerm}%", 3) + // .Search(x => x.Address.Street, $"%{streetTerm}%", 2); + + // var actual = _evaluator.GetQuery(DbContext.Stores, spec); + // var actualSql = GetQueryString(DbContext, actual); + + // var expected = DbContext.Stores + // .Where(x => DbFunctions.Like(x.Name, "%" + storeTerm + "%") + // || DbFunctions.Like(x.Company.Name, "%" + storeTerm + "%")) + // .Where(x => DbFunctions.Like(x.Address.Street, "%" + storeTerm + "%")) + // .Where(x => DbFunctions.Like(x.Company.Country.Name, "%" + storeTerm + "%")); + // var expectedSql = GetQueryString(DbContext, expected); + + // actualSql.Should().Be(expectedSql); + //} +} diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Address.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Address.cs index e644cac6..cdbd0f2f 100644 --- a/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Address.cs +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Address.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; - -namespace Tests.FixtureNew; +namespace Tests.FixtureNew; public record Address { diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Company.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Company.cs index b78353b0..bd6cef7c 100644 --- a/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Company.cs +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/FixtureNew/Data/Company.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; - -namespace Tests.FixtureNew; +namespace Tests.FixtureNew; public record Company { From 6d4dfe75b15d8f746ddc4bb8ea5c6f2161efb936 Mon Sep 17 00:00:00 2001 From: Fati Iseni Date: Sun, 15 Jun 2025 15:51:31 +0200 Subject: [PATCH 5/5] Add Skip attributes instead of commented tests. --- .../Evaluators/IncludeEvaluatorTests.cs | 44 ++++----- .../Evaluators/SearchEvaluatorTests.cs | 98 +++++++++---------- 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs index d666298e..f1bf239f 100644 --- a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/IncludeEvaluatorTests.cs @@ -89,26 +89,26 @@ public void QueriesMatch_GivenMultipleIncludeExpression() actualSql.Should().Be(expectedSql); } - // TODO: Found out that EF6 include evaluator fails for multiple include chains. [Fati Iseni, 15/06/2025] - //[Fact] - //public void QueriesMatch_GivenThenIncludeExpression() - //{ - // var spec = new Specification(); - // spec.Query - // .Include(x => x.Products) - // .ThenInclude(x => x.Images) - // .Include(x => x.Company) - // .ThenInclude(x => x.Country); - - // var actual = _evaluator.GetQuery(DbContext.Stores, spec); - // var actualSql = GetQueryString(DbContext, actual); - - // // EF6 doe't support ThenInclude, it uses string-based includes - // var expected = DbContext.Stores - // .Include($"{nameof(Store.Products)}.{nameof(Product.Images)}") - // .Include($"{nameof(Store.Company)}.{nameof(Company.Country)}"); - // var expectedSql = GetQueryString(DbContext, expected); - - // actualSql.Should().Be(expectedSql); - //} + + [Fact(Skip = "EF6 include evaluator fails for multiple include chains [Fati Iseni, 15/06/2025]")] + public void QueriesMatch_GivenThenIncludeExpression() + { + var spec = new Specification(); + spec.Query + .Include(x => x.Products) + .ThenInclude(x => x.Images) + .Include(x => x.Company) + .ThenInclude(x => x.Country); + + var actual = _evaluator.GetQuery(DbContext.Stores, spec); + var actualSql = GetQueryString(DbContext, actual); + + // EF6 doe't support ThenInclude, it uses string-based includes + var expected = DbContext.Stores + .Include($"{nameof(Store.Products)}.{nameof(Product.Images)}") + .Include($"{nameof(Store.Company)}.{nameof(Company.Country)}"); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } } diff --git a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs index 21864d65..741c98ae 100644 --- a/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs @@ -24,54 +24,52 @@ public void QueriesMatch_GivenNoSearch() actualSql.Should().Be(expectedSql); } - //// TODO: Not producing the same query, it's not parameterized and no null check [Fati Iseni, 15/06/2025] - //[Fact] - //public void QueriesMatch_GivenSingleSearch() - //{ - // var storeTerm = "ab1"; - - // var spec = new Specification(); - // spec.Query - // .Where(x => x.Id > 0) - // .Search(x => x.Name, $"%{storeTerm}%"); - - // var actual = _evaluator.GetQuery(DbContext.Companies, spec); - // var actualSql = GetQueryString(DbContext, actual); - - // var expected = DbContext.Companies - // .Where(x => DbFunctions.Like(x.Name, "%" + storeTerm + "%")); - // var expectedSql = GetQueryString(DbContext, expected); - - // actualSql.Should().Be(expectedSql); - //} - - //// TODO: Not producing the same query, it's not parameterized and no null check [Fati Iseni, 15/06/2025] - //[Fact] - //public void QueriesMatch_GivenMultipleSearch() - //{ - // var storeTerm = "ab1"; - // var companyTerm = "ab2"; - // var countryTerm = "ab3"; - // var streetTerm = "ab4"; - - // var spec = new Specification(); - // spec.Query - // .Where(x => x.Id > 0) - // .Search(x => x.Name, $"%{storeTerm}%") - // .Search(x => x.Company.Name, $"%{companyTerm}%") - // .Search(x => x.Company.Country.Name, $"%{countryTerm}%", 3) - // .Search(x => x.Address.Street, $"%{streetTerm}%", 2); - - // var actual = _evaluator.GetQuery(DbContext.Stores, spec); - // var actualSql = GetQueryString(DbContext, actual); - - // var expected = DbContext.Stores - // .Where(x => DbFunctions.Like(x.Name, "%" + storeTerm + "%") - // || DbFunctions.Like(x.Company.Name, "%" + storeTerm + "%")) - // .Where(x => DbFunctions.Like(x.Address.Street, "%" + storeTerm + "%")) - // .Where(x => DbFunctions.Like(x.Company.Country.Name, "%" + storeTerm + "%")); - // var expectedSql = GetQueryString(DbContext, expected); - - // actualSql.Should().Be(expectedSql); - //} + [Fact(Skip = "Not producing the same query, it's not parameterized and no null check [Fati Iseni, 15/06/2025]")] + public void QueriesMatch_GivenSingleSearch() + { + var storeTerm = "ab1"; + + var spec = new Specification(); + spec.Query + .Where(x => x.Id > 0) + .Search(x => x.Name, $"%{storeTerm}%"); + + var actual = _evaluator.GetQuery(DbContext.Companies, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Companies + .Where(x => DbFunctions.Like(x.Name, "%" + storeTerm + "%")); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } + + [Fact(Skip = "Not producing the same query, it's not parameterized and no null check [Fati Iseni, 15/06/2025]")] + public void QueriesMatch_GivenMultipleSearch() + { + var storeTerm = "ab1"; + var companyTerm = "ab2"; + var countryTerm = "ab3"; + var streetTerm = "ab4"; + + var spec = new Specification(); + spec.Query + .Where(x => x.Id > 0) + .Search(x => x.Name, $"%{storeTerm}%") + .Search(x => x.Company.Name, $"%{companyTerm}%") + .Search(x => x.Company.Country.Name, $"%{countryTerm}%", 3) + .Search(x => x.Address.Street, $"%{streetTerm}%", 2); + + var actual = _evaluator.GetQuery(DbContext.Stores, spec); + var actualSql = GetQueryString(DbContext, actual); + + var expected = DbContext.Stores + .Where(x => DbFunctions.Like(x.Name, "%" + storeTerm + "%") + || DbFunctions.Like(x.Company.Name, "%" + storeTerm + "%")) + .Where(x => DbFunctions.Like(x.Address.Street, "%" + storeTerm + "%")) + .Where(x => DbFunctions.Like(x.Company.Country.Name, "%" + storeTerm + "%")); + var expectedSql = GetQueryString(DbContext, expected); + + actualSql.Should().Be(expectedSql); + } }