Skip to content

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.

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.

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 Tag entity derived from TagBase
  • a {Parent}Tag junction entity derived from EntityTagBase<TEntityId>
  • actions for add, remove, and list
  • endpoints under the parent resource
  • a Tags navigation on the parent entity
<PackageReference Include="Pragmatic.Tags" />

Typical consumers also reference:

  • Pragmatic.Persistence
  • Pragmatic.Endpoints if HTTP endpoints are desired
[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
OptionDefaultDescription
MaxPerEntity50Maximum number of tags on a single entity instance. 0 means unlimited.
AllowCustomtrueWhether new tags can be created on the fly.
CaseSensitivefalseWhether Urgent and urgent are distinct tags.
Scopeparent entity type nameNamespace for tag isolation or sharing.
SubBoundary{Parent}TagsOverride generated sub-boundary name.

TagBase provides the shared tag metadata:

  • Value
  • DisplayValue
  • Scope
  • UsageCount
  • CreatedAt, CreatedBy

EntityTagBase<TEntityId> provides the junction record:

  • ParentEntityId
  • TagId
  • AddedAt
  • AddedBy

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:

MemberPurpose
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;
}
}
// Registration
services.AddScoped<ITagPolicy<Guid>, ArticleTagPolicy>();

Local docs:

Related modules:

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).