SlugKit.Dapper
1.0.0
dotnet add package SlugKit.Dapper --version 1.0.0
NuGet\Install-Package SlugKit.Dapper -Version 1.0.0
<PackageReference Include="SlugKit.Dapper" Version="1.0.0" />
<PackageVersion Include="SlugKit.Dapper" Version="1.0.0" />
<PackageReference Include="SlugKit.Dapper" />
paket add SlugKit.Dapper --version 1.0.0
#r "nuget: SlugKit.Dapper, 1.0.0"
#:package SlugKit.Dapper@1.0.0
#addin nuget:?package=SlugKit.Dapper&version=1.0.0
#tool nuget:?package=SlugKit.Dapper&version=1.0.0
SlugKit
Automatic URL-safe slug generation for EF Core and Dapper entities with scoped uniqueness, transliteration, and regeneration control. Inspired by spatie/laravel-sluggable.
Table of Contents
- Packages
- Quick Start
- Core Concepts
- Slug Generation Pipeline
- Configuration
- EF Core Integration
- Dapper Integration
- ASP.NET Core Integration
- Advanced Scenarios
- Entity Examples
- Supported Frameworks
Packages
| Package | NuGet ID | Description |
|---|---|---|
| Core | SlugKit |
Interfaces, attributes, generator, options |
| ASP.NET Core | SlugKit.AspNetCore |
DI registration helpers |
| EF Core | SlugKit.EntityFrameworkCore |
SaveChanges interceptor, query extensions |
| Dapper | SlugKit.Dapper |
IDbConnection lookup extensions |
Quick Start
1. Install packages
dotnet add package SlugKit
dotnet add package SlugKit.EntityFrameworkCore # for EF Core
dotnet add package SlugKit.Dapper # for Dapper
2. Mark your entity
public class Article : IHasSlug
{
public int Id { get; set; }
public string Title { get; set; }
[Slug(From = nameof(Title), Unique = true, MaxLength = 80)]
public string Slug { get; set; }
}
3. Register services
// Program.cs
builder.Services.AddSlugKit(options =>
{
options.DefaultSeparator = "-";
options.DefaultMaxLength = 100;
options.DefaultOnUpdate = SlugBehavior.NeverRegenerate;
options.ReservedSlugs = ["admin", "api", "settings", "new", "edit", "delete"];
})
.UseEntityFrameworkCore<AppDbContext>()
.UseDapper();
4. Register the EF Core interceptor
// In your DbContext OnConfiguring, or when registering DbContext:
optionsBuilder.AddInterceptors(serviceProvider.GetRequiredService<SlugGenerationInterceptor>());
5. Save your entity — slug is generated automatically
var article = new Article { Title = "Hello World" };
context.Articles.Add(article);
await context.SaveChangesAsync();
Console.WriteLine(article.Slug); // "hello-world"
Core Concepts
IHasSlug Interface
All slug-bearing entities must implement IHasSlug:
public interface IHasSlug
{
string Slug { get; set; }
}
SlugAttribute
Decorate the Slug property with [Slug] to configure generation:
[AttributeUsage(AttributeTargets.Property)]
public class SlugAttribute : Attribute
{
public string From { get; set; } // Source property name (required)
public string? Scope { get; set; } // Scoping property name (optional)
public bool Unique { get; set; } = true;
public int MaxLength { get; set; } = 100;
public SlugBehavior OnUpdate { get; set; } = SlugBehavior.NeverRegenerate;
}
SlugBehavior
| Value | Description |
|---|---|
NeverRegenerate |
Slug is set once on insert and never changed (default). Stable, bookmarkable URLs. |
AlwaysRegenerate |
Slug is regenerated from source on every save, even if source did not change. |
RegenerateOnChange |
Slug is regenerated only when the source property value has changed. |
Slug Generation Pipeline
Given the input "Héllo, Wörld!" with MaxLength = 100:
| Step | Operation | Result |
|---|---|---|
| 1 | Transliterate | "Hello, World!" |
| 2 | Lowercase | "hello, world!" |
| 3 | Replace non-alphanumeric with separator | "hello-world-" |
| 4 | Collapse consecutive separators | "hello-world-" |
| 5 | Trim separators from start/end | "hello-world" |
| 6 | Truncate to MaxLength | "hello-world" (no truncation needed) |
| 7 | Uniqueness check + suffix | "hello-world" (unique, no suffix needed) |
If "hello-world" already exists, the result would be "hello-world-2", then "hello-world-3", etc.
Configuration
Global Options
services.AddSlugKit(options =>
{
options.DefaultSeparator = "-"; // Word separator character
options.DefaultMaxLength = 100; // Max slug length
options.DefaultOnUpdate = SlugBehavior.NeverRegenerate;
options.ReservedSlugs = ["admin", "api", "settings"];
});
Individual [Slug] attribute settings always override global options.
Reserved Slugs
Slugs that match a reserved value are automatically suffixed:
// "admin" is reserved → generated slug becomes "admin-2"
var entity = new SomeEntity { Name = "Admin" };
// slug → "admin-2"
Default reserved slugs: admin, api, settings, new, edit, delete.
Custom Transliterator
Replace the default NFD-based transliterator with your own for non-Latin scripts:
public class CyrillicTransliterator : ISlugTransliterator
{
private static readonly Dictionary<char, string> Map = new()
{
['а'] = "a", ['б'] = "b", ['в'] = "v", // ...
};
public string Transliterate(string input)
=> string.Concat(input.Select(c => Map.TryGetValue(c, out var v) ? v : c.ToString()));
}
// Registration:
services.AddSlugKit().UseTransliterator<CyrillicTransliterator>();
EF Core Integration
Setup
Install the package:
dotnet add package SlugKit.EntityFrameworkCore
Register services and wire the interceptor into your DbContext:
// Program.cs
builder.Services.AddSlugKit(/* options */)
.UseEntityFrameworkCore<AppDbContext>();
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(connectionString);
options.AddInterceptors(sp.GetRequiredService<SlugGenerationInterceptor>());
});
Scoped Uniqueness
Use Scope to make slugs unique within a parent entity:
public class TicketTag : IHasSlug
{
public string Name { get; set; }
public Guid ProjectId { get; set; }
[Slug(From = nameof(Name), Scope = nameof(ProjectId), Unique = true)]
public string Slug { get; set; }
}
Two TicketTag rows with the same Name in different projects can share the same slug. Two rows in the same project will get "tag-name" and "tag-name-2".
Update Behaviour
// Never regenerate (default) — stable URLs
public class Page : IHasSlug
{
public string Title { get; set; }
[Slug(From = nameof(Title), OnUpdate = SlugBehavior.NeverRegenerate)]
public string Slug { get; set; }
}
// Always regenerate — slug tracks title exactly
public class Article : IHasSlug
{
public string Title { get; set; }
[Slug(From = nameof(Title), OnUpdate = SlugBehavior.AlwaysRegenerate)]
public string Slug { get; set; }
}
// Regenerate only when title changes
public class Post : IHasSlug
{
public string Title { get; set; }
[Slug(From = nameof(Title), OnUpdate = SlugBehavior.RegenerateOnChange)]
public string Slug { get; set; }
}
Query Extensions
using SlugKit.EntityFrameworkCore.Extensions;
// Filter by slug
var article = await context.Articles.WhereSlug("hello-world").FirstOrDefaultAsync();
// Convenience single-result lookup
var article = await context.Articles.FindBySlugAsync("hello-world");
Dapper Integration
Setup
dotnet add package SlugKit.Dapper
services.AddSlugKit().UseDapper();
FindBySlugAsync
using SlugKit.Dapper.Extensions;
// Simple lookup by slug
var project = await connection.FindBySlugAsync<Project>("my-project");
// Scoped lookup (unique within project)
var tag = await connection.FindBySlugAsync<TicketTag>(
slug: "backend",
scopeId: projectId,
scopeColumn: "project_id");
The table name is resolved automatically:
- If the entity has
[Table("table_name")], that name is used. - Otherwise, the type name is converted to
snake_caseand pluralised (e.g.TicketTag→ticket_tags).
GetExistingSlugsAsync
Use this to populate the existingSlugs list before calling ISlugGenerator.Generate:
var existingSlugs = await connection.GetExistingSlugsAsync<TicketTag>(
scopeId: projectId,
scopeColumn: "project_id");
var newSlug = slugGenerator.Generate("Backend", existingSlugs);
// "backend-2" if "backend" is already taken in this project
ASP.NET Core Integration
dotnet add package SlugKit.AspNetCore
using SlugKit.AspNetCore.DependencyInjection;
services.AddSlugKit().AddAspNetCore();
Advanced Scenarios
Custom Slug Generator
Replace the default generator entirely:
public class MySlugGenerator : ISlugGenerator
{
public string Generate(string source, IReadOnlyList<string>? existingSlugs = null, int maxLength = 0)
{
// your implementation
}
}
services.AddSlugKit();
services.AddSingleton<ISlugGenerator, MySlugGenerator>();
Unicode and Non-Latin Scripts
The default transliterator handles Western European accents via Unicode NFD decomposition:
| Input | Output |
|---|---|
café |
cafe |
naïve |
naive |
Zürich |
zurich |
crème brûlée |
creme-brulee |
Año Nuevo |
ano-nuevo |
For Cyrillic, CJK, Arabic, or other scripts, register a custom ISlugTransliterator.
Table Name Resolution (Dapper)
| Entity Type | Resolved Table Name |
|---|---|
[Table("articles")] |
articles |
Project (no attribute) |
projects |
TicketTag (no attribute) |
ticket_tags |
BlogPost (no attribute) |
blog_posts |
Entity Examples
Simple Entity (global uniqueness)
public class Project : IHasSlug
{
public int Id { get; set; }
public string Name { get; set; }
[Slug(From = nameof(Name), Unique = true, MaxLength = 60)]
public string Slug { get; set; }
}
Scoped Entity (unique per parent)
public class TicketTag : IHasSlug
{
public int Id { get; set; }
public string Name { get; set; }
public Guid ProjectId { get; set; }
[Slug(From = nameof(Name), Scope = nameof(ProjectId), Unique = true)]
public string Slug { get; set; }
}
Always-fresh slug (tracks source changes)
public class WikiPage : IHasSlug
{
public int Id { get; set; }
public string Title { get; set; }
[Slug(From = nameof(Title), OnUpdate = SlugBehavior.RegenerateOnChange, MaxLength = 120)]
public string Slug { get; set; }
}
Supported Frameworks
All packages multi-target:
| Framework | Supported |
|---|---|
| .NET 8 | Yes |
| .NET 9 | Yes |
| .NET 10 | Yes |
SlugKit.AspNetCore targets net8.0 and net10.0 (ASP.NET Core LTS cadence).
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Dapper (>= 2.1.35)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Options (>= 10.0.3)
- SlugKit (>= 1.0.0)
-
net8.0
- Dapper (>= 2.1.35)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
- SlugKit (>= 1.0.0)
-
net9.0
- Dapper (>= 2.1.35)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
- SlugKit (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 111 | 3/26/2026 |