Pragmatic.Notes
Add internal staff notes to any Pragmatic entity with a single attribute.
Pragmatic.Notes is the “internal annotation” sibling of Pragmatic.Comments: flat notes, no threading, no anonymous authors, and no public-facing discussion model.
Status: implemented preview package. The package exists and works today; what was missing was public-ready documentation, not the core feature.
The Problem
Section titled “The Problem”Internal notes are common in line-of-business systems, but they still require repetitive boilerplate:
- child entity and FK
- CRUD actions
- endpoint mapping
- auditing fields
- rules around editing and visibility
Most projects end up rebuilding the same pattern repeatedly.
The Solution
Section titled “The Solution”One attribute activates the note feature:
using Pragmatic.Notes;using Pragmatic.Persistence.Entity;
[Entity<Guid>][Resource("reservations")][HasNotes<Reservation>]public partial class Reservation{ public string GuestName { get; set; } = "";}HasNotesAttribute<TParent> is generic (arity 1) — pass the parent entity type, e.g. [HasNotes<Reservation>]. [Resource(...)] is required for endpoints; without it the generator reports PRAG2601 and skips endpoint generation.
The generator creates:
- a typed
{Parent}Noteentity derived fromNoteBase<TEntityId> - EF configuration
- add, update, delete, and get-by-id actions
- note endpoints under the parent resource
- a
Notesnavigation on the parent entity
Installation
Section titled “Installation”<PackageReference Include="Pragmatic.Notes" />Typical consumers also reference:
Pragmatic.PersistencePragmatic.Endpointsif HTTP endpoints are desired
Quick Start
Section titled “Quick Start”[Entity<Guid>][Resource("reservations")][HasNotes<Reservation>(MaxLength = 4000, EditWindowMinutes = 60)]public partial class Reservation{ public string GuestName { get; set; } = "";}Generated surface typically includes:
- add note
- get note by id
- update note content
- delete or soft-delete a note
Attribute Options
Section titled “Attribute Options”| Option | Default | Description |
|---|---|---|
MaxLength | 4000 | Maximum note content length. |
AllowEditing | true | Whether authors can edit their own notes. |
EditWindowMinutes | -1 | Minutes after creation in which edits are allowed. -1 means no limit. |
SubBoundary | {Parent}Notes | Override generated sub-boundary name. |
Note Model
Section titled “Note Model”Generated notes derive from NoteBase<TEntityId> and include:
ParentEntityIdContentAuthorIdandAuthorNameIsEditedCreatedAt,UpdatedAt,UpdatedBy- soft-delete fields
Unlike comments, notes are always internal and always require an author.
Generated Endpoints
Section titled “Generated Endpoints”For [HasNotes<Reservation>] on Reservation ([Resource("reservations")]), endpoints are generated under /api/{boundary}/reservations/{reservationId}/notes (OpenAPI tag Notes). {boundary} is the lowercased boundary name from [BelongsTo<TBoundary>], or v1 if none. Generation requires HasPersistenceEFCore + HasActions; end-to-end compilation tests confirm [HasNotes] produces working code.
| Verb | Route | Maps to | Request body | Response |
|---|---|---|---|---|
POST | /notes | AddReservationNoteAction | { "content": string } | 201, new note Guid |
GET | /notes | ListReservationNotesQuery (paged) | — | ReservationNoteDto[] (paged; Page/PageSize) |
GET | /notes/{noteId} | GetReservationNoteAction | — | ReservationNoteDto |
PUT | /notes/{noteId} | UpdateReservationNoteAction | { "content": string } | 204 |
DELETE | /notes/{noteId} | DeleteReservationNoteAction | — | 204 (soft-delete) |
ReservationNoteDto shape: Id, ReservationId, Content, AuthorId, AuthorName, IsEdited, CreatedAt, UpdatedAt?. The route parameter {reservationId} binds to the action/query ReservationId property; {noteId} binds to NoteId.
Authorization and Visibility Constraints
Section titled “Authorization and Visibility Constraints”Two layers apply.
Permission constants — ReservationNotePermissions (static class) exposes:
Create={boundary}.reservation.notes.createRead={boundary}.reservation.notes.readUpdate={boundary}.reservation.notes.updateDelete={boundary}.reservation.notes.delete
({boundary} lowercased, or app when the parent has no [BelongsTo<TBoundary>].) These are constants the SG emits; the generated endpoints do not auto-bind them — apply them when wiring authorization on the endpoint group.
Author-ownership enforced in the action body — independent of permissions, UpdateReservationNoteAction checks note.AuthorId == ICurrentUser.Id and returns ForbiddenError otherwise, so only the original author can edit a note. When EditWindowMinutes > 0, edits past CreatedAt + EditWindowMinutes also return ForbiddenError. On a successful edit the action sets IsEdited = true, UpdatedAt, and UpdatedBy. Add records AuthorId/AuthorName from ICurrentUser; Delete is a soft-delete (IsDeleted/DeletedAt/DeletedBy) and reads exclude soft-deleted rows via the EF query filter.
Note: the Delete action body does not itself re-check author-ownership — gate delete via the Delete permission at the endpoint.
Relationship to Pragmatic.Comments
Section titled “Relationship to Pragmatic.Comments”Pragmatic.Commentsis for public or customer-facing discussionPragmatic.Notesis for staff-only internal annotations
Use one or both depending on the domain.
Documentation
Section titled “Documentation”Local docs:
Related modules:
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Notes is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).