TagKit.AspNetCore 1.0.0

dotnet add package TagKit.AspNetCore --version 1.0.0
                    
NuGet\Install-Package TagKit.AspNetCore -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TagKit.AspNetCore" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TagKit.AspNetCore" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="TagKit.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TagKit.AspNetCore --version 1.0.0
                    
#r "nuget: TagKit.AspNetCore, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TagKit.AspNetCore@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TagKit.AspNetCore&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=TagKit.AspNetCore&version=1.0.0
                    
Install as a Cake Tool

TagKit

Polymorphic tagging for .NET — typed tags with colors, ordering, and tenant scoping. Inspired by spatie/laravel-tags.

Table of Contents


Overview

TagKit provides a complete polymorphic tagging system:

  • Tag any entity type with the ITaggable interface
  • 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
  • IQueryable extensions WithAnyTags / WithAllTags for EF Core
  • EF Core and Dapper persistence backends

Packages

Package Description NuGet
TagKit Core — entities, interfaces, ITagService NuGet
TagKit.AspNetCore ASP.NET Core DI helpers NuGet
TagKit.EntityFrameworkCore EF Core store + query extensions NuGet
TagKit.Dapper Dapper store NuGet

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 / WithAllTags to work, the entity's Tags collection must be populated — either via EF Core Include or 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 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 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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 108 3/26/2026