Skip to content

Pragmatic.Comments

Add threaded, moderated comments to any entity with a single attribute.

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 entity
public class ReservationComment { ... } // Entity
public class ReservationCommentConfig { ... } // EF config
public class AddReservationCommentAction { ... } // Create action
public class UpdateReservationComment { ... } // Update action
public class DeleteReservationComment { ... } // Delete action
public class ListReservationComments { ... } // Query
public class ReservationCommentDto { ... } // DTO
public class CommentEndpoints { ... } // HTTP handlers
// + Invokers, SetDependencies, permissions, request DTOs...

One attribute, zero boilerplate. The source generator creates everything:

[Entity<Guid>]
[Resource("reservations")]
[HasComments] // <-- this is all you write
public 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.

<PackageReference Include="Pragmatic.Comments" />

No additional configuration needed. The SG detects [HasComments] automatically.

using Pragmatic.Comments;
using Pragmatic.Persistence.Entity;
[Entity<Guid>]
[Resource("reservations")]
[HasComments]
[BelongsTo<BookingBoundary>]
public partial class Reservation { ... }
POST /api/booking/reservations/{id}/comments → Add comment
GET /api/booking/reservations/{id}/comments → List (paged)
GET /api/booking/reservations/{id}/comments/{id} → Get detail
PUT /api/booking/reservations/{id}/comments/{id} → Update
DELETE /api/booking/reservations/{id}/comments/{id} → Soft-delete
// Actions in IBookingReservationCommentsActions sub-boundary
var result = await actions.ReservationComments.AddReservationComment(
new AddReservationCommentAction
{
ReservationId = reservationId,
Content = "Great stay!",
}, ct);

All options are on the [HasComments] attribute:

OptionDefaultDescription
MaxLength2000Maximum content length
AllowRepliestrueEnable threaded replies (self-referencing FK)
AllowEditingtrueAuthors can edit their own comments
EditWindowMinutes-1 (no limit)Time limit for editing after creation
RequireApprovalfalseNew comments start as PendingApproval
SupportInternalNotesfalseEnable Public/Internal visibility
AllowAnonymousfalseAllow null AuthorId
SubBoundary{Entity}CommentsOverride sub-boundary name

Every generated comment entity inherits CommentBase<TEntityId>:

PropertyTypeDescription
IdGuidPrimary key
ParentEntityIdTEntityIdFK to parent (abstract, concrete generated)
ContentstringComment text
AuthorIdstring?User who created
AuthorNamestring?Display name snapshot
ReplyToIdGuid?Parent comment for threading
StatusCommentStatusVisible, PendingApproval, Rejected, Hidden
VisibilityCommentVisibilityPublic or Internal
IsEditedboolTrue after first edit
CreatedAtDateTimeOffsetCreation timestamp
UpdatedAtDateTimeOffset?Last edit timestamp
UpdatedBystring?Who last edited
IsDeletedboolSoft-delete flag
DeletedAtDateTimeOffset?When deleted
DeletedBystring?Who deleted
Metadatastring?JSON extensibility field

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

ReservationCommentPermissions.Create // "booking.reservation.comments.create"
ReservationCommentPermissions.Read // "booking.reservation.comments.read"
ReservationCommentPermissions.Update // "booking.reservation.comments.update"
ReservationCommentPermissions.Delete // "booking.reservation.comments.delete"

From a single [HasComments] on an entity:

ArtifactHint NameDescription
Entity class{Entity}Comment.Entity.g.csConcrete entity with typed FK
EF ConfigEntityConfig.{Entity}Comment.g.csPK, FK, indexes, soft-delete filter
Parent navigation{Entity}.TraitNavigations.g.csICollection<{E}Comment> Comments
Add actionAdd{E}CommentAction.Action.g.cs+ Invoker + SetDependencies
GetById actionGet{E}CommentAction.Action.g.cs+ Invoker + SetDependencies
Update actionUpdate{E}CommentAction.Action.g.cs+ Invoker + SetDependencies
Delete actionDelete{E}CommentAction.Action.g.cs+ Invoker + SetDependencies
List queryList{E}CommentsQuery.QueryClass.g.cs+ Apply() from QueryFeature
DTO{E}CommentDto.Dto.g.csWith Projection expression
Permissions{E}.CommentPermissions.g.csPermission constants
Metadata_Metadata.TraitEntities.g.csFor host DbContext discovery
5 Endpoints*.Endpoint.g.csPOST/GET/PUT/DELETE handlers
  • .NET 10+
  • Pragmatic.Abstractions (transitive)
  • Pragmatic.SourceGenerator (analyzer)
  • [Resource("segment")] on entity for endpoint generation
  • [Entity<TId>] on entity

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