Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntityWithVeryLongNameThatWouldExceedTheLimit>()
.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:
Expand Down
34 changes: 33 additions & 1 deletion src/EfOrderBy/OrderByBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ namespace EfOrderBy;
public sealed class OrderByBuilder<TEntity>
where TEntity : class
{
const int MaxIndexNameLength = 128;

Configuration configuration;
EntityTypeBuilder<TEntity> entityBuilder;
string? customIndexName;

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

/// <summary>
/// Specifies a custom index name for the default ordering index.
/// Use this when the auto-generated index name would exceed the 128 character limit.
/// </summary>
public OrderByBuilder<TEntity> 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;
}

/// <summary>
/// Creates or updates a composite index for all ordering properties.
/// </summary>
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
Expand Down
197 changes: 197 additions & 0 deletions src/Tests/IndexNameTests.cs
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>(() =>
{
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<ArgumentException>(() =>
{
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<ArgumentException>(() =>
{
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<ShortEntity> Entities => Set<ShortEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ShortEntity>()
.OrderBy(_ => _.Name);
}
}

class ContextWithLongEntityName(DbContextOptions options)
: DbContext(options)
{
public DbSet<EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix> Entities => Set<EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<EntityWithAnExtremelyLongNameThatWillDefinitelyExceedTheMaximumIndexNameLengthOfOneHundredTwentyEightCharactersWhenCombinedWithThePrefix>()
.OrderBy(_ => _.Name);
}
}

class ContextWithCustomIndexName(DbContextOptions options)
: DbContext(options)
{
public DbSet<EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere> Entities => Set<EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<EntityWithVeryLongNameThatWouldExceedTheLimitIfWeDidNotUseCustomIndexNameHere>()
.OrderBy(_ => _.Name)
.WithIndexName("IX_LongEntity_Order");
}
}

class ContextWithTooLongCustomIndexName(DbContextOptions options)
: DbContext(options)
{
public DbSet<ShortEntity> Entities => Set<ShortEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ShortEntity>()
.OrderBy(_ => _.Name)
.WithIndexName(new string('X', 129)); // 129 characters exceeds limit
}
}

class ContextWithEmptyCustomIndexName(DbContextOptions options)
: DbContext(options)
{
public DbSet<ShortEntity> Entities => Set<ShortEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ShortEntity>()
.OrderBy(_ => _.Name)
.WithIndexName("");
}
}

class ContextWithChainedIndexName(DbContextOptions options)
: DbContext(options)
{
public DbSet<MultiPropertyEntity> Entities => Set<MultiPropertyEntity>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<MultiPropertyEntity>()
.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; }
}