-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeed.json
139 lines (123 loc) · 71.4 KB
/
feed.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
{
"version": "https://jsonfeed.org/version/1",
"title": "Saji Weerasingham",
"icon": "https://micro.blog/sajilicious/avatar.jpg",
"home_page_url": "https://sajilicious.micro.blog/",
"feed_url": "https://sajilicious.micro.blog/feed.json",
"items": [
{
"id": "http://sajilicious.micro.blog/2025/02/27/and-its-hard-to-hold.html",
"content_html": "<p>And it’s hard to hold a candle, in the cold Manchester rain #munips #mufc #manchesterunited</p>\n",
"content_text": "And it's hard to hold a candle, in the cold Manchester rain #munips #mufc #manchesterunited\n",
"date_published": "2025-02-27T05:58:23+10:00",
"url": "https://sajilicious.micro.blog/2025/02/27/and-its-hard-to-hold.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/25/make-your-own-mind-up.html",
"content_html": "<p>Make your own mind up on the PM’s performance on #qanda #auspol</p>\n<p><a href=\"https://www.youtube.com/live/mx8k9k83sWw?si=WDeJL9w48Vv_5DBG\">www.youtube.com/live/mx8k…</a></p>\n",
"content_text": "Make your own mind up on the PM's performance on #qanda #auspol\r\n\r\n[www.youtube.com/live/mx8k...](https://www.youtube.com/live/mx8k9k83sWw?si=WDeJL9w48Vv_5DBG)\n",
"date_published": "2025-02-25T06:03:55+10:00",
"url": "https://sajilicious.micro.blog/2025/02/25/make-your-own-mind-up.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/24/great-video-with-examples-that.html",
"content_html": "<p>Great video, with examples, that explains how preferential voting works in #auspol</p>\n<p><a href=\"https://youtu.be/YiLAx7kp4Rc?si=Oy0jLQMpZzB1mkwh\">youtu.be/YiLAx7kp4…</a></p>\n",
"content_text": "Great video, with examples, that explains how preferential voting works in #auspol\r\n\r\n[youtu.be/YiLAx7kp4...](https://youtu.be/YiLAx7kp4Rc?si=Oy0jLQMpZzB1mkwh)\n",
"date_published": "2025-02-24T12:25:58+10:00",
"url": "https://sajilicious.micro.blog/2025/02/24/great-video-with-examples-that.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/24/its-a-shame-to-see.html",
"content_html": "<p>It’s a shame to see that our Matildas are just not in the same class as best national women’s teams, despite having a lot a players playing in the English WSL #matildas</p>\n<p><!-- raw HTML omitted --><!-- raw HTML omitted --></p>\n",
"content_text": "It's a shame to see that our Matildas are just not in the same class as best national women's teams, despite having a lot a players playing in the English WSL #matildas\r\n\n\n<img src=\"uploads/2025/sofascore-1740354555.png\" width=\"385\" height=\"600\" alt=\"\"><img src=\"uploads/2025/sofascore-1740354526.png\" width=\"364\" height=\"600\" alt=\"\">\n",
"date_published": "2025-02-24T09:53:07+10:00",
"url": "https://sajilicious.micro.blog/2025/02/24/its-a-shame-to-see.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/22/maguire-ballwatching-on-that-second.html",
"content_html": "<p>Maguire, ball-watching on that second goal, as usual.</p>\n<p>He never scans! #manutd #manchesterunited</p>\n",
"content_text": "Maguire, ball-watching on that second goal, as usual.\r\n\r\nHe never scans! #manutd #manchesterunited \n",
"date_published": "2025-02-22T23:37:47+10:00",
"url": "https://sajilicious.micro.blog/2025/02/22/maguire-ballwatching-on-that-second.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/22/elon-wont-be-able-to.html",
"content_html": "<p>#elon won’t be able to cut $2 trillion from the US federal budget, here’s why: <a href=\"https://youtu.be/lG9pxvpGY-Q?si=vIefVZreW5mDAAvg\">youtu.be/lG9pxvpGY…</a></p>\n<p>#trump #politics</p>\n",
"content_text": "#elon won't be able to cut $2 trillion from the US federal budget, here's why: [youtu.be/lG9pxvpGY...](https://youtu.be/lG9pxvpGY-Q?si=vIefVZreW5mDAAvg)\r\n\r\n#trump #politics\n",
"date_published": "2025-02-22T17:07:52+10:00",
"url": "https://sajilicious.micro.blog/2025/02/22/elon-wont-be-able-to.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/22/nuclearfusion-reactor-run-for-minutes.html",
"content_html": "<p>#nuclearfusion reactor run for 22 minutes in France #energytransition #netzero</p>\n<p><a href=\"https://newatlas.com/energy/france-tokamak-cea-west-fusion-reactor-record-plasma-duration/\">newatlas.com/energy/fr…</a></p>\n",
"content_text": "#nuclearfusion reactor run for 22 minutes in France #energytransition #netzero\r\n\r\n[newatlas.com/energy/fr...](https://newatlas.com/energy/france-tokamak-cea-west-fusion-reactor-record-plasma-duration/)\n",
"date_published": "2025-02-22T15:44:04+10:00",
"url": "https://sajilicious.micro.blog/2025/02/22/nuclearfusion-reactor-run-for-minutes.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/22/toxic-culture-in-spanish-football.html",
"content_html": "<p>Toxic culture in Spanish football as exposed by the Rubiales kiss #womensfootball</p>\n<p><a href=\"https://youtu.be/nbLH9sXAGLE?si=tS1QYR2ncIwQmrWV\">youtu.be/nbLH9sXAG…</a></p>\n",
"content_text": "Toxic culture in Spanish football as exposed by the Rubiales kiss #womensfootball\r\n\r\n[youtu.be/nbLH9sXAG...](https://youtu.be/nbLH9sXAGLE?si=tS1QYR2ncIwQmrWV)\n",
"date_published": "2025-02-22T06:05:51+10:00",
"url": "https://sajilicious.micro.blog/2025/02/22/toxic-culture-in-spanish-football.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/21/the-worlds-largest-electric-ferry.html",
"content_html": "<p>The world’s largest electric ferry #renewableenergy</p>\n<p><a href=\"https://youtu.be/4rR57tXdOHQ?si=UDzJPnLmhUPfEq6t\">youtu.be/4rR57tXdO…</a></p>\n",
"content_text": "The world's largest electric ferry #renewableenergy\r\n\r\n[youtu.be/4rR57tXdO...](https://youtu.be/4rR57tXdOHQ?si=UDzJPnLmhUPfEq6t)\n",
"date_published": "2025-02-22T05:13:27+10:00",
"url": "https://sajilicious.micro.blog/2025/02/21/the-worlds-largest-electric-ferry.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/21/dont-believe-everything-you-hear.html",
"content_html": "<p>Don’t believe everything you hear about making the public service smaller and more efficient #auspol</p>\n<p><a href=\"https://youtu.be/j2X7ICTqiOc?si=DFUxRS32oz5sMyhz\">youtu.be/j2X7ICTqi…</a></p>\n",
"content_text": "Don't believe everything you hear about making the public service smaller and more efficient #auspol\r\n\r\n[youtu.be/j2X7ICTqi...](https://youtu.be/j2X7ICTqiOc?si=DFUxRS32oz5sMyhz)\n",
"date_published": "2025-02-21T12:46:44+10:00",
"url": "https://sajilicious.micro.blog/2025/02/21/dont-believe-everything-you-hear.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/21/if-youre-a-windows-user.html",
"content_html": "<p>If you’re a Windows user and wished File Explorer could do more, then check these out #windows #fileexplorer</p>\n<p><a href=\"https://youtu.be/uDUQrC5YxT0?si=A8wzTheIsWVvFiP5\">youtu.be/uDUQrC5Yx…</a></p>\n",
"content_text": "If you're a Windows user and wished File Explorer could do more, then check these out #windows #fileexplorer\r\n\r\n[youtu.be/uDUQrC5Yx...](https://youtu.be/uDUQrC5YxT0?si=A8wzTheIsWVvFiP5)\n",
"date_published": "2025-02-21T11:26:21+10:00",
"url": "https://sajilicious.micro.blog/2025/02/21/if-youre-a-windows-user.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/21/great-chat-about-manchesterunited-especially.html",
"content_html": "<p>Great chat about #manchesterunited especially Ten Hag’s man-management issues, the precarious financial position, where the #glazersout movement is, and how Sir Jim is going so far</p>\n<p><a href=\"https://open.spotify.com/episode/4WYeg7duHhs515R30owBIQ?si=mpmxpkPIR1mz_F-Nb38h0g\">open.spotify.com/episode/4…</a></p>\n",
"content_text": "Great chat about #manchesterunited especially Ten Hag's man-management issues, the precarious financial position, where the #glazersout movement is, and how Sir Jim is going so far\r\n\r\n[open.spotify.com/episode/4...](https://open.spotify.com/episode/4WYeg7duHhs515R30owBIQ?si=mpmxpkPIR1mz_F-Nb38h0g)\n",
"date_published": "2025-02-21T06:17:44+10:00",
"url": "https://sajilicious.micro.blog/2025/02/21/great-chat-about-manchesterunited-especially.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/21/i-found-this-super-interesting.html",
"content_html": "<p>I found this super interesting #football #footballanalytics #ai #bigdata</p>\n<p><a href=\"https://open.spotify.com/episode/1c6H6gR5eB91Wg2YoGmaiM?si=wtBdDOt2SfeFpjU_LOBArA\">open.spotify.com/episode/1…</a></p>\n",
"content_text": "I found this super interesting #football #footballanalytics #ai #bigdata\r\n\r\n[open.spotify.com/episode/1...](https://open.spotify.com/episode/1c6H6gR5eB91Wg2YoGmaiM?si=wtBdDOt2SfeFpjU_LOBArA)\n",
"date_published": "2025-02-21T05:25:16+10:00",
"url": "https://sajilicious.micro.blog/2025/02/21/i-found-this-super-interesting.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/20/good-history-lesson-on-australian.html",
"content_html": "<p>Good history lesson on Australian interest rates and why things are so expensive.</p>\n<p>Spoiler, it’s not a domestic issue.</p>\n<p>#auspol #costoflivingcrisis\n<a href=\"https://open.spotify.com/episode/0a94fRXCVwYmxInr7iBlOW?si=a0OsrIdYSR6le93YnpJijw\">open.spotify.com/episode/0…</a></p>\n",
"content_text": "Good history lesson on Australian interest rates and why things are so expensive.\r\n\r\nSpoiler, it's not a domestic issue.\r\n\r\n#auspol #costoflivingcrisis\r\n[open.spotify.com/episode/0...](https://open.spotify.com/episode/0a94fRXCVwYmxInr7iBlOW?si=a0OsrIdYSR6le93YnpJijw)\r\n",
"date_published": "2025-02-20T16:02:50+10:00",
"url": "https://sajilicious.micro.blog/2025/02/20/good-history-lesson-on-australian.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/16/informative-video-listing-the-events.html",
"content_html": "<p>Informative video listing the events that shaped Trump’s idea to acquire Palestinian-occupied territories for American real estate developments #gaza #westbank</p>\n<p><a href=\"https://youtu.be/DdUGdSEXkQU?si=lgPUu0tovHUPBdEs\">youtu.be/DdUGdSEXk…</a></p>\n",
"content_text": "Informative video listing the events that shaped Trump's idea to acquire Palestinian-occupied territories for American real estate developments #gaza #westbank\r\n\r\n[youtu.be/DdUGdSEXk...](https://youtu.be/DdUGdSEXkQU?si=lgPUu0tovHUPBdEs)\n",
"date_published": "2025-02-16T06:34:33+10:00",
"url": "https://sajilicious.micro.blog/2025/02/16/informative-video-listing-the-events.html"
},
{
"id": "http://sajilicious.micro.blog/2025/02/15/domain-event-dispatching-using-the.html",
"title": "Domain event dispatching using the outbox pattern with Entity Framework",
"content_html": "<h2 id=\"what-is-domain-event-dispatching\">What is domain event dispatching?</h2>\n<p>Domain event dispatching is a concept that related to <a href=\"https://martinfowler.com/bliki/DomainDrivenDesign.html\">domain-driven design</a>, or DDD as it’s also known.</p>\n<p>Having said that, event dispatching is central to any <a href=\"https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven\">event-driven architecture</a>, which follows the <a href=\"https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber\">publisher-subscriber pattern</a>.</p>\n<p>Now, I’ve not actually read Eric Evans' seminal book on domain-driven design, <a href=\"https://www.amazon.com.au/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215\">Domain-driven Design: Tackling Complexity in the Heart of Software</a>, so I’m unsure whether Evans suggests whether domain events should be published as part of the transaction that creates them, or have the event persisted with the change in application state and then later published using an <a href=\"https://codeopinion.com/outbox-pattern-reliably-save-state-publish-events\">outbox pattern</a>.</p>\n<p>I prefer the outbox pattern for domain event dispatching because I don’t think you want a scenario where data is persisted and an event is raised that has multiple subscribers, but one of the subscribers fails to execute, causing the whole transation to be rolled back.</p>\n<p>You then get an inconsistent scenario where certain event handlers have been handled, but the data that relates to those actions does not exist. Why send a confirmation email for an account that wasn’t successfully regsitered.</p>\n<p>Or, conversely, the transaction is not rolled back and one subscriber has failed to execute (including retries with back-off).</p>\n<p>The outbox pattern, keeps a record of whether the event has been dispatched or “sent”, behaving a bit like a service bus.</p>\n<p>In this blog post, I’m going to explore how an application using <a href=\"https://learn.microsoft.com/en-us/aspnet/entity-framework\">Entity Framework</a> as an ORM can use an outbox pattern to publish domain events that are persisted with the application data.</p>\n<p>The packages used will be Entity Framewok and I will leverage <code>INotification</code> in <a href=\"https://github.com/jbogard/MediatR\">MediatR</a> to assist with the publisher-subscriber implementation.</p>\n<p>All of these code samples are taken from my <a href=\"https://github.com/TheMagnificent11/lewee\">Lewee</a> project.</p>\n<h2 id=\"domain-event\">Domain Event</h2>\n<p>Here’s rough representation of a domain event.</p>\n<p>I’ve included a <code>CorrelationId</code> property that I believe should be set to allow you to correlate the logs from the original transaction that persisted the <a href=\"https://martinfowler.com/bliki/DDD_Aggregate.html\">aggregate root</a> and all the subsequent events that get handled as part of the dispatching.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> MediatR;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">interface</span> IDomainEvent : INotification\n{\n Guid CorrelationId { <span style=\"color:#66d9ef\">get</span>; }\n}\n</code></pre></div><p>And here’s a sample implementation of an menu item being added to the table’s order at a restaurant.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">MenuItemAddedToOrderDomainEvent</span> : IDomainEvent\n{\n <span style=\"color:#66d9ef\">public</span> MenuItemAddedToOrderDomainEvent(\n Guid correlationId,\n Guid tableId,\n <span style=\"color:#66d9ef\">int</span> tableNumber,\n Guid orderId,\n Guid menuItemId,\n <span style=\"color:#66d9ef\">decimal</span> price)\n {\n <span style=\"color:#66d9ef\">this</span>.CorrelationId = correlationId;\n <span style=\"color:#66d9ef\">this</span>.TableId = tableId;\n <span style=\"color:#66d9ef\">this</span>.TableNumber = tableNumber;\n <span style=\"color:#66d9ef\">this</span>.OrderId = orderId;\n <span style=\"color:#66d9ef\">this</span>.MenuItemId = menuItemId;\n <span style=\"color:#66d9ef\">this</span>.Price = price;\n }\n\n\n <span style=\"color:#66d9ef\">public</span> Guid CorrelationId { <span style=\"color:#66d9ef\">get</span>; }\n <span style=\"color:#66d9ef\">public</span> Guid TableId { <span style=\"color:#66d9ef\">get</span>; }\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">int</span> TableNumber { <span style=\"color:#66d9ef\">get</span>; }\n <span style=\"color:#66d9ef\">public</span> Guid OrderId { <span style=\"color:#66d9ef\">get</span>; }\n <span style=\"color:#66d9ef\">public</span> Guid MenuItemId { <span style=\"color:#66d9ef\">get</span>; }\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">decimal</span> Price { <span style=\"color:#66d9ef\">get</span>; }\n}\n</code></pre></div><h2 id=\"storing-domain-events\">Storing Domain Events</h2>\n<h3 id=\"entity-framework-entity\">Entity Framework Entity</h3>\n<p>Below is entity class used to store the details about a domain event after it related aggregate root has been persisted.</p>\n<p>Things to note, we are storing the domain event as JSON (<code>DomainEventJson</code>) and also storing the assembly name (<code>DomainEventAssemblyName</code>) and class name (<code>DomainEventClassName</code>) to allow us to deserialize the JSON back to the doamin event in the <code>ToDomainEvent</code> method.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> System.Reflection;\n<span style=\"color:#66d9ef\">using</span> System.Text.Json;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">DomainEventReference</span>\n{\n <span style=\"color:#66d9ef\">public</span> DomainEventReference(DomainEvent domainEvent)\n {\n <span style=\"color:#66d9ef\">this</span>.Id = Guid.NewGuid();\n <span style=\"color:#66d9ef\">this</span>.DomainEventAssemblyName = assemblyName;\n <span style=\"color:#66d9ef\">this</span>.DomainEventClassName = fullClassName;\n <span style=\"color:#66d9ef\">this</span>.DomainEventJson = JsonSerializer.Serialize(domainEvent, domainEventType);\n <span style=\"color:#66d9ef\">this</span>.PersistedAt = DateTime.UtcNow;\n <span style=\"color:#66d9ef\">this</span>.Dispatched = <span style=\"color:#66d9ef\">false</span>;\n }\n\n\n <span style=\"color:#75715e\">// EF constructor\n</span><span style=\"color:#75715e\"></span> <span style=\"color:#66d9ef\">private</span> DomainEventReference()\n {\n <span style=\"color:#66d9ef\">this</span>.DomainEventAssemblyName = <span style=\"color:#66d9ef\">string</span>.Empty;\n <span style=\"color:#66d9ef\">this</span>.DomainEventClassName = <span style=\"color:#66d9ef\">string</span>.Empty;\n <span style=\"color:#66d9ef\">this</span>.DomainEventJson = <span style=\"color:#e6db74\">"{}"</span>;\n <span style=\"color:#66d9ef\">this</span>.PersistedAt = DateTime.UtcNow;\n <span style=\"color:#66d9ef\">this</span>.Dispatched = <span style=\"color:#66d9ef\">false</span>;\n }\n\n\n <span style=\"color:#66d9ef\">public</span> Guid Id { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">string</span> DomainEventAssemblyName { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">string</span> DomainEventClassName { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">string</span> DomainEventJson { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">bool</span> Dispatched { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> DateTime PersistedAt { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> DateTime? DispatchedAt { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n\n\n <span style=\"color:#66d9ef\">public</span> DomainEvent? ToDomainEvent()\n {\n <span style=\"color:#66d9ef\">if</span> (<span style=\"color:#66d9ef\">string</span>.IsNullOrWhiteSpace(<span style=\"color:#66d9ef\">this</span>.DomainEventJson))\n {\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">null</span>;\n }\n\n\n <span style=\"color:#66d9ef\">var</span> assembly = Assembly.Load(<span style=\"color:#66d9ef\">this</span>.DomainEventAssemblyName);\n <span style=\"color:#66d9ef\">var</span> targetType = assembly.GetType(<span style=\"color:#66d9ef\">this</span>.DomainEventClassName);\n\n\n <span style=\"color:#66d9ef\">if</span> (targetType == <span style=\"color:#66d9ef\">null</span>)\n {\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">null</span>;\n }\n\n\n <span style=\"color:#66d9ef\">var</span> objDomainEvent = Deserialize(<span style=\"color:#66d9ef\">this</span>.DomainEventJson, targetType);\n <span style=\"color:#66d9ef\">if</span> (objDomainEvent <span style=\"color:#66d9ef\">is</span> not DomainEvent domainEvent)\n {\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">null</span>;\n }\n\n\n domainEvent.UserId = <span style=\"color:#66d9ef\">this</span>.UserId;\n\n\n <span style=\"color:#66d9ef\">return</span> domainEvent;\n\n\n <span style=\"color:#66d9ef\">static</span> <span style=\"color:#66d9ef\">object?</span> Deserialize(<span style=\"color:#66d9ef\">string</span> json, Type type)\n {\n <span style=\"color:#66d9ef\">try</span>\n {\n <span style=\"color:#66d9ef\">return</span> JsonSerializer.Deserialize(json, type);\n }\n <span style=\"color:#66d9ef\">catch</span>\n {\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">null</span>;\n }\n }\n }\n\n\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">void</span> Dispatch()\n {\n <span style=\"color:#66d9ef\">this</span>.Dispatched = <span style=\"color:#66d9ef\">true</span>;\n <span style=\"color:#66d9ef\">this</span>.DispatchedAt = DateTime.UtcNow;\n }\n}\n</code></pre></div><p>And here’s how we’ve configured the underlying database table.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore;\n<span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore.Metadata.Builders;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">DomainEventReferenceConfiguration</span> : IEntityTypeConfiguration<DomainEventReference>\n{\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">void</span> Configure(EntityTypeBuilder<DomainEventReference> builder)\n {\n builder.HasKey(x => x.Id);\n\n\n builder.Property(x => x.DomainEventAssemblyName)\n .HasMaxLength(<span style=\"color:#ae81ff\">255</span>)\n .IsRequired();\n\n\n builder.Property(x => x.DomainEventClassName)\n .HasMaxLength(<span style=\"color:#ae81ff\">255</span>)\n .IsRequired();\n\n\n builder.Property(x => x.DomainEventJson)\n .HasMaxLength(<span style=\"color:#ae81ff\">8000</span>)\n .IsRequired();\n\n\n builder.Property(x => x.Dispatched)\n .IsRequired();\n\n\n builder.Property(x => x.PersistedAt)\n .IsRequired();\n\n\n builder.Property(x => x.DispatchedAt)\n .IsRequired(<span style=\"color:#66d9ef\">false</span>);\n\n\n builder.Property(x => x.UserId)\n .HasMaxLength(<span style=\"color:#ae81ff\">50</span>);\n\n\n builder.HasIndex(\n nameof(DomainEventReference.Dispatched),\n nameof(DomainEventReference.PersistedAt));\n }\n}\n</code></pre></div><h3 id=\"how-the-aggregate-root-stores-the-domain-event\">How the aggregate root stores the domain event</h3>\n<p>The aggregate root needs a collection of domain events on it.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">DomainEventsCollection</span>\n{\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">readonly</span> List<DomainEvent> domainEvents = <span style=\"color:#66d9ef\">new</span>();\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">readonly</span> <span style=\"color:#66d9ef\">object</span> syncLock = <span style=\"color:#66d9ef\">new</span>();\n\n\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">void</span> Raise<T>(T domainEvent)\n <span style=\"color:#66d9ef\">where</span> T : DomainEvent\n {\n <span style=\"color:#66d9ef\">lock</span> (<span style=\"color:#66d9ef\">this</span>.syncLock)\n {\n <span style=\"color:#66d9ef\">this</span>.domainEvents.Add(domainEvent);\n }\n }\n\n\n <span style=\"color:#66d9ef\">public</span> DomainEvent[] GetAndClear()\n {\n <span style=\"color:#66d9ef\">lock</span> (<span style=\"color:#66d9ef\">this</span>.syncLock)\n {\n <span style=\"color:#66d9ef\">var</span> events = <span style=\"color:#66d9ef\">this</span>.domainEvents.ToArray();\n <span style=\"color:#66d9ef\">this</span>.domainEvents.Clear();\n\n\n <span style=\"color:#66d9ef\">return</span> events;\n }\n }\n}\n</code></pre></div><div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">abstract</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">AggregateRoot</span> : Entity\n{\n <span style=\"color:#66d9ef\">protected</span> AggregateRoot()\n : <span style=\"color:#66d9ef\">base</span>(Guid.NewGuid())\n {\n }\n\n\n <span style=\"color:#66d9ef\">protected</span> AggregateRoot(Guid id)\n : <span style=\"color:#66d9ef\">base</span>(id)\n {\n }\n\n\n <span style=\"color:#66d9ef\">public</span> DomainEventsCollection DomainEvents { <span style=\"color:#66d9ef\">get</span>; } = <span style=\"color:#66d9ef\">new</span> DomainEventsCollection();\n}\n</code></pre></div><p>And here’s a sample implementation of an aggregate root “raising” a domain event.</p>\n<p>In this example, a menu item is being added to the order of a table at a restaurant.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">Table</span> : AggregateRoot\n{\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">readonly</span> List<Order> orders = <span style=\"color:#66d9ef\">new</span>();\n\n\n <span style=\"color:#66d9ef\">public</span> Table(Guid id, <span style=\"color:#66d9ef\">int</span> tableNumber)\n : <span style=\"color:#66d9ef\">base</span>(id)\n {\n <span style=\"color:#66d9ef\">this</span>.TableNumber = tableNumber;\n }\n\n\n <span style=\"color:#66d9ef\">public</span> Order? CurrentOrder => <span style=\"color:#66d9ef\">this</span>.orders\n .Where(x => x.OrderStatusId != OrderStatus.Paid)\n .Where(x => !x.IsDeleted)\n .OrderByDescending(x => x.CreatedAtUtc)\n .FirstOrDefault();\n\n\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">int</span> TableNumber { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">bool</span> IsInUse { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">set</span>; }\n <span style=\"color:#66d9ef\">public</span> IReadOnlyCollection<Order> Orders => <span style=\"color:#66d9ef\">this</span>.orders;\n\n\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">void</span> OrderMenuItem(MenuItem menuItem, Guid correlationId)\n {\n <span style=\"color:#66d9ef\">if</span> (!<span style=\"color:#66d9ef\">this</span>.IsInUse || <span style=\"color:#66d9ef\">this</span>.CurrentOrder == <span style=\"color:#66d9ef\">null</span>)\n {\n <span style=\"color:#66d9ef\">throw</span> <span style=\"color:#66d9ef\">new</span> DomainException(ErrorMessages.CannotOrderIfTableNotInUse);\n }\n\n\n <span style=\"color:#66d9ef\">this</span>.CurrentOrder.AddItem(menuItem);\n\n\n <span style=\"color:#66d9ef\">this</span>.DomainEvents.Raise(<span style=\"color:#66d9ef\">new</span> MenuItemAddedToOrderDomainEvent(\n correlationId,\n <span style=\"color:#66d9ef\">this</span>.Id,\n <span style=\"color:#66d9ef\">this</span>.TableNumber,\n <span style=\"color:#66d9ef\">this</span>.CurrentOrder.Id,\n menuItem.Id,\n menuItem.Price));\n }\n\n\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">static</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">ErrorMessages</span>\n {\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">const</span> <span style=\"color:#66d9ef\">string</span> CannotOrderIfTableNotInUse = <span style=\"color:#e6db74\">"Cannot order items if table is not in use"</span>;\n }\n}\n</code></pre></div><h3 id=\"how-the-dbcontext-stores-the-domain-event\">How the DbContext stores the domain event</h3>\n<p>We are going to use a <a href=\"https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.diagnostics.savechangesinterceptor\">SaveChangesInterceptor</a> to find the domain events added to our aggregate roots and add them as <code>DomainEventReference</code> instances before “save changes” executes.</p>\n<p>To do that, we need to be able easily identify the <code>DbSet</code> for our <code>DomainEventReference</code> entities.</p>\n<p>So, we’ve created an <code>IApplicationDbContext</code> interface that we can add to our Entity Framework <code>DbContext</code>.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">interface</span> IApplicationDbContext\n{\n DbSet<DomainEventReference>? DomainEventReferences { <span style=\"color:#66d9ef\">get</span>; }\n}\n</code></pre></div><p>And here’s the interceptor that find the domain events on the aggregate roots and stores them in the <code>DbContext</code>.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore;\n<span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore.ChangeTracking;\n<span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore.Diagnostics;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">DomainEventSaveChangesInterceptor</span><TContext> : SaveChangesInterceptor\n <span style=\"color:#66d9ef\">where</span> TContext : DbContext, IApplicationDbContext\n{\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">override</span> InterceptionResult<<span style=\"color:#66d9ef\">int</span>> SavingChanges(\n DbContextEventData eventData,\n InterceptionResult<<span style=\"color:#66d9ef\">int</span>> result)\n {\n <span style=\"color:#66d9ef\">this</span>.StoreDomainEvents(eventData.Context);\n\n\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">base</span>.SavingChanges(eventData, result);\n }\n\n\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">override</span> ValueTask<InterceptionResult<<span style=\"color:#66d9ef\">int</span>>> SavingChangesAsync(\n DbContextEventData eventData,\n InterceptionResult<<span style=\"color:#66d9ef\">int</span>> result,\n CancellationToken cancellationToken = <span style=\"color:#66d9ef\">default</span>)\n {\n <span style=\"color:#66d9ef\">this</span>.StoreDomainEvents(eventData.Context);\n\n\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">base</span>.SavingChangesAsync(eventData, result, cancellationToken);\n }\n\n\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">void</span> StoreDomainEvents(DbContext? context)\n {\n <span style=\"color:#66d9ef\">if</span> (context == <span style=\"color:#66d9ef\">null</span> || context <span style=\"color:#66d9ef\">is</span> not TContext)\n {\n <span style=\"color:#66d9ef\">return</span>;\n }\n\n\n <span style=\"color:#66d9ef\">foreach</span> (<span style=\"color:#66d9ef\">var</span> entry <span style=\"color:#66d9ef\">in</span> context.ChangeTracker.Entries().ToList())\n {\n <span style=\"color:#66d9ef\">this</span>.StoreDomainEventsForEntry((TContext)context, entry);\n }\n }\n\n\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">void</span> StoreDomainEventsForEntry(TContext context, EntityEntry entry)\n {\n <span style=\"color:#66d9ef\">if</span> (entry.Entity <span style=\"color:#66d9ef\">is</span> not AggregateRoot aggregateRootEntity)\n {\n <span style=\"color:#66d9ef\">return</span>;\n }\n\n\n <span style=\"color:#66d9ef\">var</span> events = aggregateRootEntity.DomainEvents.GetAndClear();\n\n\n <span style=\"color:#66d9ef\">foreach</span> (<span style=\"color:#66d9ef\">var</span> domainEvent <span style=\"color:#66d9ef\">in</span> events)\n {\n <span style=\"color:#66d9ef\">if</span> (domainEvent == <span style=\"color:#66d9ef\">null</span>)\n {\n <span style=\"color:#66d9ef\">continue</span>;\n }\n\n\n <span style=\"color:#66d9ef\">var</span> reference = <span style=\"color:#66d9ef\">new</span> DomainEventReference(domainEvent);\n\n\n context.DomainEventReferences?.Add(reference);\n }\n }\n}\n</code></pre></div><p>Finally, here’s an abstract <code>DbContext</code> implementation that exposes the <code>DomainEventReference</code> <code>DbSet</code> and adds the <code>DomainEventSaveChangesInterceptor</code>.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">abstract</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">ApplicationDbContext</span><TContext> : DbContext, IApplicationDbContext\n <span style=\"color:#66d9ef\">where</span> TContext : DbContext, IApplicationDbContext\n{\n <span style=\"color:#66d9ef\">protected</span> ApplicationDbContext(DbContextOptions<TContext> options)\n : <span style=\"color:#66d9ef\">base</span>(options)\n {\n }\n\n\n <span style=\"color:#66d9ef\">public</span> DbSet<DomainEventReference>? DomainEventReferences { <span style=\"color:#66d9ef\">get</span>; <span style=\"color:#66d9ef\">internal</span> <span style=\"color:#66d9ef\">set</span>; }\n\n\n <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">override</span> <span style=\"color:#66d9ef\">void</span> OnModelCreating(ModelBuilder modelBuilder)\n {\n <span style=\"color:#66d9ef\">base</span>.OnModelCreating(modelBuilder);\n\n\n modelBuilder.ApplyConfiguration(<span style=\"color:#66d9ef\">new</span> DomainEventReferenceConfiguration());\n\n\n <span style=\"color:#66d9ef\">this</span>.ConfigureDatabaseModel(modelBuilder);\n }\n\n\n <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">override</span> <span style=\"color:#66d9ef\">void</span> OnConfiguring(DbContextOptionsBuilder optionsBuilder)\n {\n <span style=\"color:#66d9ef\">base</span>.OnConfiguring(optionsBuilder);\n\n\n optionsBuilder.AddInterceptors(<span style=\"color:#66d9ef\">new</span> DomainEventSaveChangesInterceptor<TContext>());\n }\n\n\n <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">abstract</span> <span style=\"color:#66d9ef\">void</span> ConfigureDatabaseModel(ModelBuilder modelBuilder);\n}\n</code></pre></div><h2 id=\"dispatching-domain-events\">Dispatching Domain Events</h2>\n<p>The code below reads from the database table for the <code>DomainEventReference</code> entity and dispatches them in batches of 50.</p>\n<p>For event row, it deserializes the JSON to the original <code>IDomainEvent</code> and because <code>IDomainEvent</code> implements <code>INotification</code> from <code>MediatR</code>, you can use the <code>Publish</code> to achieve the publisher-subscriber pattern; there can be multiple notification handlers for each event. Conversely, if there are not notification handlers, <code>MediatR</code> will handle this for us and return successfully.</p>\n<p>The domain event does not get marked as “dispatched” in the database unless all dispatching succeeds.</p>\n<p>There is a downside here because you could have three notification handlers for an event, two could succeed and one could fail, causing the event does not get marked as dispatched.</p>\n<p>Any process that tries to re-dispatch will attempt all three handlers again, so you could get duplication of certain handlers.</p>\n<p>However, I think this is still better than the other scenario I outlined earlier.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> MediatR;\n<span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore;\n<span style=\"color:#66d9ef\">using</span> Serilog;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">DomainEventDispatcher</span><TContext>\n <span style=\"color:#66d9ef\">where</span> TContext : DbContext, IApplicationDbContext\n{\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">const</span> <span style=\"color:#66d9ef\">int</span> BatchSize = <span style=\"color:#ae81ff\">50</span>;\n\n\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">readonly</span> IDbContextFactory<TContext> dbContextFactory;\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">readonly</span> IMediator mediator;\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">readonly</span> ILogger logger;\n\n\n <span style=\"color:#66d9ef\">public</span> DomainEventDispatcher(\n IDbContextFactory<TContext> dbContextFactory,\n IMediator mediator,\n ILogger logger)\n {\n <span style=\"color:#66d9ef\">this</span>.dbContextFactory = dbContextFactory;\n <span style=\"color:#66d9ef\">this</span>.mediator = mediator;\n <span style=\"color:#66d9ef\">this</span>.logger = logger.ForContext<DomainEventDispatcher<TContext>>();\n }\n\n\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">async</span> Task DispatchEvents(CancellationToken cancellationToken)\n {\n <span style=\"color:#66d9ef\">var</span> eventsToDispatch = <span style=\"color:#66d9ef\">await</span> <span style=\"color:#66d9ef\">this</span>.ThereAreEventsToDispatch(cancellationToken);\n\n\n <span style=\"color:#66d9ef\">while</span> (eventsToDispatch && !cancellationToken.IsCancellationRequested)\n {\n <span style=\"color:#66d9ef\">await</span> <span style=\"color:#66d9ef\">this</span>.DispatchBatch(cancellationToken);\n\n\n eventsToDispatch = <span style=\"color:#66d9ef\">await</span> <span style=\"color:#66d9ef\">this</span>.ThereAreEventsToDispatch(cancellationToken);\n }\n }\n\n\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">async</span> Task<<span style=\"color:#66d9ef\">bool</span>> ThereAreEventsToDispatch(CancellationToken token)\n {\n <span style=\"color:#66d9ef\">using</span> (<span style=\"color:#66d9ef\">var</span> scope = <span style=\"color:#66d9ef\">this</span>.dbContextFactory.CreateDbContext())\n {\n <span style=\"color:#66d9ef\">var</span> dbSet = scope.Set<DomainEventReference>();\n\n\n <span style=\"color:#66d9ef\">if</span> (dbSet == <span style=\"color:#66d9ef\">null</span>)\n {\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">false</span>;\n }\n\n\n <span style=\"color:#66d9ef\">return</span> <span style=\"color:#66d9ef\">await</span> dbSet\n .Where(x => !x.Dispatched)\n .OrderBy(x => x.PersistedAt)\n .AnyAsync(token);\n }\n }\n\n\n <span style=\"color:#66d9ef\">private</span> <span style=\"color:#66d9ef\">async</span> Task DispatchBatch(CancellationToken token)\n {\n <span style=\"color:#66d9ef\">using</span> (<span style=\"color:#66d9ef\">var</span> scope = <span style=\"color:#66d9ef\">this</span>.dbContextFactory.CreateDbContext())\n {\n <span style=\"color:#66d9ef\">var</span> dbSet = scope.Set<DomainEventReference>();\n <span style=\"color:#66d9ef\">if</span> (dbSet == <span style=\"color:#66d9ef\">null</span>)\n {\n <span style=\"color:#66d9ef\">return</span>;\n }\n\n\n <span style=\"color:#66d9ef\">var</span> events = <span style=\"color:#66d9ef\">await</span> dbSet\n .Where(x => !x.Dispatched)\n .OrderBy(x => x.PersistedAt)\n .Take(BatchSize)\n .ToArrayAsync(token);\n\n\n <span style=\"color:#66d9ef\">var</span> domainEvents = <span style=\"color:#66d9ef\">new</span> List<DomainEvent>();\n\n\n <span style=\"color:#66d9ef\">foreach</span> (<span style=\"color:#66d9ef\">var</span> domainEventReference <span style=\"color:#66d9ef\">in</span> events)\n {\n domainEventReference.Dispatch();\n\n\n <span style=\"color:#66d9ef\">var</span> domainEvent = domainEventReference.ToDomainEvent();\n\n\n <span style=\"color:#66d9ef\">if</span> (domainEvent == <span style=\"color:#66d9ef\">null</span>)\n {\n <span style=\"color:#66d9ef\">this</span>.logger.Warning(\n <span style=\"color:#e6db74\">"Could not deserialize DomainEventReference {Id}"</span>,\n domainEventReference.Id);\n }\n <span style=\"color:#66d9ef\">else</span>\n {\n domainEvents.Add(domainEvent);\n }\n }\n\n\n <span style=\"color:#66d9ef\">if</span> (domainEvents.Any())\n {\n <span style=\"color:#66d9ef\">foreach</span> (<span style=\"color:#66d9ef\">var</span> domainEvent <span style=\"color:#66d9ef\">in</span> domainEvents)\n {\n <span style=\"color:#66d9ef\">await</span> <span style=\"color:#66d9ef\">this</span>.mediator.Publish(domainEvent, token);\n }\n }\n\n\n <span style=\"color:#66d9ef\">await</span> scope.SaveChangesAsync(token);\n }\n }\n}\n</code></pre></div><p>So the code above does the dispatching, but what triggers the dispatching?</p>\n<p>As far as I know, there is nothing similar to the <code>SaveChangesInterceptor</code> that executes after a successful save changes.</p>\n<p>So, we’ve decided to use a background service.</p>\n<p>In the code below, we are expecting our <code>DomainEventDispatcher</code> to dispatch events every 2.5 seconds.</p>\n<p>So, any events that failed to dispatch will be retried after 2.5 seconds.</p>\n<p><code>DomainEventReference</code> does not currently have a “retry count” or a “failed” property and that is definitely an improvement that could be added; fail if retried 10 times.</p>\n<p>Under this solution, we will keep retrying and failed events will be attempted before new events, potentially causing a performance issue if failed events build up.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore;\n<span style=\"color:#66d9ef\">using</span> Microsoft.Extensions.Hosting;\n\n\n<span style=\"color:#66d9ef\">namespace</span> Lewee.Infrastructure.Data;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">DomainEventDispatcherService</span><TContext> : BackgroundService\n <span style=\"color:#66d9ef\">where</span> TContext : DbContext, IApplicationDbContext\n{\n <span style=\"color:#66d9ef\">public</span> DomainEventDispatcherService(DomainEventDispatcher<TContext> domainEventDispatcher)\n {\n <span style=\"color:#66d9ef\">this</span>.DomainEventDispatcher = domainEventDispatcher;\n }\n\n\n <span style=\"color:#66d9ef\">public</span> DomainEventDispatcher<TContext> DomainEventDispatcher { <span style=\"color:#66d9ef\">get</span>; }\n\n\n <span style=\"color:#66d9ef\">protected</span> <span style=\"color:#66d9ef\">override</span> <span style=\"color:#66d9ef\">async</span> Task ExecuteAsync(CancellationToken stoppingToken)\n {\n <span style=\"color:#66d9ef\">while</span> (!stoppingToken.IsCancellationRequested)\n {\n <span style=\"color:#66d9ef\">await</span> <span style=\"color:#66d9ef\">this</span>.DomainEventDispatcher.DispatchEvents(stoppingToken);\n\n\n <span style=\"color:#66d9ef\">await</span> Task.Delay(<span style=\"color:#ae81ff\">2500</span>, stoppingToken);\n }\n }\n}\n</code></pre></div><p>There are definitely better ways to achieve this.</p>\n<p>You could override save changes on your <code>DbContext</code> to trigger your domain event dispatcher and later you use some sort of retry policy if any events fails to dispatch.</p>\n<p>That would be more efficient, but more complicated and harder to implement.</p>\n<h2 id=\"dependency-injection-configuration\">Dependency Injection Configuration</h2>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-cs\" data-lang=\"cs\"><span style=\"color:#66d9ef\">using</span> Microsoft.EntityFrameworkCore;\n<span style=\"color:#66d9ef\">using</span> Microsoft.Extensions.DependencyInjection;\n\n\n<span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">static</span> <span style=\"color:#66d9ef\">class</span> <span style=\"color:#a6e22e\">ApplicationDbContextServiceCollectionExtensions</span>\n{\n <span style=\"color:#66d9ef\">public</span> <span style=\"color:#66d9ef\">static</span> IServiceCollection ConfigureDatabase<T>(\n <span style=\"color:#66d9ef\">this</span> IServiceCollection services,\n <span style=\"color:#66d9ef\">string</span> connectionString)\n <span style=\"color:#66d9ef\">where</span> T : ApplicationDbContext<T>\n {\n services.AddDbContextFactory<T>(options => options.UseSqlServer(connectionString));\n services.AddScoped<T>();\n\n\n services.AddSingleton<DomainEventDispatcher<T>>();\n services.AddHostedService<DomainEventDispatcherService<T>>();\n\n\n <span style=\"color:#66d9ef\">return</span> services;\n }\n}\n</code></pre></div><h2 id=\"this-is-a-lot-of-boilerplate\">This is a lot of boilerplate</h2>\n<p>This seems like a lot of code to achieve what I’d expect to be relatively straight-forward.</p>\n<p>Given this is fairly common pattern and that DDD is used by a lot in software development, you’d expect that there are frameworks that do this for you.</p>\n<p>That’s what I’ve tried to achieve with <a href=\"https://github.com/TheMagnificent11/lewee\">Lewee</a>.</p>\n<p>However, there’s definitely a better way.</p>\n<p>In a future blog post, I’d like to explore how to dispatch to a message broker, probably <code>RabbitMQ</code> via <a href=\"https://masstransit.io\">MassTransit</a>, instead of using <code>Mediatr</code> notifications. I feel like this is more flexible as any service within your distributed application can handle your domain events. not just the service that dispatches it.</p>\n",
"content_text": "## What is domain event dispatching?\n\n\nDomain event dispatching is a concept that related to [domain-driven design](https://martinfowler.com/bliki/DomainDrivenDesign.html), or DDD as it's also known.\n\n\nHaving said that, event dispatching is central to any [event-driven architecture](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/event-driven), which follows the [publisher-subscriber pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber).\n\n\nNow, I've not actually read Eric Evans' seminal book on domain-driven design, [Domain-driven Design: Tackling Complexity in the Heart of Software](https://www.amazon.com.au/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215), so I'm unsure whether Evans suggests whether domain events should be published as part of the transaction that creates them, or have the event persisted with the change in application state and then later published using an [outbox pattern](https://codeopinion.com/outbox-pattern-reliably-save-state-publish-events).\n\n\nI prefer the outbox pattern for domain event dispatching because I don't think you want a scenario where data is persisted and an event is raised that has multiple subscribers, but one of the subscribers fails to execute, causing the whole transation to be rolled back.\n\n\nYou then get an inconsistent scenario where certain event handlers have been handled, but the data that relates to those actions does not exist. Why send a confirmation email for an account that wasn't successfully regsitered.\n\n\nOr, conversely, the transaction is not rolled back and one subscriber has failed to execute (including retries with back-off).\n\n\nThe outbox pattern, keeps a record of whether the event has been dispatched or \"sent\", behaving a bit like a service bus.\n\n\nIn this blog post, I'm going to explore how an application using [Entity Framework](https://learn.microsoft.com/en-us/aspnet/entity-framework) as an ORM can use an outbox pattern to publish domain events that are persisted with the application data.\n\n\nThe packages used will be Entity Framewok and I will leverage `INotification` in [MediatR](https://github.com/jbogard/MediatR) to assist with the publisher-subscriber implementation.\n\n\nAll of these code samples are taken from my [Lewee](https://github.com/TheMagnificent11/lewee) project.\n\n\n## Domain Event\n\n\nHere's rough representation of a domain event.\n\n\nI've included a `CorrelationId` property that I believe should be set to allow you to correlate the logs from the original transaction that persisted the [aggregate root](https://martinfowler.com/bliki/DDD_Aggregate.html) and all the subsequent events that get handled as part of the dispatching.\n\n\n```cs\nusing MediatR;\n\n\npublic interface IDomainEvent : INotification\n{\n Guid CorrelationId { get; }\n}\n```\nAnd here's a sample implementation of an menu item being added to the table's order at a restaurant.\n\n\n```cs\npublic class MenuItemAddedToOrderDomainEvent : IDomainEvent\n{\n public MenuItemAddedToOrderDomainEvent(\n Guid correlationId,\n Guid tableId,\n int tableNumber,\n Guid orderId,\n Guid menuItemId,\n decimal price)\n {\n this.CorrelationId = correlationId;\n this.TableId = tableId;\n this.TableNumber = tableNumber;\n this.OrderId = orderId;\n this.MenuItemId = menuItemId;\n this.Price = price;\n }\n\n\n public Guid CorrelationId { get; }\n public Guid TableId { get; }\n public int TableNumber { get; }\n public Guid OrderId { get; }\n public Guid MenuItemId { get; }\n public decimal Price { get; }\n}\n```\n## Storing Domain Events\n\n\n### Entity Framework Entity\n\n\nBelow is entity class used to store the details about a domain event after it related aggregate root has been persisted.\n\n\nThings to note, we are storing the domain event as JSON (`DomainEventJson`) and also storing the assembly name (`DomainEventAssemblyName`) and class name (`DomainEventClassName`) to allow us to deserialize the JSON back to the doamin event in the `ToDomainEvent` method.\n\n\n```cs\nusing System.Reflection;\nusing System.Text.Json;\n\n\npublic class DomainEventReference\n{\n public DomainEventReference(DomainEvent domainEvent)\n {\n this.Id = Guid.NewGuid();\n this.DomainEventAssemblyName = assemblyName;\n this.DomainEventClassName = fullClassName;\n this.DomainEventJson = JsonSerializer.Serialize(domainEvent, domainEventType);\n this.PersistedAt = DateTime.UtcNow;\n this.Dispatched = false;\n }\n\n\n // EF constructor\n private DomainEventReference()\n {\n this.DomainEventAssemblyName = string.Empty;\n this.DomainEventClassName = string.Empty;\n this.DomainEventJson = \"{}\";\n this.PersistedAt = DateTime.UtcNow;\n this.Dispatched = false;\n }\n\n\n public Guid Id { get; protected set; }\n public string DomainEventAssemblyName { get; protected set; }\n public string DomainEventClassName { get; protected set; }\n public string DomainEventJson { get; protected set; }\n public bool Dispatched { get; protected set; }\n public DateTime PersistedAt { get; protected set; }\n public DateTime? DispatchedAt { get; protected set; }\n\n\n public DomainEvent? ToDomainEvent()\n {\n if (string.IsNullOrWhiteSpace(this.DomainEventJson))\n {\n return null;\n }\n\n\n var assembly = Assembly.Load(this.DomainEventAssemblyName);\n var targetType = assembly.GetType(this.DomainEventClassName);\n\n\n if (targetType == null)\n {\n return null;\n }\n\n\n var objDomainEvent = Deserialize(this.DomainEventJson, targetType);\n if (objDomainEvent is not DomainEvent domainEvent)\n {\n return null;\n }\n\n\n domainEvent.UserId = this.UserId;\n\n\n return domainEvent;\n\n\n static object? Deserialize(string json, Type type)\n {\n try\n {\n return JsonSerializer.Deserialize(json, type);\n }\n catch\n {\n return null;\n }\n }\n }\n\n\n public void Dispatch()\n {\n this.Dispatched = true;\n this.DispatchedAt = DateTime.UtcNow;\n }\n}\n```\nAnd here's how we've configured the underlying database table.\n\n\n```cs\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\n\npublic class DomainEventReferenceConfiguration : IEntityTypeConfiguration<DomainEventReference>\n{\n public void Configure(EntityTypeBuilder<DomainEventReference> builder)\n {\n builder.HasKey(x => x.Id);\n\n\n builder.Property(x => x.DomainEventAssemblyName)\n .HasMaxLength(255)\n .IsRequired();\n\n\n builder.Property(x => x.DomainEventClassName)\n .HasMaxLength(255)\n .IsRequired();\n\n\n builder.Property(x => x.DomainEventJson)\n .HasMaxLength(8000)\n .IsRequired();\n\n\n builder.Property(x => x.Dispatched)\n .IsRequired();\n\n\n builder.Property(x => x.PersistedAt)\n .IsRequired();\n\n\n builder.Property(x => x.DispatchedAt)\n .IsRequired(false);\n\n\n builder.Property(x => x.UserId)\n .HasMaxLength(50);\n\n\n builder.HasIndex(\n nameof(DomainEventReference.Dispatched),\n nameof(DomainEventReference.PersistedAt));\n }\n}\n```\n### How the aggregate root stores the domain event\n\n\nThe aggregate root needs a collection of domain events on it.\n\n\n```cs\npublic class DomainEventsCollection\n{\n private readonly List<DomainEvent> domainEvents = new();\n private readonly object syncLock = new();\n\n\n public void Raise<T>(T domainEvent)\n where T : DomainEvent\n {\n lock (this.syncLock)\n {\n this.domainEvents.Add(domainEvent);\n }\n }\n\n\n public DomainEvent[] GetAndClear()\n {\n lock (this.syncLock)\n {\n var events = this.domainEvents.ToArray();\n this.domainEvents.Clear();\n\n\n return events;\n }\n }\n}\n```\n```cs\npublic abstract class AggregateRoot : Entity\n{\n protected AggregateRoot()\n : base(Guid.NewGuid())\n {\n }\n\n\n protected AggregateRoot(Guid id)\n : base(id)\n {\n }\n\n\n public DomainEventsCollection DomainEvents { get; } = new DomainEventsCollection();\n}\n```\nAnd here's a sample implementation of an aggregate root \"raising\" a domain event.\n\n\nIn this example, a menu item is being added to the order of a table at a restaurant.\n\n\n```cs\npublic class Table : AggregateRoot\n{\n private readonly List<Order> orders = new();\n\n\n public Table(Guid id, int tableNumber)\n : base(id)\n {\n this.TableNumber = tableNumber;\n }\n\n\n public Order? CurrentOrder => this.orders\n .Where(x => x.OrderStatusId != OrderStatus.Paid)\n .Where(x => !x.IsDeleted)\n .OrderByDescending(x => x.CreatedAtUtc)\n .FirstOrDefault();\n\n\n public int TableNumber { get; protected set; }\n public bool IsInUse { get; protected set; }\n public IReadOnlyCollection<Order> Orders => this.orders;\n\n\n public void OrderMenuItem(MenuItem menuItem, Guid correlationId)\n {\n if (!this.IsInUse || this.CurrentOrder == null)\n {\n throw new DomainException(ErrorMessages.CannotOrderIfTableNotInUse);\n }\n\n\n this.CurrentOrder.AddItem(menuItem);\n\n\n this.DomainEvents.Raise(new MenuItemAddedToOrderDomainEvent(\n correlationId,\n this.Id,\n this.TableNumber,\n this.CurrentOrder.Id,\n menuItem.Id,\n menuItem.Price));\n }\n\n\n public static class ErrorMessages\n {\n public const string CannotOrderIfTableNotInUse = \"Cannot order items if table is not in use\";\n }\n}\n```\n### How the DbContext stores the domain event\n\n\nWe are going to use a [SaveChangesInterceptor](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.diagnostics.savechangesinterceptor) to find the domain events added to our aggregate roots and add them as `DomainEventReference` instances before \"save changes\" executes.\n\n\nTo do that, we need to be able easily identify the `DbSet` for our `DomainEventReference` entities.\n\n\nSo, we've created an `IApplicationDbContext` interface that we can add to our Entity Framework `DbContext`.\n\n\n```cs\npublic interface IApplicationDbContext\n{\n DbSet<DomainEventReference>? DomainEventReferences { get; }\n}\n```\nAnd here's the interceptor that find the domain events on the aggregate roots and stores them in the `DbContext`.\n\n\n```cs\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.ChangeTracking;\nusing Microsoft.EntityFrameworkCore.Diagnostics;\n\n\npublic class DomainEventSaveChangesInterceptor<TContext> : SaveChangesInterceptor\n where TContext : DbContext, IApplicationDbContext\n{\n public override InterceptionResult<int> SavingChanges(\n DbContextEventData eventData,\n InterceptionResult<int> result)\n {\n this.StoreDomainEvents(eventData.Context);\n\n\n return base.SavingChanges(eventData, result);\n }\n\n\n public override ValueTask<InterceptionResult<int>> SavingChangesAsync(\n DbContextEventData eventData,\n InterceptionResult<int> result,\n CancellationToken cancellationToken = default)\n {\n this.StoreDomainEvents(eventData.Context);\n\n\n return base.SavingChangesAsync(eventData, result, cancellationToken);\n }\n\n\n private void StoreDomainEvents(DbContext? context)\n {\n if (context == null || context is not TContext)\n {\n return;\n }\n\n\n foreach (var entry in context.ChangeTracker.Entries().ToList())\n {\n this.StoreDomainEventsForEntry((TContext)context, entry);\n }\n }\n\n\n private void StoreDomainEventsForEntry(TContext context, EntityEntry entry)\n {\n if (entry.Entity is not AggregateRoot aggregateRootEntity)\n {\n return;\n }\n\n\n var events = aggregateRootEntity.DomainEvents.GetAndClear();\n\n\n foreach (var domainEvent in events)\n {\n if (domainEvent == null)\n {\n continue;\n }\n\n\n var reference = new DomainEventReference(domainEvent);\n\n\n context.DomainEventReferences?.Add(reference);\n }\n }\n}\n```\nFinally, here's an abstract `DbContext` implementation that exposes the `DomainEventReference` `DbSet` and adds the `DomainEventSaveChangesInterceptor`.\n\n\n```cs\nusing Microsoft.EntityFrameworkCore;\n\n\npublic abstract class ApplicationDbContext<TContext> : DbContext, IApplicationDbContext\n where TContext : DbContext, IApplicationDbContext\n{\n protected ApplicationDbContext(DbContextOptions<TContext> options)\n : base(options)\n {\n }\n\n\n public DbSet<DomainEventReference>? DomainEventReferences { get; internal set; }\n\n\n protected override void OnModelCreating(ModelBuilder modelBuilder)\n {\n base.OnModelCreating(modelBuilder);\n\n\n modelBuilder.ApplyConfiguration(new DomainEventReferenceConfiguration());\n\n\n this.ConfigureDatabaseModel(modelBuilder);\n }\n\n\n protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)\n {\n base.OnConfiguring(optionsBuilder);\n\n\n optionsBuilder.AddInterceptors(new DomainEventSaveChangesInterceptor<TContext>());\n }\n\n\n protected abstract void ConfigureDatabaseModel(ModelBuilder modelBuilder);\n}\n```\n## Dispatching Domain Events\n\n\nThe code below reads from the database table for the `DomainEventReference` entity and dispatches them in batches of 50.\n\n\nFor event row, it deserializes the JSON to the original `IDomainEvent` and because `IDomainEvent` implements `INotification` from `MediatR`, you can use the `Publish` to achieve the publisher-subscriber pattern; there can be multiple notification handlers for each event. Conversely, if there are not notification handlers, `MediatR` will handle this for us and return successfully.\n\n\nThe domain event does not get marked as \"dispatched\" in the database unless all dispatching succeeds.\n\n\nThere is a downside here because you could have three notification handlers for an event, two could succeed and one could fail, causing the event does not get marked as dispatched.\n\n\nAny process that tries to re-dispatch will attempt all three handlers again, so you could get duplication of certain handlers.\n\n\nHowever, I think this is still better than the other scenario I outlined earlier.\n\n\n```cs\nusing MediatR;\nusing Microsoft.EntityFrameworkCore;\nusing Serilog;\n\n\npublic class DomainEventDispatcher<TContext>\n where TContext : DbContext, IApplicationDbContext\n{\n private const int BatchSize = 50;\n\n\n private readonly IDbContextFactory<TContext> dbContextFactory;\n private readonly IMediator mediator;\n private readonly ILogger logger;\n\n\n public DomainEventDispatcher(\n IDbContextFactory<TContext> dbContextFactory,\n IMediator mediator,\n ILogger logger)\n {\n this.dbContextFactory = dbContextFactory;\n this.mediator = mediator;\n this.logger = logger.ForContext<DomainEventDispatcher<TContext>>();\n }\n\n\n public async Task DispatchEvents(CancellationToken cancellationToken)\n {\n var eventsToDispatch = await this.ThereAreEventsToDispatch(cancellationToken);\n\n\n while (eventsToDispatch && !cancellationToken.IsCancellationRequested)\n {\n await this.DispatchBatch(cancellationToken);\n\n\n eventsToDispatch = await this.ThereAreEventsToDispatch(cancellationToken);\n }\n }\n\n\n private async Task<bool> ThereAreEventsToDispatch(CancellationToken token)\n {\n using (var scope = this.dbContextFactory.CreateDbContext())\n {\n var dbSet = scope.Set<DomainEventReference>();\n\n\n if (dbSet == null)\n {\n return false;\n }\n\n\n return await dbSet\n .Where(x => !x.Dispatched)\n .OrderBy(x => x.PersistedAt)\n .AnyAsync(token);\n }\n }\n\n\n private async Task DispatchBatch(CancellationToken token)\n {\n using (var scope = this.dbContextFactory.CreateDbContext())\n {\n var dbSet = scope.Set<DomainEventReference>();\n if (dbSet == null)\n {\n return;\n }\n\n\n var events = await dbSet\n .Where(x => !x.Dispatched)\n .OrderBy(x => x.PersistedAt)\n .Take(BatchSize)\n .ToArrayAsync(token);\n\n\n var domainEvents = new List<DomainEvent>();\n\n\n foreach (var domainEventReference in events)\n {\n domainEventReference.Dispatch();\n\n\n var domainEvent = domainEventReference.ToDomainEvent();\n\n\n if (domainEvent == null)\n {\n this.logger.Warning(\n \"Could not deserialize DomainEventReference {Id}\",\n domainEventReference.Id);\n }\n else\n {\n domainEvents.Add(domainEvent);\n }\n }\n\n\n if (domainEvents.Any())\n {\n foreach (var domainEvent in domainEvents)\n {\n await this.mediator.Publish(domainEvent, token);\n }\n }\n\n\n await scope.SaveChangesAsync(token);\n }\n }\n}\n```\nSo the code above does the dispatching, but what triggers the dispatching?\n\n\nAs far as I know, there is nothing similar to the `SaveChangesInterceptor` that executes after a successful save changes.\n\n\nSo, we've decided to use a background service.\n\n\nIn the code below, we are expecting our `DomainEventDispatcher` to dispatch events every 2.5 seconds.\n\n\nSo, any events that failed to dispatch will be retried after 2.5 seconds.\n\n\n`DomainEventReference` does not currently have a \"retry count\" or a \"failed\" property and that is definitely an improvement that could be added; fail if retried 10 times.\n\n\nUnder this solution, we will keep retrying and failed events will be attempted before new events, potentially causing a performance issue if failed events build up.\n\n\n```cs\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Hosting;\n\n\nnamespace Lewee.Infrastructure.Data;\n\n\npublic class DomainEventDispatcherService<TContext> : BackgroundService\n where TContext : DbContext, IApplicationDbContext\n{\n public DomainEventDispatcherService(DomainEventDispatcher<TContext> domainEventDispatcher)\n {\n this.DomainEventDispatcher = domainEventDispatcher;\n }\n\n\n public DomainEventDispatcher<TContext> DomainEventDispatcher { get; }\n\n\n protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n {\n while (!stoppingToken.IsCancellationRequested)\n {\n await this.DomainEventDispatcher.DispatchEvents(stoppingToken);\n\n\n await Task.Delay(2500, stoppingToken);\n }\n }\n}\n```\nThere are definitely better ways to achieve this.\n\n\nYou could override save changes on your `DbContext` to trigger your domain event dispatcher and later you use some sort of retry policy if any events fails to dispatch.\n\n\nThat would be more efficient, but more complicated and harder to implement.\n\n\n## Dependency Injection Configuration\n\n\n```cs\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n\npublic static class ApplicationDbContextServiceCollectionExtensions\n{\n public static IServiceCollection ConfigureDatabase<T>(\n this IServiceCollection services,\n string connectionString)\n where T : ApplicationDbContext<T>\n {\n services.AddDbContextFactory<T>(options => options.UseSqlServer(connectionString));\n services.AddScoped<T>();\n\n\n services.AddSingleton<DomainEventDispatcher<T>>();\n services.AddHostedService<DomainEventDispatcherService<T>>();\n\n\n return services;\n }\n}\n```\n## This is a lot of boilerplate\n\n\nThis seems like a lot of code to achieve what I'd expect to be relatively straight-forward.\n\n\nGiven this is fairly common pattern and that DDD is used by a lot in software development, you'd expect that there are frameworks that do this for you.\n\n\nThat's what I've tried to achieve with [Lewee](https://github.com/TheMagnificent11/lewee).\n\n\nHowever, there's definitely a better way.\n\n\nIn a future blog post, I'd like to explore how to dispatch to a message broker, probably `RabbitMQ` via [MassTransit](https://masstransit.io), instead of using `Mediatr` notifications. I feel like this is more flexible as any service within your distributed application can handle your domain events. not just the service that dispatches it.\n\n",
"date_published": "2025-02-15T02:33:16+10:00",
"url": "https://sajilicious.micro.blog/2025/02/15/domain-event-dispatching-using-the.html",
"tags": ["dotnet","Domain Driven Design","Entity Framework"]
}
]
}