GUIDs are great for database keys but terrible for humans. When a user calls support about order "a3f2b1c4-d5e6-7890-abcd-ef1234567890", both parties are frustrated. The Blazor CRM solves this with auto-number generation — a system that produces short, readable, sequential identifiers like "BKG/2026/00001" using consonant-based templates. This part shows how auto-numbers work in VSA, how they integrate with handlers, and how EF Core Fluent API configures the underlying entity model.
The auto-number system is an extension method on AppDbContext called GenerateAutoNumberAsync. It takes an entity name (used to find the right sequence), a prefix template (with placeholders like {Year}), and produces the next sequential number. The system uses a dedicated AutoNumber table in the database to track sequences per entity type. This is called inside the Create handler before entity creation, keeping the handler clean while providing production-quality identifiers.
The GenerateAutoNumberAsync Extension
The extension method queries the AutoNumber table for the entity's current sequence, increments it, formats the prefix template, and returns the result. The consonant-based short name is generated by taking the first 3 consonants from the entity name — "Booking" becomes "BKG", "Todo" becomes "TD". The {Year} placeholder is replaced with the current year:
var autoNo = await _context.GenerateAutoNumberAsync(
entityName: nameof(Data.Entities.Booking),
prefixTemplate: $"BKG/{{Year}}/",
ct: cancellationToken);
The handler stores the auto-number on the entity before saving. The entity's AutoNumber property is a simple string — no special database configuration needed. The extension method handles all the sequencing logic, concurrency protection, and formatting.
EF Core Fluent API Configuration
While auto-numbers are simple strings on the entity, other configurations benefit from EF Core's Fluent API. Shadow properties store data without cluttering the entity class — useful for audit fields managed by the DbContext. Value converters transform data between the database and the entity — useful for enums stored as strings:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Todo>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(200).IsRequired();
entity.Property(e => e.Description).HasMaxLength(1000);
entity.Property(e => e.AutoNumber).HasMaxLength(50);
// Shadow property for soft delete (not on the entity class)
entity.Property<bool>("IsDeleted").HasDefaultValue(false);
// Value converter for enum stored as string
entity.Property(e => e.Priority)
.HasConversion<string>()
.HasMaxLength(20);
});
// Global query filter for soft delete
builder.Entity<Todo>().HasQueryFilter(e => !EF.Property<bool>(e, "IsDeleted"));
}
The Fluent API configuration lives in OnModelCreating in the DbContext — not in the feature folder. This is intentional: entity configuration is infrastructure, not feature logic. The feature handlers work with the configured entities without knowing about shadow properties or query filters. This separation keeps VSA slices focused on business logic.
Concurrency Protection for Auto-Numbers
Auto-numbers must be unique and sequential, even under concurrent requests. The GenerateAutoNumberAsync method uses a database transaction with row-level locking to prevent duplicate numbers. It queries the current max number for the entity type within a transaction, increments it, saves the new sequence, and returns the formatted number. If two requests arrive simultaneously, the database lock ensures they get different numbers. This is production-grade concurrency handling that most auto-number implementations miss.
Integration with VSA Handlers
The auto-number generation is called inside the Create handler, right after validation but before entity creation. The handler doesn't need to know about the AutoNumber table or the sequencing logic — it just calls the extension method and assigns the result. This is the VSA pattern for cross-cutting concerns: build infrastructure as extensions, call them from handlers, keep handlers focused on their feature's business logic. The auto-number system is available to any feature without coupling features together.