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); + } +} 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..f1bf239f --- /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); + } + + + [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/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); + } +} 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..741c98ae --- /dev/null +++ b/tests/Ardalis.Specification.EntityFramework6.Tests/Evaluators/SearchEvaluatorTests.cs @@ -0,0 +1,75 @@ +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); + } + + [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); + } +} 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 { 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; }