Sencilla.Component.Tags 10.0.62

dotnet add package Sencilla.Component.Tags --version 10.0.62
                    
NuGet\Install-Package Sencilla.Component.Tags -Version 10.0.62
                    
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="Sencilla.Component.Tags" Version="10.0.62" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sencilla.Component.Tags" Version="10.0.62" />
                    
Directory.Packages.props
<PackageReference Include="Sencilla.Component.Tags" />
                    
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 Sencilla.Component.Tags --version 10.0.62
                    
#r "nuget: Sencilla.Component.Tags, 10.0.62"
                    
#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 Sencilla.Component.Tags@10.0.62
                    
#: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=Sencilla.Component.Tags&version=10.0.62
                    
Install as a Cake Addin
#tool nuget:?package=Sencilla.Component.Tags&version=10.0.62
                    
Install as a Cake Tool

Sencilla.Component.Tags

Free-text tagging for any Sencilla entity: implement one interface, get ?tag= filtering, tag endpoints and a tags: string[] field on the wire.

[CrudApi("api/v1/pricerules")]
public class PriceRule : IEntity<Guid>, IEntityTaggableInline
{
    public Guid Id { get; set; }
    public List<string>? Tags { get; set; }   // NVARCHAR(4000) NULL on the table
}
GET    /api/v1/pricerules?tag=promo&tag=delivery   # rows carrying ANY of these tags
GET    /api/v1/pricerules?with=tags                # ...and fill tags[] on them (side-table storage only)
GET    /api/v1/pricerules/tags                     # every tag in use — autocomplete
POST   /api/v1/pricerules/{id}/tags                # ["promo","delivery"] — REPLACES the set
DELETE /api/v1/pricerules/{id}/tags?tag=promo      # remove named tags

Three repositories, one contract

IEntityTaggable.Tags is the wire contract for all three; consumers never learn where the rows live, so an entity can migrate between repositories without a single consumer changing.

Interface Storage Pick it when DDL
IEntityTaggableInline JSON array column on the entity's own row the table is small or loaded wholesale (tags arrive with the row; no join, no second query) [Tags] NVARCHAR(4000) NULL on your table
IEntityTaggableLinked {Entity}Tag — typed FK, ON DELETE CASCADE large/hot tables that must filter by tag in SQL your own table + class XTag : EntityTagLink<X, TKey> {}
IEntityTaggableShared tag.EntityTag(Entity, EntityId, Name) tagging with no DDL of your own, or cross-entity tag questions reference Sencilla.Component.Tags.Mssql

Exactly one storage interface per entity — TagRegistrator fails at startup otherwise, as it does for a linked entity missing its (necessarily non-generic) link entity.

Layout

Contract/   IEntityTaggable + the three storage markers, ITagRepository
Entity/     TagName (what a valid tag IS) — EntityTagBase → EntityTagLink<TEntity,TKey> | EntityTag
Impl/Repo/  TagReadRepository (abstract) → TagInlineRepository
                                         → TagLinkedRepository → TagSharedRepository
Impl/Handlers/  ?tag= filtering, tags[] hydration, orphan sweeping
Impl/       TagRegistrator (wiring), TagsModelConfigurator (EF mapping), TagKey (key ⇄ text)
Web/        TagApiController — the four endpoints above
Database/   the [tag] schema package

The endpoints ship here rather than in CrudApiController, and mount themselves onto every [CrudApi] entity's route through Sencilla.Web's [EntityApi] seam — an open generic Controller<TEntity, TKey> marked with it is closed over each entity and routed under that entity's route. A host that doesn't reference this package therefore has no tag routes at all, rather than routes that answer 501.

The repositories are ReadRepository<TEntity, DynamicDbContext, TKey> subclasses, so the DbContext, the resolver, Save and the ambient-transaction helper come from the framework rather than being rebuilt. TagSharedRepository is a TagLinkedRepository whose foreign key was traded for a type discriminator plus a stringified id: it inherits every row-shaped operation and overrides only the four addressing seams. The link table's key is read and written through EF's model by name (EF.Property), which is what lets one implementation serve any adopter's link entity.

Guarantees

  • Normalised by TagName: lowercase, trimmed, deduped, ordinally sorted, charset a-z 0-9 - _ . : (: is for namespacing, e.g. promo:black-friday). Invalid input is rejected with an error code, never silently rewritten. This is what keeps ordinal comparison in a consumer and SQL Server's case-insensitive collation from ever disagreeing.
  • A tag write is a write of the tagged rowUpdatedDate moves and the audit log records it, so caches keyed on the row invalidate. Side-table writes are diffed (an unchanged set writes nothing) and atomic.
  • Authorisation comes from the parent. The endpoints load the tagged row through its own read repository first, so tag permissions are the row's permissions — tags are never a back door around them.
  • ?tag= composes with permission constraints and every other filter (it is a reading-pipeline handler), and ?tag= with only malformed values matches nothing rather than widening to everything.
  • Reading tags back is opt-in, on ?with=tags, so a list read pays the side table's second query only when the caller wants tags. It cannot be a real EF Include: Tags is a primitive collection for inline storage, ignored by the model for the other two, and the shared table has no foreign key to navigate — so FilterConstraintHandler drops with=tags as a non-navigation and the hydration handler gives it meaning. Inline entities ignore the flag; their column always arrives with the row. Single-row GET {route}/{id} cannot carry ?with= at all (IReadRepository.GetById takes no filter) — use GET {route}/{id}/tags.

Wiring

AddSencilla()'s assembly scan registers everything. AddSencillaTags() exists for hosts that skip the scan and is idempotent.

Storage notes: NULL means "no tags" (never []); consumers treat null, absent and non-array alike as empty. Soft-deleted rows keep their tags; hard deletes cascade (linked) or are swept (shared).

Product Compatible and additional computed target framework versions.
.NET 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
10.0.62 64 9/9/2026
10.0.61 40 9/9/2026
10.0.59 171 8/21/2026
10.0.58 98 8/21/2026
10.0.57 91 8/21/2026
10.0.56 89 8/21/2026
10.0.55 110 8/18/2026
10.0.54 105 8/14/2026
10.0.53 119 8/10/2026
10.0.52 108 8/7/2026
10.0.51 95 8/5/2026
10.0.50 90 8/5/2026
10.0.49 114 7/31/2026