TagKit 1.0.0
dotnet add package TagKit --version 1.0.0
NuGet\Install-Package TagKit -Version 1.0.0
<PackageReference Include="TagKit" Version="1.0.0" />
<PackageVersion Include="TagKit" Version="1.0.0" />
<PackageReference Include="TagKit" />
paket add TagKit --version 1.0.0
#r "nuget: TagKit, 1.0.0"
#:package TagKit@1.0.0
#addin nuget:?package=TagKit&version=1.0.0
#tool nuget:?package=TagKit&version=1.0.0
TagKit
Polymorphic tagging for .NET — typed tags with colors, ordering, and tenant scoping. Inspired by spatie/laravel-tags.
Table of Contents
- Overview
- Packages
- Installation
- Quick Start
- Core Concepts
- ITagService API
- Registration
- Entity Framework Core
- Dapper
- Database Schema
- Multi-Tenant Scoping
- Tag Types
- Tag Colors
- Contributing
- License
Overview
TagKit provides a complete polymorphic tagging system:
- Tag any entity type with the
ITaggableinterface - Scoped tags per tenant / project
- Typed tags (
label,category,status-tag, or any custom type) - Optional hex color codes per tag
- Configurable sort ordering
- Slug auto-generation from tag names
IQueryableextensionsWithAnyTags/WithAllTagsfor EF Core- EF Core and Dapper persistence backends
Packages
Installation
# Core only
dotnet add package TagKit
# With EF Core
dotnet add package TagKit.EntityFrameworkCore
# With Dapper
dotnet add package TagKit.Dapper
# ASP.NET Core helpers
dotnet add package TagKit.AspNetCore
Quick Start
1. Implement ITaggable
using TagKit.Abstractions;
using TagKit.Entities;
public class Ticket : ITaggable, IHasTagScope
{
public static string TaggableEntityType => "ticket"; // stable discriminator
public object Id => TicketId;
public object ScopeId => ProjectId;
public ICollection<TagAssignment>? Tags { get; set; }
public string TicketId { get; set; } = Guid.NewGuid().ToString("N");
public string ProjectId { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
}
2. Register TagKit
// Program.cs
services.AddTagKit(options =>
{
options.TagsTable = "tags";
options.AssignmentsTable = "tag_assignments";
options.EnableColors = true;
options.EnableSortOrder = true;
})
.UseEntityFrameworkCore<AppDbContext>();
// or .UseDapper();
3. Use ITagService
public class TicketService(ITagService tagService)
{
public async Task AddLabelsAsync(Ticket ticket, IEnumerable<string> labels)
=> await tagService.AttachAsync(ticket, labels);
public async Task ReplaceLabelsAsync(Ticket ticket, IEnumerable<string> labels)
=> await tagService.SyncAsync(ticket, labels);
}
Core Concepts
Tag Entity
public class Tag
{
public string Id { get; set; } // 32-char hex GUID
public string ScopeId { get; set; } // tenant/project isolation
public string Name { get; set; } // display name
public string Slug { get; set; } // URL-friendly key, auto-generated
public string Type { get; set; } // "label" | "category" | "status-tag" | custom
public string? Color { get; set; } // hex, e.g. "#FF5733"
public int SortOrder { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}
TagAssignment Entity
The join record between a Tag and any taggable entity.
public class TagAssignment
{
public string TagId { get; set; }
public string TaggableType { get; set; } // e.g. "ticket"
public string TaggableId { get; set; } // entity PK as string
public DateTimeOffset CreatedAt { get; set; }
}
ITaggable Interface
public interface ITaggable
{
object Id { get; }
static abstract string TaggableEntityType { get; }
ICollection<TagAssignment>? Tags { get; set; }
}
The TaggableEntityType is a static abstract member — it acts as the polymorphic discriminator stored in tag_assignments.taggable_type. Keep it stable across class renames.
IHasTagScope Interface
public interface IHasTagScope
{
object ScopeId { get; }
}
Implement this on your entity to enable per-tenant tag isolation. When not implemented, an empty scope ("") is used (single-tenant).
ITagService API
| Method | Description |
|---|---|
AttachAsync(entity, tagNames) |
Attach tags by name; creates missing tags; idempotent |
DetachAsync(entity, tagNames) |
Remove tags from an entity; missing tags silently ignored |
SyncAsync(entity, tagNames) |
Replace entity's tags with exactly the provided set |
GetForEntityAsync<T>(entityId) |
Return all tags for a specific entity |
GetAllAsync(scopeId, type?) |
Return all tags in a scope, optionally filtered by type |
SuggestAsync(scopeId, prefix, limit) |
Autocomplete — tags starting with prefix |
CreateAsync(name, scopeId, type?, color?) |
Create and persist a new tag |
DeleteAsync(tagId) |
Delete a tag and all its assignments |
// Attach
await tagService.AttachAsync(ticket, ["bug", "critical"]);
// Detach
await tagService.DetachAsync(ticket, ["critical"]);
// Sync (replace entirely)
await tagService.SyncAsync(ticket, ["bug", "needs-review"]);
// Query
var tags = await tagService.GetForEntityAsync<Ticket>(ticketId);
var allLabels = await tagService.GetAllAsync("project-1", type: "label");
var suggestions = await tagService.SuggestAsync("project-1", "bu", limit: 5);
// Create explicitly
var tag = await tagService.CreateAsync("My Tag", "project-1", type: "category", color: "#3498DB");
// Delete
await tagService.DeleteAsync(tag.Id);
Registration
Options
services.AddTagKit(options =>
{
options.TagsTable = "tags"; // default: "tags"
options.AssignmentsTable = "tag_assignments"; // default: "tag_assignments"
options.EnableColors = true; // default: true
options.EnableSortOrder = true; // default: true
options.DefaultType = "label"; // default: "label"
});
Fluent builder
services
.AddTagKit(options => { ... })
.UseEntityFrameworkCore<AppDbContext>();
services
.AddTagKit(options => { ... })
.UseDapper();
Entity Framework Core
DbContext Setup
Apply TagKit entity configurations in OnModelCreating:
using TagKit.EntityFrameworkCore.Extensions;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Tag> Tags => Set<Tag>();
public DbSet<TagAssignment> TagAssignments => Set<TagAssignment>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply with default options (reads TagsTable / AssignmentsTable from config)
modelBuilder.ApplyTagKitConfiguration();
// Or pass options explicitly
modelBuilder.ApplyTagKitConfiguration(new TagKitOptions
{
TagsTable = "tags",
AssignmentsTable = "tag_assignments",
});
}
}
Register the EF Core store:
services
.AddTagKit(options => { ... })
.UseEntityFrameworkCore<AppDbContext>();
Query Extensions
Filter entity queries using tag slugs:
using TagKit.EntityFrameworkCore.Extensions;
// Entities with ANY of the given tags
var openOrBugTickets = await context.Tickets
.Include(t => t.Tags).ThenInclude(a => a.Tag)
.WithAnyTags("open", "bug")
.ToListAsync();
// Entities with ALL of the given tags
var criticalBugTickets = await context.Tickets
.Include(t => t.Tags).ThenInclude(a => a.Tag)
.WithAllTags("bug", "critical")
.ToListAsync();
Note: For
WithAnyTags/WithAllTagsto work, the entity'sTagscollection must be populated — either via EF CoreIncludeor manually.
Dapper
Register and provide an IDbConnection:
services.AddScoped<IDbConnection>(_ => new SqliteConnection(connectionString));
services
.AddTagKit(options => { ... })
.UseDapper();
All tag store operations use INSERT OR IGNORE for idempotent assignment insertion and standard ANSI SQL for all other queries.
Database Schema
CREATE TABLE tags (
id TEXT PRIMARY KEY,
scope_id TEXT NOT NULL,
name TEXT NOT NULL,
slug TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'label',
color TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE (scope_id, slug)
);
CREATE INDEX ix_tags_scope_type ON tags (scope_id, type);
CREATE TABLE tag_assignments (
tag_id TEXT NOT NULL,
taggable_type TEXT NOT NULL,
taggable_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (tag_id, taggable_type, taggable_id),
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE
);
CREATE INDEX ix_tag_assignments_subject ON tag_assignments (taggable_type, taggable_id);
Multi-Tenant Scoping
Implement IHasTagScope on your entity to isolate tags per tenant:
public class Ticket : ITaggable, IHasTagScope
{
public static string TaggableEntityType => "ticket";
public object Id => TicketId;
public object ScopeId => ProjectId; // tags are scoped to this project
public ICollection<TagAssignment>? Tags { get; set; }
public string TicketId { get; set; } = Guid.NewGuid().ToString("N");
public string ProjectId { get; set; } = string.Empty;
}
Tags created in project-1 are invisible in project-2. The same slug can exist independently in different scopes.
Tag Types
Use type to categorise tags within a scope:
// Create typed tags
var label = await tagService.CreateAsync("Bug", "project-1", type: "label");
var status = await tagService.CreateAsync("Open", "project-1", type: "status-tag");
var category = await tagService.CreateAsync("Backend", "project-1", type: "category");
// Query by type
var labels = await tagService.GetAllAsync("project-1", type: "label");
var statuses = await tagService.GetAllAsync("project-1", type: "status-tag");
The DefaultType option (default: "label") is applied when no type is specified.
Tag Colors
Assign a hex color to any tag:
var tag = await tagService.CreateAsync("Critical", "project-1", color: "#E74C3C");
Set EnableColors = false in options to disable color support entirely — the color field will be ignored on create.
Contributing
Contributions are welcome. Please open an issue or pull request on GitHub.
License
MIT License. See LICENSE for details.
| 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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Options (>= 10.0.3)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Options (>= 9.0.3)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on TagKit:
| Package | Downloads |
|---|---|
|
TagKit.Dapper
Dapper integration for TagKit — raw-SQL ITagStore implementation with SQLite/SQL Server/PostgreSQL compatibility. |
|
|
TagKit.AspNetCore
ASP.NET Core integration for TagKit — DI registration helpers and HttpContext-aware scope resolution. |
|
|
TagKit.EntityFrameworkCore
Entity Framework Core integration for TagKit — entity configuration, ITagStore implementation, and IQueryable extension methods. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 155 | 3/26/2026 |