diff --git a/readme.md b/readme.md index 4cdf738..9a90bdb 100644 --- a/readme.md +++ b/readme.md @@ -134,6 +134,19 @@ The index: This eliminates the need to manually create indexes that match the ordering configuration. +### Custom Index Names + +The auto-generated index name must not exceed 128 characters (SQL Server limit). If an entity name is too long, use `WithIndexName` to specify a custom index name: + +```cs +builder.Entity() + .OrderBy(_ => _.Name) + .WithIndexName("IX_LongEntity_Order"); +``` + +If the auto-generated name exceeds 128 characters, an `InvalidOperationException` is thrown with a message suggesting to use `WithIndexName()`. + + ## Require Ordering for All Entities Enable validation mode to ensure all entities have default ordering configured: diff --git a/src/EfOrderBy/OrderByBuilder.cs b/src/EfOrderBy/OrderByBuilder.cs index c5367f5..44e9105 100644 --- a/src/EfOrderBy/OrderByBuilder.cs +++ b/src/EfOrderBy/OrderByBuilder.cs @@ -6,8 +6,11 @@ namespace EfOrderBy; public sealed class OrderByBuilder where TEntity : class { + const int MaxIndexNameLength = 128; + Configuration configuration; EntityTypeBuilder entityBuilder; + string? customIndexName; internal OrderByBuilder(EntityTypeBuilder builder, PropertyInfo propertyInfo, bool descending) { @@ -43,12 +46,41 @@ public OrderByBuilder ThenByDescending(Expression + /// Specifies a custom index name for the default ordering index. + /// Use this when the auto-generated index name would exceed the 128 character limit. + /// + public OrderByBuilder WithIndexName(string indexName) + { + if (string.IsNullOrWhiteSpace(indexName)) + { + throw new ArgumentException("Index name cannot be null or whitespace.", nameof(indexName)); + } + + if (indexName.Length > MaxIndexNameLength) + { + throw new ArgumentException($"Index name '{indexName}' exceeds maximum length of {MaxIndexNameLength} characters.", nameof(indexName)); + } + + customIndexName = indexName; + UpdateIndex(); + return this; + } + /// /// Creates or updates a composite index for all ordering properties. /// void UpdateIndex() { - var indexName = $"IX_{typeof(TEntity).Name}_DefaultOrder"; + var indexName = customIndexName ?? $"IX_{typeof(TEntity).Name}_DefaultOrder"; + + if (indexName.Length > MaxIndexNameLength) + { + throw new InvalidOperationException( + $"The auto-generated index name '{indexName}' exceeds the maximum length of {MaxIndexNameLength} characters. " + + $"Use .WithIndexName() to specify a shorter custom index name."); + } + var entityType = entityBuilder.Metadata; // Remove existing index with this name (if any) before creating the updated one diff --git a/src/Tests/IndexNameTests.cs b/src/Tests/IndexNameTests.cs new file mode 100644 index 0000000..79b8f29 --- /dev/null +++ b/src/Tests/IndexNameTests.cs @@ -0,0 +1,197 @@ +[TestFixture] +public class IndexNameTests +{ + static DbContextOptions CreateOptions() => + new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Database=Test;Trusted_Connection=True") + .UseDefaultOrderBy() + .Options; + + [Test] + public void AutoGeneratedIndexName_WithinLimit_Succeeds() + { + // Should not throw - entity name is short + using var context = new ContextWithShortEntityName(CreateOptions()); + _ = context.Model; + + // Verify the index was created with auto-generated name + var entityType = context.Model.FindEntityType(typeof(ShortEntity))!; + var index = entityType.GetIndexes().FirstOrDefault(); + Assert.That(index, Is.Not.Null); + Assert.That(index?.GetDatabaseName(), Is.EqualTo("IX_ShortEntity_DefaultOrder")); + } + + [Test] + public void AutoGeneratedIndexName_ExceedsLimit_ThrowsWithHelpfulMessage() + { + var exception = Assert.Throws(() => + { + using var context = new ContextWithLongEntityName(CreateOptions()); + _ = context.Model; + })!; + + Assert.That(exception.Message, Does.Contain("exceeds the maximum length of 128")); + Assert.That(exception.Message, Does.Contain("WithIndexName()")); + } + + [Test] + public void CustomIndexName_WithinLimit_Succeeds() + { + // Should not throw + using var context = new ContextWithCustomIndexName(CreateOptions()); + _ = context.Model; + + // Verify the index was created with custom name + var entityType = context.Model.FindEntityType(typeof(EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere))!; + var index = entityType.GetIndexes().FirstOrDefault(); + Assert.That(index, Is.Not.Null); + Assert.That(index?.GetDatabaseName(), Is.EqualTo("IX_LongEntity_Order")); + } + + [Test] + public void CustomIndexName_ExceedsLimit_ThrowsArgumentException() + { + var exception = Assert.Throws(() => + { + using var context = new ContextWithTooLongCustomIndexName(CreateOptions()); + _ = context.Model; + })!; + + Assert.That(exception.Message, Does.Contain("exceeds maximum length of 128")); + } + + [Test] + public void CustomIndexName_NullOrEmpty_ThrowsArgumentException() + { + var exception = Assert.Throws(() => + { + using var context = new ContextWithEmptyCustomIndexName(CreateOptions()); + _ = context.Model; + })!; + + Assert.That(exception.Message, Does.Contain("cannot be null or whitespace")); + } + + [Test] + public void WithIndexName_CanBeChainedWithThenBy() + { + // Should not throw + using var context = new ContextWithChainedIndexName(CreateOptions()); + _ = context.Model; + + // Verify the index was created with custom name and has all properties + var entityType = context.Model.FindEntityType(typeof(MultiPropertyEntity))!; + var index = entityType.GetIndexes().FirstOrDefault(); + Assert.That(index, Is.Not.Null); + Assert.That(index?.GetDatabaseName(), Is.EqualTo("IX_Multi_Custom")); + Assert.That(index?.Properties.Select(p => p.Name), Is.EquivalentTo(new[] { "Category", "Priority" })); + } +} + +class ContextWithShortEntityName(DbContextOptions options) + : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .OrderBy(_ => _.Name); + } +} + +class ContextWithLongEntityName(DbContextOptions options) + : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .OrderBy(_ => _.Name); + } +} + +class ContextWithCustomIndexName(DbContextOptions options) + : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .OrderBy(_ => _.Name) + .WithIndexName("IX_LongEntity_Order"); + } +} + +class ContextWithTooLongCustomIndexName(DbContextOptions options) + : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .OrderBy(_ => _.Name) + .WithIndexName(new string('X', 129)); // 129 characters exceeds limit + } +} + +class ContextWithEmptyCustomIndexName(DbContextOptions options) + : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .OrderBy(_ => _.Name) + .WithIndexName(""); + } +} + +class ContextWithChainedIndexName(DbContextOptions options) + : DbContext(options) +{ + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + .OrderBy(_ => _.Category) + .ThenBy(_ => _.Priority) + .WithIndexName("IX_Multi_Custom"); + } +} + +public class ShortEntity +{ + public int Id { get; set; } + public string Name { get; set; } = ""; +} + +public class EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix +{ + public int Id { get; set; } + public string Name { get; set; } = ""; +} + +public class EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere +{ + public int Id { get; set; } + public string Name { get; set; } = ""; +} + +public class MultiPropertyEntity +{ + public int Id { get; set; } + public string Category { get; set; } = ""; + public int Priority { get; set; } +}