Skip to content

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.

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.

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}Note entity derived from NoteBase<TEntityId>
  • EF configuration
  • add, update, delete, and get-by-id actions
  • note endpoints under the parent resource
  • a Notes navigation on the parent entity
<PackageReference Include="Pragmatic.Notes" />

Typical consumers also reference:

  • Pragmatic.Persistence
  • Pragmatic.Endpoints if HTTP endpoints are desired
[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
OptionDefaultDescription
MaxLength4000Maximum note content length.
AllowEditingtrueWhether authors can edit their own notes.
EditWindowMinutes-1Minutes after creation in which edits are allowed. -1 means no limit.
SubBoundary{Parent}NotesOverride generated sub-boundary name.

Generated notes derive from NoteBase<TEntityId> and include:

  • ParentEntityId
  • Content
  • AuthorId and AuthorName
  • IsEdited
  • CreatedAt, UpdatedAt, UpdatedBy
  • soft-delete fields

Unlike comments, notes are always internal and always require an author.

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.

VerbRouteMaps toRequest bodyResponse
POST/notesAddReservationNoteAction{ "content": string }201, new note Guid
GET/notesListReservationNotesQuery (paged)ReservationNoteDto[] (paged; Page/PageSize)
GET/notes/{noteId}GetReservationNoteActionReservationNoteDto
PUT/notes/{noteId}UpdateReservationNoteAction{ "content": string }204
DELETE/notes/{noteId}DeleteReservationNoteAction204 (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.

Two layers apply.

Permission constantsReservationNotePermissions (static class) exposes:

  • Create = {boundary}.reservation.notes.create
  • Read = {boundary}.reservation.notes.read
  • Update = {boundary}.reservation.notes.update
  • Delete = {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.

  • Pragmatic.Comments is for public or customer-facing discussion
  • Pragmatic.Notes is for staff-only internal annotations

Use one or both depending on the domain.

Local docs:

Related modules:

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