From 963890d3d71a697257682d90b3de46edade5256d Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 10 Feb 2026 14:26:43 +1100 Subject: [PATCH] Detect conflicting default ordering across contexts Add runtime validation to Configuration.Cache to detect and reject conflicting default ordering configurations for the same entity type across different DbContext types, throwing an InvalidOperationException when ClauseMetadataList differs. Add tests to cover the new behavior (MigrationTests.ConflictingOrderingAcrossContexts_Throws), introduce SharedEntity and supporting contexts, and add ThenBy/ThenByDesc test entities and contexts. Also adjust RequireOrderingTests to match the expected ordering and bump package version to 0.3.2 in Directory.Build.props. --- src/Directory.Build.props | 2 +- src/EfOrderBy/Configuration.cs | 15 +++++++- src/Tests/DuplicateOrderByTests.cs | 22 +++++++++--- src/Tests/MigrationTests.cs | 55 ++++++++++++++++++++++++++++++ src/Tests/RequireOrderingTests.cs | 4 +-- 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5572c51..2d85462 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ enable latest true - 0.3.1 + 0.3.2 1.0.0 true true diff --git a/src/EfOrderBy/Configuration.cs b/src/EfOrderBy/Configuration.cs index fdfbe58..2880012 100644 --- a/src/EfOrderBy/Configuration.cs +++ b/src/EfOrderBy/Configuration.cs @@ -8,7 +8,20 @@ sealed class Configuration(Type elementType) static readonly ConcurrentDictionary cache = new(); internal static void Cache(Type entityType, Configuration configuration) - => cache[entityType] = configuration; + => cache.AddOrUpdate( + entityType, + configuration, + (type, existing) => + { + if (!existing.ClauseMetadataList.SequenceEqual(configuration.ClauseMetadataList)) + { + throw new InvalidOperationException( + $"Conflicting default ordering configurations for entity type '{type.Name}'. " + + $"When multiple DbContext types share the same entity, they must configure the same default ordering."); + } + + return existing; + }); internal static Configuration? TryGet(Type entityType) => cache.GetValueOrDefault(entityType); diff --git a/src/Tests/DuplicateOrderByTests.cs b/src/Tests/DuplicateOrderByTests.cs index 73468e9..709524d 100644 --- a/src/Tests/DuplicateOrderByTests.cs +++ b/src/Tests/DuplicateOrderByTests.cs @@ -132,6 +132,20 @@ public class AnotherDuplicateTestEntity public string Value { get; set; } = ""; } +public class ThenByEntity +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public int Priority { get; set; } +} + +public class ThenByDescEntity +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public int Priority { get; set; } +} + #endregion #region Test Contexts - Each with unique configuration @@ -186,20 +200,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public class OrderByWithThenByContext(DbContextOptions options) : DbContext(options) { - public DbSet Entities => Set(); + public DbSet Entities => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) => - modelBuilder.Entity() + modelBuilder.Entity() .OrderBy(_ => _.Name) .ThenBy(_ => _.Priority); // Correct usage } public class OrderByDescWithThenByDescContext(DbContextOptions options) : DbContext(options) { - public DbSet Entities => Set(); + public DbSet Entities => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) => - modelBuilder.Entity() + modelBuilder.Entity() .OrderByDescending(_ => _.Name) .ThenByDescending(_ => _.Priority); // Correct usage } diff --git a/src/Tests/MigrationTests.cs b/src/Tests/MigrationTests.cs index 32a2e33..9b7729f 100644 --- a/src/Tests/MigrationTests.cs +++ b/src/Tests/MigrationTests.cs @@ -105,4 +105,59 @@ public void DesignTimeModelHasNoConfigurationAnnotations() Assert.That(designTimeModel.FindAnnotation("DefaultOrderBy:InterceptorRegistered"), Is.Null); Assert.That(designTimeModel.FindAnnotation("DefaultOrderBy:IndexCreationDisabled"), Is.Null); } + + [Test] + public void ConflictingOrderingAcrossContexts_Throws() + { + // First context configures SharedEntity with OrderBy(Name) + var options1 = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Database=Test;Trusted_Connection=True") + .UseDefaultOrderBy() + .Options; + + using (var context = new ContextWithNameOrdering(options1)) + { + _ = context.Model; + } + + // Second context configures SharedEntity with OrderByDescending(Value) - should throw + var options2 = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Database=Test;Trusted_Connection=True") + .UseDefaultOrderBy() + .Options; + + var exception = Assert.Throws(() => + { + using var context = new ContextWithValueOrdering(options2); + _ = context.Model; + }); + + Assert.That(exception!.Message, Does.Contain("SharedEntity")); + Assert.That(exception.Message, Does.Contain("Conflicting")); + } +} + +public class SharedEntity +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Value { get; set; } = ""; +} + +class ContextWithNameOrdering(DbContextOptions options) : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) => + modelBuilder.Entity() + .OrderBy(_ => _.Name); +} + +class ContextWithValueOrdering(DbContextOptions options) : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) => + modelBuilder.Entity() + .OrderByDescending(_ => _.Value); } diff --git a/src/Tests/RequireOrderingTests.cs b/src/Tests/RequireOrderingTests.cs index 907b619..4cdeba9 100644 --- a/src/Tests/RequireOrderingTests.cs +++ b/src/Tests/RequireOrderingTests.cs @@ -113,9 +113,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // All entities have ordering configured + // All entities have ordering configured (must match TestDbContext's ordering) modelBuilder.Entity() - .OrderBy(_ => _.CreatedDate); + .OrderByDescending(_ => _.CreatedDate); } }