Pragmatic.Comments
Add threaded, moderated comments to any entity with a single attribute.
The Problem
Section titled “The Problem”Adding comments to a domain entity requires creating a child entity, EF configuration, FK relationships, CRUD actions, HTTP endpoints, permissions, and DTOs. For each entity that needs comments, you repeat 15+ files of boilerplate:
// Without Pragmatic.Comments: 15+ files per entitypublic class ReservationComment { ... } // Entitypublic class ReservationCommentConfig { ... } // EF configpublic class AddReservationCommentAction { ... } // Create actionpublic class UpdateReservationComment { ... } // Update actionpublic class DeleteReservationComment { ... } // Delete actionpublic class ListReservationComments { ... } // Querypublic class ReservationCommentDto { ... } // DTOpublic class CommentEndpoints { ... } // HTTP handlers// + Invokers, SetDependencies, permissions, request DTOs...The Solution
Section titled “The Solution”One attribute, zero boilerplate. The source generator creates everything:
[Entity<Guid>][Resource("reservations")][HasComments] // <-- this is all you writepublic partial class Reservation { ... }The SG generates 20+ files: entity, EF config with FK/indexes/soft-delete, Add/GetById/Update/Delete/Moderate actions with Invokers, paged list query, DTO with Projection, HTTP endpoints, and permission constants.
Installation
Section titled “Installation”<PackageReference Include="Pragmatic.Comments" />No additional configuration needed. The SG detects [HasComments] automatically.
Quick Start
Section titled “Quick Start”1. Add the attribute
Section titled “1. Add the attribute”using Pragmatic.Comments;using Pragmatic.Persistence.Entity;
[Entity<Guid>][Resource("reservations")][HasComments][BelongsTo<BookingBoundary>]public partial class Reservation { ... }2. Use the generated endpoints
Section titled “2. Use the generated endpoints”POST /api/booking/reservations/{id}/comments → Add commentGET /api/booking/reservations/{id}/comments → List (paged)GET /api/booking/reservations/{id}/comments/{id} → Get detailPUT /api/booking/reservations/{id}/comments/{id} → UpdateDELETE /api/booking/reservations/{id}/comments/{id} → Soft-delete3. Use via boundary interface
Section titled “3. Use via boundary interface”// Actions in IBookingReservationCommentsActions sub-boundaryvar result = await actions.ReservationComments.AddReservationComment( new AddReservationCommentAction { ReservationId = reservationId, Content = "Great stay!", }, ct);Configuration
Section titled “Configuration”All options are on the [HasComments] attribute:
| Option | Default | Description |
|---|---|---|
MaxLength | 2000 | Maximum content length |
AllowReplies | true | Enable threaded replies (self-referencing FK) |
AllowEditing | true | Authors can edit their own comments |
EditWindowMinutes | -1 (no limit) | Time limit for editing after creation |
RequireApproval | false | New comments start as PendingApproval |
SupportInternalNotes | false | Enable Public/Internal visibility |
AllowAnonymous | false | Allow null AuthorId |
SubBoundary | {Entity}Comments | Override sub-boundary name |
Comment Entity (CommentBase)
Section titled “Comment Entity (CommentBase)”Every generated comment entity inherits CommentBase<TEntityId>:
| Property | Type | Description |
|---|---|---|
Id | Guid | Primary key |
ParentEntityId | TEntityId | FK to parent (abstract, concrete generated) |
Content | string | Comment text |
AuthorId | string? | User who created |
AuthorName | string? | Display name snapshot |
ReplyToId | Guid? | Parent comment for threading |
Status | CommentStatus | Visible, PendingApproval, Rejected, Hidden |
Visibility | CommentVisibility | Public or Internal |
IsEdited | bool | True after first edit |
CreatedAt | DateTimeOffset | Creation timestamp |
UpdatedAt | DateTimeOffset? | Last edit timestamp |
UpdatedBy | string? | Who last edited |
IsDeleted | bool | Soft-delete flag |
DeletedAt | DateTimeOffset? | When deleted |
DeletedBy | string? | Who deleted |
Metadata | string? | JSON extensibility field |
ICommentPolicy (Hooks)
Section titled “ICommentPolicy (Hooks)”Implement ICommentPolicy<TEntityId> to customize behavior:
public class ReservationCommentPolicy : ICommentPolicy<Guid>{ public async Task<bool> CanAddAsync(Guid reservationId, string content, string? authorId, CancellationToken ct) { // Custom validation: check reservation status, rate limiting, etc. return true; }
public async Task OnAddedAsync(Guid reservationId, Guid commentId, CancellationToken ct) { // Send notification, update counter, etc. }
public async Task<bool> CanDeleteAsync(Guid commentId, string? requesterId, CancellationToken ct) { // Allow admins to delete any comment return true; }
public async Task<bool> CanEditAsync(Guid commentId, string? requesterId, CancellationToken ct) { // Custom edit permission logic return true; }}Register in DI:
services.AddScoped<ICommentPolicy<Guid>, ReservationCommentPolicy>();If no policy is registered, default behavior applies (author-only edit, anyone can delete).
Generated Permissions
Section titled “Generated Permissions”ReservationCommentPermissions.Create // "booking.reservation.comments.create"ReservationCommentPermissions.Read // "booking.reservation.comments.read"ReservationCommentPermissions.Update // "booking.reservation.comments.update"ReservationCommentPermissions.Delete // "booking.reservation.comments.delete"What Gets Generated
Section titled “What Gets Generated”From a single [HasComments] on an entity:
| Artifact | Hint Name | Description |
|---|---|---|
| Entity class | {Entity}Comment.Entity.g.cs | Concrete entity with typed FK |
| EF Config | EntityConfig.{Entity}Comment.g.cs | PK, FK, indexes, soft-delete filter |
| Parent navigation | {Entity}.TraitNavigations.g.cs | ICollection<{E}Comment> Comments |
| Add action | Add{E}CommentAction.Action.g.cs | + Invoker + SetDependencies |
| GetById action | Get{E}CommentAction.Action.g.cs | + Invoker + SetDependencies |
| Update action | Update{E}CommentAction.Action.g.cs | + Invoker + SetDependencies |
| Delete action | Delete{E}CommentAction.Action.g.cs | + Invoker + SetDependencies |
| List query | List{E}CommentsQuery.QueryClass.g.cs | + Apply() from QueryFeature |
| DTO | {E}CommentDto.Dto.g.cs | With Projection expression |
| Permissions | {E}.CommentPermissions.g.cs | Permission constants |
| Metadata | _Metadata.TraitEntities.g.cs | For host DbContext discovery |
| 5 Endpoints | *.Endpoint.g.cs | POST/GET/PUT/DELETE handlers |
Requirements
Section titled “Requirements”- .NET 10+
Pragmatic.Abstractions(transitive)Pragmatic.SourceGenerator(analyzer)[Resource("segment")]on entity for endpoint generation[Entity<TId>]on entity
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Comments is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).