Pragmatic.Tags
Add normalized many-to-many tags to any Pragmatic entity with a single attribute.
Pragmatic.Tags is a trait package that generates the shared tag entity, junction entity, actions, and endpoints in the consuming boundary.
Status: implemented preview package. The package is already real and usable inside the repo; the missing piece was public-quality docs.
The Problem
Section titled “The Problem”Tagging looks simple until you try to keep it consistent:
- normalize values
- avoid duplicates
- decide whether tags are curated or free-form
- model the many-to-many relationship
- expose add/remove/list endpoints
- keep usage counts and scope rules coherent
Without a shared implementation, every project solves these details differently.
The Solution
Section titled “The Solution”One attribute activates the tagging pipeline:
using Pragmatic.Tags;using Pragmatic.Persistence.Entity;
[Entity<Guid>][Resource("articles")][HasTags]public partial class Article{ public string Title { get; set; } = "";}The generator creates:
- a shared concrete
Tagentity derived fromTagBase - a
{Parent}Tagjunction entity derived fromEntityTagBase<TEntityId> - actions for add, remove, and list
- endpoints under the parent resource
- a
Tagsnavigation on the parent entity
Installation
Section titled “Installation”<PackageReference Include="Pragmatic.Tags" />Typical consumers also reference:
Pragmatic.PersistencePragmatic.Endpointsif HTTP endpoints are desired
Quick Start
Section titled “Quick Start”[Entity<Guid>][Resource("articles")][HasTags(AllowCustom = true, Scope = "content", MaxPerEntity = 25)]public partial class Article{ public string Title { get; set; } = "";}Generated workflows typically include:
- add a tag to an entity
- remove a tag from an entity
- list current tags for an entity
Attribute Options
Section titled “Attribute Options”| Option | Default | Description |
|---|---|---|
MaxPerEntity | 50 | Maximum number of tags on a single entity instance. 0 means unlimited. |
AllowCustom | true | Whether new tags can be created on the fly. |
CaseSensitive | false | Whether Urgent and urgent are distinct tags. |
Scope | parent entity type name | Namespace for tag isolation or sharing. |
SubBoundary | {Parent}Tags | Override generated sub-boundary name. |
Tag Models
Section titled “Tag Models”TagBase provides the shared tag metadata:
ValueDisplayValueScopeUsageCountCreatedAt,CreatedBy
EntityTagBase<TEntityId> provides the junction record:
ParentEntityIdTagIdAddedAtAddedBy
Custom Policy Hook
Section titled “Custom Policy Hook”Register an ITagPolicy<TEntityId> implementation to enforce a curated taxonomy and
react to tag lifecycle events. All four members have default implementations, so
override only what you need:
| Member | Purpose |
|---|---|
Task<bool> IsAllowedAsync(string tagValue, CancellationToken) | Validation gate — return false to reject a tag value. Default allows any non-blank value. |
string Normalize(string tagValue) | Normalizes a value before storage and matching. Default trims and lowercases. |
Task OnTagAddedAsync(TEntityId entityId, Guid tagId, string tagValue, CancellationToken) | Side effect after a tag is added (audit, projections, notifications). |
Task OnTagRemovedAsync(TEntityId entityId, Guid tagId, string tagValue, CancellationToken) | Side effect after a tag is removed. |
public sealed class ArticleTagPolicy : ITagPolicy<Guid>{ private static readonly HashSet<string> Curated = new(StringComparer.OrdinalIgnoreCase) { "news", "opinion", "review" };
public Task<bool> IsAllowedAsync(string tagValue, CancellationToken ct = default) => Task.FromResult(Curated.Contains(tagValue));
public Task OnTagAddedAsync(Guid entityId, Guid tagId, string tagValue, CancellationToken ct = default) { // e.g. refresh a read model or emit a domain event return Task.CompletedTask; }}
// Registrationservices.AddScoped<ITagPolicy<Guid>, ArticleTagPolicy>();Documentation
Section titled “Documentation”Local docs:
Related modules:
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Tags is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).