1"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[22353],{79153(e,n,t){t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>m,frontMatter:()=>d,metadata:()=>o,toc:()=>l});var i=t(86070),r=t(81753),s=t(43292);const d={title:"Domain Events",sidebar_position:2,description:"Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline."},a="Domain Events",o={id:"domain-driven-design/domain-events",title:"Domain Events",description:"Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline.",source:"@site/versioned_docs/version-3.2.1/domain-driven-design/domain-events.mdx",sourceDirName:"domain-driven-design",slug:"/domain-driven-design/domain-events",permalink:"/docs/domain-driven-design/domain-events",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-3.2.1/domain-driven-design/domain-events.mdx",tags:[],version:"3.2.1",sidebarPosition:2,frontMatter:{title:"Domain Events",sidebar_position:2,description:"Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline."},sidebar:"docsSidebar",previous:{title:"Entities & Aggregate Roots",permalink:"/docs/domain-driven-design/entities-aggregates"},next:{title:"Value Objects",permalink:"/docs/domain-driven-design/value-objects"}},c={},l=[{value:"Installation",id:"installation",level:2},{value:"The IDomainEvent Interface",id:"the-idomainevent-interface",level:2},{value:"The DomainEvent Base Record",id:"the-domainevent-base-record",level:2},{value:"Defining Domain Events",id:"defining-domain-events",level:2},{value:"Raising Events from Aggregate Roots",id:"raising-events-from-aggregate-roots",level:2},{value:"How Events Flow Through the System",id:"how-events-flow-through-the-system",level:2},{value:"Handling Domain Events",id:"handling-domain-events",level:2},{value:"Removing or Cancelling Events",id:"removing-or-cancelling-events",level:2},{value:"Inspecting Pending Events",id:"inspecting-pending-events",level:2},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"domain-events",children:"Domain Events"}),"\n",(0,i.jsx)(n.p,{children:"Domain events represent something meaningful that happened within your domain. They are raised by aggregate roots when state changes occur, then dispatched after the unit of work commits. This keeps side effects (sending emails, updating read models, publishing to message brokers) decoupled from the core business logic."}),"\n",(0,i.jsx)(n.admonition,{title:"See it in a full example",type:"tip",children:(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.a,{href:"/docs/examples-recipes/domain-driven-design",children:"End-to-End DDD Recipe"})," raises ",(0,i.jsx)(n.code,{children:"TeamMemberAddedEvent"})," from ",(0,i.jsx)(n.code,{children:"Team.AddMember"})," and dispatches it to a real ",(0,i.jsx)(n.code,{children:"ISubscriber<TEvent>"})," via ",(0,i.jsx)(n.code,{children:"UnitOfWork.CommitAsync()"}),", in both the common single-scope case and a cross-scope mutation case."]})}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Entities"}),"\n",(0,i.jsx)(n.h2,{id:"the-idomainevent-interface",children:"The IDomainEvent Interface"}),"\n",(0,i.jsxs)(n.p,{children:["All domain events must implement ",(0,i.jsx)(n.code,{children:"IDomainEvent"}),", which extends ",(0,i.jsx)(n.code,{children:"ISerializableEvent"})," to make events compatible with the existing RCommon event routing pipeline:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public interface IDomainEvent : ISerializableEvent\n{\n /// <summary>\n /// Unique identifier for this event instance.\n /// </summary>\n Guid EventId { get; }\n\n /// <summary>\n /// The date and time when this event occurred.\n /// </summary>
1\n DateTimeOffset OccurredOn { get; }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"the-domainevent-base-record",children:"The DomainEvent Base Record"}),"\n",(0,i.jsxs)(n.p,{children:["Rather than implementing ",(0,i.jsx)(n.code,{children:"IDomainEvent"})," manually, extend the ",(0,i.jsx)(n.code,{children:"DomainEvent"})," abstract record. It automatically assigns a unique ",(0,i.jsx)(n.code,{children:"EventId"})," and captures ",(0,i.jsx)(n.code,{children:"OccurredOn"})," at construction time:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public abstract record DomainEvent : IDomainEvent\n{\n public Guid EventId { get; init; } = Guid.NewGuid();\n public DateTimeOffset OccurredOn { get; init; } = DateTimeOffset.UtcNow;\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Using a C# ",(0,i.jsx)(n.code,{children:"record"})," gives you structural equality, ",(0,i.jsx)(n.code,{children:"with"}),"-expression support, and immutability without boilerplate."]}),"\n",(0,i.jsx)(n.h2,{id:"defining-domain-events",children:"Defining Domain Events"}),"\n",(0,i.jsxs)(n.p,{children:["Create concrete domain events by extending ",(0,i.jsx)(n.code,{children:"DomainEvent"})," and adding the properties that describe what happened:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public record OrderCreatedEvent(Guid OrderId, string CustomerId) : DomainEvent;\n\npublic record OrderLineAddedEvent(Guid OrderId, string ProductId, int Quantity) : DomainEvent;\n\npublic record OrderSubmittedEvent(Guid OrderId) : DomainEvent;\n\npublic record CustomerEmailChangedEvent(\n Guid CustomerId,\n string OldEmail,\n string NewEmail) : DomainEvent;\n"})}),"\n",(0,i.jsx)(n.p,{children:"Keep domain events in the past tense. They describe facts about what has already happened, not intentions about what should happen next."}),"\n",(0,i.jsx)(n.h2,{id:"raising-events-from-aggregate-roots",children:"Raising Events from Aggregate Roots"}),"\n",(0,i.jsxs)(n.p,{children:["Domain events are raised inside aggregate root methods using ",(0,i.jsx)(n.code,{children:"AddDomainEvent"}),". Events accumulate on the aggregate until the unit of work completes:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'public class Order : AggregateRoot<Guid>\n{\n public string CustomerId { get; private set; }\n public OrderStatus Status { get; private set; }\n\n public Order(Guid id, string customerId)\n {\n Id = id;\n CustomerId = customerId;\n Status = OrderStatus.Draft;\n IncrementVersion();\n AddDomainEvent(new OrderCreatedEvent(id, customerId));\n }\n\n public void Submit()\n {\n if (Status != OrderStatus.Draft)\n throw new InvalidOperationException("Only draft orders can be submitted.");\n\n Status = OrderStatus.Submitted;\n IncrementVersion();\n AddDomainEvent(new OrderSubmittedEvent(Id));\n }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"AddDomainEvent"})," method places the event in both:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"DomainEvents"})," \u2014 a typed ",(0,i.jsx)(n.code,{children:"IReadOnlyCollection<IDomainEvent>"})," specific to the aggregate root"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"LocalEvents"})," \u2014 the inherited ",(0,i.jsx)(n.code,{children:"BusinessEntity"})," collection consumed by the event tracking infrastructure"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Both collections stay in sync through ",(0,i.jsx)(n.code,{children:"AddDomainEvent"}),", ",(0,i.jsx)(n.code,{children:"RemoveDomainEvent"}),", and ",(0,i.jsx)(n.code,{children:"ClearDomainEvents"}),". Do not call the inherited ",(0,i.jsx)(n.code,{children:"AddLocalEvent"})," directly on an aggregate root, as this would break the dual-list invariant."]}),"\n",(0,i.jsx)(n.h2,{id:"how-events-flow-through-the-system",children:"How Events Flow Through the System"}),"\n",(0,i.jsx)(n.p,{children:"After your aggregate is saved through a repository, the event tracking pipeline dispatches all accumulated events:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"1. Aggregate raises events via AddDomainEvent()\n |\n v\n2. Repository saves the aggregate, then calls IEntityEventTracker.AddEntity(aggregate)\n |\n v\n3. IEntityEventTracker.EmitTransactionalEventsAsync()\n \u2014 traverses the aggregate's object graph for IBusinessEntity instances\n \u2014 collects LocalEvents from the aggregate root (and any nested IBusinessEntity children)\n |\n v\n4. IEventRouter.AddTransactionalEvents() + RouteEventsAsync()\n \u2014 routes events through registered IEventProducer implementations\n |\n v\n5. IEventProducer dispatches events to subscribers\n \u2014 in-memory via IEventBus\n \u2014 or to external brokers (MassTransit, Wolverine, etc.)\n"})}),"\n",(0,i.jsxs)(n.p,{children:["This pipeline is provided by ",(0,i.jsx)(n.code,{children:"RCommon.Core"})," and ",(0,i.jsx)(n.code,{children:"RCommon.Entities"})," with no extra configuration needed when you use the standard repository implementations."]}),"\n",(0,i.jsx)(n.h2,{id:"handling-domain-events",children:"Handling Domain Events"}),"\n",(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.code,{children:"ISubscriber<TEvent>"})," to handle a domain event. The framework resolves handlers from the DI container when events are published via ",(0,i.jsx)(n.code,{children:"IEventBus"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class OrderSubmittedHandler : ISubscriber<OrderSubmittedEvent>\n{\n private readonly IEmailService _emailService;\n private readonly IOrderReadModelRepository _readModels;\n\n public OrderSubmittedHandler(IEmailService emailService, IOrderReadModelRepository readModels)\n {\n _emailService = emailService;\n _readModels = readModels;\n }\n\n public async Task HandleAsync(OrderSubmittedEvent @event, CancellationToken cancellationToken = default)\n {\n // Update a read model\n await _readModels.MarkSubmittedAsync(@event.OrderId, cancellationToken);\n\n // Send a notification\n await _emailService.SendOrderConfirmationAsync(@event.OrderId, cancellationToken);\n }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["A handler can implement ",(0,i.jsx)(n.code,{children:"ISubscriber<TEvent>"})," for multiple event types if needed:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'public class OrderAuditHandler\n : ISubscriber<OrderCreatedEvent>,\n ISubscriber<OrderSubmittedEvent>\n{\n public Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default)\n =>
1 LogAsync("created", @event.OrderId, cancellationToken);\n\n public Task HandleAsync(OrderSubmittedEvent @event, CancellationToken cancellationToken = default)\n => LogAsync("submitted", @event.OrderId, cancellationToken);\n\n private Task LogAsync(string action, Guid orderId, CancellationToken ct) { ... }\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"removing-or-cancelling-events",children:"Removing or Cancelling Events"}),"\n",(0,i.jsx)(n.p,{children:"If business logic determines an event should not be dispatched (for example, a validation step fails after the event was raised), remove it before the unit of work completes:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void CancelSubmission()\n{\n if (Status == OrderStatus.Submitted)\n {\n Status = OrderStatus.Draft;\n // Remove the previously raised event if still pending\n var pending = DomainEvents.OfType<OrderSubmittedEvent>().FirstOrDefault();\n if (pending is not null)\n RemoveDomainEvent(pending);\n\n AddDomainEvent(new OrderSubmissionCancelledEvent(Id));\n }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"inspecting-pending-events",children:"Inspecting Pending Events"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"DomainEvents"})," property on an aggregate root exposes all events that have been raised but not yet dispatched. This is useful for testing:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'var order = new Order(Guid.NewGuid(), "C42");\norder.Submit();\n\nAssert.Single(order.DomainEvents.OfType<OrderSubmittedEvent>());\n'})}),"\n",(0,i.jsxs)(n.p,{children:["After the repository dispatches events, call ",(0,i.jsx)(n.code,{children:"ClearDomainEvents()"})," or allow the infrastructure to clear them automatically. The ",(0,i.jsx)(n.code,{children:"InMemoryEntityEventTracker"})," does not clear events after dispatch \u2014 the repository or unit of work implementation is responsible for this depending on your persistence layer."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IDomainEvent"})}),(0,i.jsxs)(n.td,{children:["Interface for all domain events; extends ",(0,i.jsx)(n.code,{children:"ISerializableEvent"})," with ",(0,i.jsx)(n.code,{children:"EventId"})," and ",(0,i.jsx)(n.code,{children:"OccurredOn"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"DomainEvent"})}),(0,i.jsxs)(n.td,{children:["Abstract base record; auto-generates ",(0,i.jsx)(n.code,{children:"EventId"})," and ",(0,i.jsx)(n.code,{children:"OccurredOn"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISubscriber<TEvent>"})}),(0,i.jsx)(n.td,{children:"Interface for event handlers; resolved from DI on event publication"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventBus"})}),(0,i.jsx)(n.td,{children:"In-process event bus for publishing and subscribing to events"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventRouter"})}),(0,i.jsxs)(n.td,{children:["Routes transactional events to registered ",(0,i.jsx)(n.code,{children:"IEventProducer"})," implementations"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventProducer"})}),(0,i.jsx)(n.td,{children:"Dispatches events to their destination (in-memory, broker, etc.)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEntityEventTracker"})}),(0,i.jsx)(n.td,{children:"Collects entities and emits their transactional events after persistence"})]})]})]})]})}function m(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},43292(e,n,t){t.d(n,{A:()=>l});var i=t(30758);const r="container_xjrG",s="label_Y4p8",d="commandRow_FY5I",a="command_m7Qs",o="copyButton_u1GK";var c=t(86070);function l({packageName:e,version:n}){const[t,l]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:r,children:[(0,c.jsx)("div",{className:s,children:"NuGet Package"}),(0,c.jsxs)("div",{className:d,children:[(0,c.jsx)("code",{className:a,children:h}),(0,c.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>d,x:()=>a});var i=t(30758);const r={},s=i.createContext(r);function d(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.