Skip to content

Commit db978d0

Browse files
authored
Add support for custom index names and enforce length limit (#8)
Introduces the WithIndexName method to OrderByBuilder, allowing users to specify a custom index name for ordering indexes. Enforces a maximum index name length of 128 characters, throwing informative exceptions if exceeded. Updates documentation and adds comprehensive tests for index name scenarios, including chaining and error cases.
1 parent cc26c94 commit db978d0

3 files changed

Lines changed: 243 additions & 1 deletion

File tree

readme.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ The index:
134134
This eliminates the need to manually create indexes that match the ordering configuration.
135135

136136

137+
### Custom Index Names
138+
139+
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:
140+
141+
```cs
142+
builder.Entity<EntityWithVeryLongNameThatWouldExceedTheLimit>()
143+
.OrderBy(_ => _.Name)
144+
.WithIndexName("IX_LongEntity_Order");
145+
```
146+
147+
If the auto-generated name exceeds 128 characters, an `InvalidOperationException` is thrown with a message suggesting to use `WithIndexName()`.
148+
149+
137150
## Require Ordering for All Entities
138151

139152
Enable validation mode to ensure all entities have default ordering configured:

src/EfOrderBy/OrderByBuilder.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ namespace EfOrderBy;
66
public sealed class OrderByBuilder<TEntity>
77
where TEntity : class
88
{
9+
const int MaxIndexNameLength = 128;
10+
911
Configuration configuration;
1012
EntityTypeBuilder<TEntity> entityBuilder;
13+
string? customIndexName;
1114

1215
internal OrderByBuilder(EntityTypeBuilder<TEntity> builder, PropertyInfo propertyInfo, bool descending)
1316
{
@@ -43,12 +46,41 @@ public OrderByBuilder<TEntity> ThenByDescending<TProperty>(Expression<Func<TEnti
4346
return this;
4447
}
4548

49+
/// <summary>
50+
/// Specifies a custom index name for the default ordering index.
51+
/// Use this when the auto-generated index name would exceed the 128 character limit.
52+
/// </summary>
53+
public OrderByBuilder<TEntity> WithIndexName(string indexName)
54+
{
55+
if (string.IsNullOrWhiteSpace(indexName))
56+
{
57+
throw new ArgumentException("Index name cannot be null or whitespace.", nameof(indexName));
58+
}
59+
60+
if (indexName.Length > MaxIndexNameLength)
61+
{
62+
throw new ArgumentException($"Index name '{indexName}' exceeds maximum length of {MaxIndexNameLength} characters.", nameof(indexName));
63+
}
64+
65+
customIndexName = indexName;
66+
UpdateIndex();
67+
return this;
68+
}
69+
4670
/// <summary>
4771
/// Creates or updates a composite index for all ordering properties.
4872
/// </summary>
4973
void UpdateIndex()
5074
{
51-
var indexName = $"IX_{typeof(TEntity).Name}_DefaultOrder";
75+
var indexName = customIndexName ?? $"IX_{typeof(TEntity).Name}_DefaultOrder";
76+
77+
if (indexName.Length > MaxIndexNameLength)
78+
{
79+
throw new InvalidOperationException(
80+
$"The auto-generated index name '{indexName}' exceeds the maximum length of {MaxIndexNameLength} characters. " +
81+
$"Use .WithIndexName() to specify a shorter custom index name.");
82+
}
83+
5284
var entityType = entityBuilder.Metadata;
5385

5486
// Remove existing index with this name (if any) before creating the updated one

src/Tests/IndexNameTests.cs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
[TestFixture]
2+
public class IndexNameTests
3+
{
4+
static DbContextOptions CreateOptions() =>
5+
new DbContextOptionsBuilder()
6+
.UseSqlServer("Server=.;Database=Test;Trusted_Connection=True")
7+
.UseDefaultOrderBy()
8+
.Options;
9+
10+
[Test]
11+
public void AutoGeneratedIndexName_WithinLimit_Succeeds()
12+
{
13+
// Should not throw - entity name is short
14+
using var context = new ContextWithShortEntityName(CreateOptions());
15+
_ = context.Model;
16+
17+
// Verify the index was created with auto-generated name
18+
var entityType = context.Model.FindEntityType(typeof(ShortEntity))!;
19+
var index = entityType.GetIndexes().FirstOrDefault();
20+
Assert.That(index, Is.Not.Null);
21+
Assert.That(index?.GetDatabaseName(), Is.EqualTo("IX_ShortEntity_DefaultOrder"));
22+
}
23+
24+
[Test]
25+
public void AutoGeneratedIndexName_ExceedsLimit_ThrowsWithHelpfulMessage()
26+
{
27+
var exception = Assert.Throws<InvalidOperationException>(() =>
28+
{
29+
using var context = new ContextWithLongEntityName(CreateOptions());
30+
_ = context.Model;
31+
})!;
32+
33+
Assert.That(exception.Message, Does.Contain("exceeds the maximum length of 128"));
34+
Assert.That(exception.Message, Does.Contain("WithIndexName()"));
35+
}
36+
37+
[Test]
38+
public void CustomIndexName_WithinLimit_Succeeds()
39+
{
40+
// Should not throw
41+
using var context = new ContextWithCustomIndexName(CreateOptions());
42+
_ = context.Model;
43+
44+
// Verify the index was created with custom name
45+
var entityType = context.Model.FindEntityType(typeof(EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere))!;
46+
var index = entityType.GetIndexes().FirstOrDefault();
47+
Assert.That(index, Is.Not.Null);
48+
Assert.That(index?.GetDatabaseName(), Is.EqualTo("IX_LongEntity_Order"));
49+
}
50+
51+
[Test]
52+
public void CustomIndexName_ExceedsLimit_ThrowsArgumentException()
53+
{
54+
var exception = Assert.Throws<ArgumentException>(() =>
55+
{
56+
using var context = new ContextWithTooLongCustomIndexName(CreateOptions());
57+
_ = context.Model;
58+
})!;
59+
60+
Assert.That(exception.Message, Does.Contain("exceeds maximum length of 128"));
61+
}
62+
63+
[Test]
64+
public void CustomIndexName_NullOrEmpty_ThrowsArgumentException()
65+
{
66+
var exception = Assert.Throws<ArgumentException>(() =>
67+
{
68+
using var context = new ContextWithEmptyCustomIndexName(CreateOptions());
69+
_ = context.Model;
70+
})!;
71+
72+
Assert.That(exception.Message, Does.Contain("cannot be null or whitespace"));
73+
}
74+
75+
[Test]
76+
public void WithIndexName_CanBeChainedWithThenBy()
77+
{
78+
// Should not throw
79+
using var context = new ContextWithChainedIndexName(CreateOptions());
80+
_ = context.Model;
81+
82+
// Verify the index was created with custom name and has all properties
83+
var entityType = context.Model.FindEntityType(typeof(MultiPropertyEntity))!;
84+
var index = entityType.GetIndexes().FirstOrDefault();
85+
Assert.That(index, Is.Not.Null);
86+
Assert.That(index?.GetDatabaseName(), Is.EqualTo("IX_Multi_Custom"));
87+
Assert.That(index?.Properties.Select(p => p.Name), Is.EquivalentTo(new[] { "Category", "Priority" }));
88+
}
89+
}
90+
91+
class ContextWithShortEntityName(DbContextOptions options)
92+
: DbContext(options)
93+
{
94+
public DbSet<ShortEntity> Entities => Set<ShortEntity>();
95+
96+
protected override void OnModelCreating(ModelBuilder modelBuilder)
97+
{
98+
base.OnModelCreating(modelBuilder);
99+
modelBuilder.Entity<ShortEntity>()
100+
.OrderBy(_ => _.Name);
101+
}
102+
}
103+
104+
class ContextWithLongEntityName(DbContextOptions options)
105+
: DbContext(options)
106+
{
107+
public DbSet<EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix> Entities => Set<EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix>();
108+
109+
protected override void OnModelCreating(ModelBuilder modelBuilder)
110+
{
111+
base.OnModelCreating(modelBuilder);
112+
modelBuilder.Entity<EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix>()
113+
.OrderBy(_ => _.Name);
114+
}
115+
}
116+
117+
class ContextWithCustomIndexName(DbContextOptions options)
118+
: DbContext(options)
119+
{
120+
public DbSet<EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere> Entities => Set<EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere>();
121+
122+
protected override void OnModelCreating(ModelBuilder modelBuilder)
123+
{
124+
base.OnModelCreating(modelBuilder);
125+
modelBuilder.Entity<EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere>()
126+
.OrderBy(_ => _.Name)
127+
.WithIndexName("IX_LongEntity_Order");
128+
}
129+
}
130+
131+
class ContextWithTooLongCustomIndexName(DbContextOptions options)
132+
: DbContext(options)
133+
{
134+
public DbSet<ShortEntity> Entities => Set<ShortEntity>();
135+
136+
protected override void OnModelCreating(ModelBuilder modelBuilder)
137+
{
138+
base.OnModelCreating(modelBuilder);
139+
modelBuilder.Entity<ShortEntity>()
140+
.OrderBy(_ => _.Name)
141+
.WithIndexName(new string('X', 129)); // 129 characters exceeds limit
142+
}
143+
}
144+
145+
class ContextWithEmptyCustomIndexName(DbContextOptions options)
146+
: DbContext(options)
147+
{
148+
public DbSet<ShortEntity> Entities => Set<ShortEntity>();
149+
150+
protected override void OnModelCreating(ModelBuilder modelBuilder)
151+
{
152+
base.OnModelCreating(modelBuilder);
153+
modelBuilder.Entity<ShortEntity>()
154+
.OrderBy(_ => _.Name)
155+
.WithIndexName("");
156+
}
157+
}
158+
159+
class ContextWithChainedIndexName(DbContextOptions options)
160+
: DbContext(options)
161+
{
162+
public DbSet<MultiPropertyEntity> Entities => Set<MultiPropertyEntity>();
163+
164+
protected override void OnModelCreating(ModelBuilder modelBuilder)
165+
{
166+
base.OnModelCreating(modelBuilder);
167+
modelBuilder.Entity<MultiPropertyEntity>()
168+
.OrderBy(_ => _.Category)
169+
.ThenBy(_ => _.Priority)
170+
.WithIndexName("IX_Multi_Custom");
171+
}
172+
}
173+
174+
public class ShortEntity
175+
{
176+
public int Id { get; set; }
177+
public string Name { get; set; } = "";
178+
}
179+
180+
public class EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix
181+
{
182+
public int Id { get; set; }
183+
public string Name { get; set; } = "";
184+
}
185+
186+
public class EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere
187+
{
188+
public int Id { get; set; }
189+
public string Name { get; set; } = "";
190+
}
191+
192+
public class MultiPropertyEntity
193+
{
194+
public int Id { get; set; }
195+
public string Category { get; set; } = "";
196+
public int Priority { get; set; }
197+
}

0 commit comments

Comments
 (0)