Concepts
The Problem
Section titled “The Problem”Every entity that needs user comments requires the same boilerplate: a child entity with FK, EF configuration, CRUD actions, DTOs, endpoints, and permissions. This is 15-20 files of repetitive code per entity.
The Solution
Section titled “The Solution”Pragmatic.Comments is a trait plugin: add [HasComments] to any entity and the source generator produces everything. No manual wiring, no base classes to configure.
How It Works
Section titled “How It Works”[HasComments] on entity ↓ ForAttributeWithMetadataName (TraitFeature)CommentTraitTransform.Transform() ↓ CommentTraitModel ↓├── CommentEntityTemplate → ReservationComment.g.cs├── CommentEntityConfigTemplate → EntityConfig.ReservationComment.g.cs├── ParentTraitNavigationTemplate → Reservation partial (ICollection<> Comments)├── CommentActionsTemplate → Add/GetById/Update/Delete actions├── CommentDtoTemplate → ReservationCommentDto with Projection├── CommentListQueryTemplate → ListReservationCommentsQuery├── CommentPermissionsTemplate → Permission constants├── TraitEntityMetadataTemplate → Assembly metadata for host DbContext└── TraitEndpointModelBuilder → EndpointModels → EndpointsFeature handlersPipeline Injection Pattern
Section titled “Pipeline Injection Pattern”The SG cannot see its own output in the same Roslyn pass. So trait-generated actions and endpoints are injected programmatically into the existing pipelines:
- ActionModel →
ActionsFeature.Register(traitActions:)→ Invoker + SetDependencies - QueryModel →
QueryFeature.Register(resourceQueries:)→ Apply() + ToSpecification() - EndpointModel →
EndpointsFeature.Register(programmaticEndpoints:)→ HTTP handlers
This means trait actions get the full pipeline: DI, UnitOfWork, validation, activity tracing — identical to hand-written [DomainAction] classes.
Entity Architecture
Section titled “Entity Architecture”Each parent entity gets its own comment table — no shared polymorphic table:
Reservations ReservationComments┌──────────┐ ┌─────────────────────┐│ Id (PK) │◄────────│ ReservationId (FK) ││ ... │ │ PersistenceId (PK) │└──────────┘ │ Content │ │ AuthorId │ │ ReplyToId (self-FK) │ │ Status (varchar) │ │ IsDeleted │ └─────────────────────┘Benefits: full referential integrity, cascade delete, no EntityType/EntityId polymorphism, independent indexes per table.
Threading Model
Section titled “Threading Model”When AllowReplies = true (default), comments support nested threading via ReplyToId self-referencing FK:
Comment A (ReplyToId = null) ← top-level├── Comment B (ReplyToId = A.Id) ← reply to A│ └── Comment C (ReplyToId = B.Id) ← reply to B└── Comment D (ReplyToId = A.Id) ← another reply to AThe self-FK uses DeleteBehavior.Restrict to prevent orphaned replies.
Moderation
Section titled “Moderation”When RequireApproval = true:
- New comments start with
Status = PendingApproval - A
Moderate{Entity}CommentActionis generated - The query filter excludes
Rejectedcomments - Permission:
{boundary}.{entity}.comments.moderate
ICommentPolicy Lifecycle
Section titled “ICommentPolicy Lifecycle”Add Comment: 1. Resolve ICommentPolicy<TId> from DI (optional) 2. policy.CanAddAsync() → reject if false 3. Create entity, set fields from ICurrentUser + IClock 4. _db.Set<T>().Add(comment) 5. policy.OnAddedAsync() → notification hook
Update Comment: 1. policy.CanEditAsync() → reject if false 2. Fallback: author-only check if no policy 3. Edit window enforcement if configured 4. Set Content, IsEdited, UpdatedAt, UpdatedBy
Delete Comment: 1. policy.CanDeleteAsync() → reject if false 2. Soft-delete: IsDeleted, DeletedAt, DeletedBySub-Boundary Grouping
Section titled “Sub-Boundary Grouping”Comment actions are grouped in a sub-boundary interface:
public interface IBookingActions{ IBookingReservationCommentsActions ReservationComments { get; } // ...}
public interface IBookingReservationCommentsActions{ Task<Result<Guid, IError>> AddReservationComment(...); Task<VoidResult<IError>> UpdateReservationComment(...); Task<VoidResult<IError>> DeleteReservationComment(...); // ...}Customizable via [HasComments(SubBoundary = "CustomName")].
DbContext Integration
Section titled “DbContext Integration”The SG generates assembly metadata (_Metadata.TraitEntities.g.cs) that the host reads at compile time. The host’s DbContextFeature:
- Reads trait entity metadata from referenced assemblies
- Adds
DbSet<ReservationComment>to BoundaryDbContext + MigrationDbContext - Applies the
ReservationCommentEntityConfig(generated in module, public)
Schema metadata also includes the trait entity with resolved properties for Pragmatic.Migrations table creation.