<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Mon, 26 Oct 2026 22:04:04 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=gdlcf88" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/notice-and-solve-abp-distributed-events-disordering-yi9vq3p4</guid>
      <link>https://abp.io/community/posts/notice-and-solve-abp-distributed-events-disordering-yi9vq3p4</link>
      <a10:author>
        <a10:name>gdlcf88</a10:name>
        <a10:uri>https://abp.io/community/members/gdlcf88</a10:uri>
      </a10:author>
      <category>distributed-events</category>
      <title>Notice and Solve ABP Distributed Events Disordering</title>
      <description>ABP developers should notice the distributed events disordering since events will no longer be linearizability due to the limitations of network latency and infrastructure efficiency in a microservice or multi-database scenario.</description>
      <pubDate>Wed, 16 Nov 2022 19:28:14 Z</pubDate>
      <a10:updated>2026-10-26T21:26:01Z</a10:updated>
      <content:encoded><![CDATA[<h1>Notice and Solve ABP Distributed Events Disordering</h1>
<p>ABP Framework 5.0 implemented the strict ordering of outboxes and inboxes for monolithic apps.
However, in a microservice or multi-database scenario,
distributed events will no longer be linearizability <a href="#references">[1]</a> due to the limitations of network latency and infrastructure efficiency.
So there will inevitably be physical time disordering events.</p>
<p>Use an illustration from Daniel Wu's article &quot;Messaging Reliability and Ordering&quot; <a href="#references">[2]</a> to show you the problem:</p>
<p><img src="https://user-images.githubusercontent.com/30018771/194262471-d5c7aa5f-adc6-4593-b0b2-9738ac56edab.png" alt="image" /> </p>
<p>If the event being handled has a causality with any other event, there may be problems caused by the two being disordering.</p>
<p>This article will discuss the situations and solutions we may encounter on the event subscriber apps based on the above truth.</p>
<h2>Cases</h2>
<p>We make the following conventions.</p>
<ol>
<li>We focus on a user score service that subscribes to some distributed events.</li>
<li><code>m1</code> and <code>m2</code> are two events that occurred <strong>successively</strong>.</li>
<li><code>t1</code> and <code>t2</code> are when the service receives and handles the events m1 and m2, respectively.</li>
<li><code>t1 &lt; t2</code> means that t1 is earlier than t2, which is called ordered; <code>t1 &gt; t2</code> means that t1 is later than t2, which is called disordered.</li>
<li>C (configuration) represents the state of the subscriber service. <code>C0</code> is the initial state, <code>CF</code> is the expected final state, and <code>CW</code> is the wrong final state.</li>
</ol>
<h3>Case 1</h3>
<ul>
<li>Event m1: event of user A created</li>
<li>Event m2: event of user B created</li>
<li>Handler jobs: Create LocalUser entities locally according to m1 and m2, respectively</li>
<li>Analysis: m1 and m2 are non-causal and ordering insensitive
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=94-kB06wg4IZdyFTBNoSe,hyQGl4eKRmgHXAU027iy0w"><img src="https://user-images.githubusercontent.com/30018771/194247285-e62dc690-e691-4c38-8616-6e4f12a81975.png" alt="s1-disordered" /> </a></p>
</li>
</ul>
</li>
</ul>
<p>We don't need to intervene in it.</p>
<h3>Case 2</h3>
<ul>
<li>Event m1: event of user A created</li>
<li>Event m2: event of order 1 paid</li>
<li>Handler jobs: According to m1, create a <code>LocalUser</code> entity locally; according to m2, increase <code>LocalUser.Score</code></li>
<li>Analysis: m1 and m2 are causal; m1 and m2 are ordering sensitive, but the &quot;entity not found exception&quot; intercepts the disordering, so handlers are idempotent, which avoids the consistency problem
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg"><img src="https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png" alt="s2-disordered" /> </a></p>
</li>
</ul>
</li>
</ul>
<p>We don't need to intervene in it. After m1 is handled, m2 delays retry handling, essentially reaching the order.</p>
<h3>Case 3</h3>
<ul>
<li>Event m1: event of order 1 paid</li>
<li>Event m2: event of order 1 canceled</li>
<li>Handler jobs: According to m1, increase <code>LocalUser.Score</code>; according to m2, deduct <code>LocalUser.Score</code>; The score is minimum deducted to 0 and will not be a negative number</li>
<li>Analysis: m1 and m2 are causal; m1 and m2 are ordering sensitive, and handlers are not idempotent, so there will be a consistency problem
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=1wnVTL1RZWpvXu3YkHpj8,uFffnJLeWEMTLk3U33Z2ZA"><img src="https://user-images.githubusercontent.com/30018771/201470772-4a01a4fe-f933-4d2c-82cf-e59fd2905bec.png" alt="s3-disordered" /> </a></p>
</li>
</ul>
</li>
</ul>
<p>The user score service creates <code>LocalOrder</code> entities to record the order handling states.</p>
<pre><code class="language-CSharp">public class LocalOrder : AggregateRoot&lt;Guid&gt;
{
    public bool HasPaidEventHandled { get; set; } // set to true after handling m1
}
</code></pre>
<p>If the m2 handler finds <code>OrderCanceledEto.OrderPaidTime != null</code> but <code>LocalOrder.HasPaidEventHandled == false</code>, it throws an exception. After m1 is handled, m2 delays retry handling, essentially reaching the order.</p>
<p>We essentially transformed Case 3 into Case 2, thus achieving idempotency.</p>
<h4>After the Change</h4>
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg"><img src="https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png" alt="s3-resolved" /> </a></p>
</li>
</ul>
<h3>Case 4</h3>
<ul>
<li>Event m1: event of user A updated (<code>Region</code> changed)</li>
<li>Event m2: event of order 1 paid</li>
<li>Handler jobs: According to m1, clear <code>LocalUser.Score</code> if <code>UserEto.Region != LocalUser.Region</code>. According to m2, increase <code>LocalUser.Score</code></li>
<li>Analysis: m1 and m2 are causal; m1 and m2 are ordering sensitive, and handlers are not idempotent, so there will be a consistency problem
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=1wnVTL1RZWpvXu3YkHpj8,uFffnJLeWEMTLk3U33Z2ZA"><img src="https://user-images.githubusercontent.com/30018771/201470772-4a01a4fe-f933-4d2c-82cf-e59fd2905bec.png" alt="s4-disordered" /> </a></p>
</li>
</ul>
</li>
</ul>
<p>We can solve the problem with these changes:</p>
<ol>
<li>Add a new <code>RegionVersion</code> property in the <code>User</code> entity with the default value of 0. It increases by 1 once the user's region is changed.</li>
<li>The user score service uses <code>LocalUserRegion.Score</code> to record user scores instead of <code>LocalUser.Score</code>.
<pre><code class="language-CSharp">public class LocalUserRegion : AggregateRoot&lt;Guid&gt;
{
    public Guid UserId { get; set; }
    public string Region { get; set; }
    public int RegionVersion { get; set; }
    public int Score { get; set; }
}
</code></pre>
</li>
<li>When handling m1, if <code>UserEto.RegionVersion</code> is new, create an new <code>LocalUserRegion</code> entity with the initial score of 0, which equals clearing the user's scores once his region is changed.</li>
<li>When the user pays, the local service invokes the Identity remote service to query and sets the found <code>UserDto.RegionVersion</code> as <code>OrderPaidEto.UserRegionVersion</code> in the event m2.</li>
<li>When handling m2, according to m1, increase the corresponding <code>LocalUserRegion.Score</code>.</li>
</ol>
<p>We made the handlers idempotent by disentangling the causality of m1 and m2.</p>
<h4>After the Change</h4>
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=6azro2d7yq3YVGqmmFkeE,vX5ZLgF_as_otPyRgZX0Yg"><img src="https://user-images.githubusercontent.com/30018771/201462287-6155f1b9-dd9f-4452-bb3d-921b2e1b876b.png" alt="s4-resolved" /> </a></p>
</li>
</ul>
<h3>Case 5: ABP Entity Synchronizer</h3>
<p>In the DDD practice of the ABP framework, modules use entity synchronizers to redundant data of external entities. A typical case is the BlogUserSynchronizer <a href="#references">[3]</a> of the Blogging module. What's unique about this case is that stale events can be skipped handling if not strictly required.</p>
<ul>
<li>Event m1: event of User A updated</li>
<li>Event m2: event of User A updated</li>
<li>Handler jobs: According to m1 and m2, update the user information in the <code>LocalUser</code> entity.</li>
<li>Analysis: Once m2 is handled earlier than m1, the stale data will overwrite the latest data, so there will be a consistency problem
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=SwmSL9qcgrFZA5UV8HuPD,_LiJ20bKVHx8D7x5c-KfAw"><img src="https://user-images.githubusercontent.com/30018771/201468277-40c792ce-9a9f-4b29-b46c-c4392b3b79bb.png" alt="s5-disordered" /> </a></p>
</li>
</ul>
</li>
</ul>
<p>Let's add a new integer property named <code>EntityVersion</code>. Its default value is 0 and increments by one every time the entity changes. When the entity synchronizer gets an <code>EntityUpdatedEto&lt;UserEto&gt;</code> event, skip handling if <code>UserEto.EntityVersion &lt;= LocalUser.EntityVersion</code> is satisfied. That's it, and we solved the problem. I tried implementing the above feature in the ABP framework. See PR #14197 <a href="#references">[4]</a>.</p>
<h4>After the Change</h4>
<ul>
<li><p>t1 &lt; t2 (ordered):</p>
<p><a href="https://excalidraw.com/#json=EzNloyRKYJa6rfvSNgm2l,HFAPhV9l9kZDT4SGJaZ-zA"><img src="https://user-images.githubusercontent.com/30018771/194246857-ec06763c-f2be-4d39-85b2-b5243fb37a65.png" alt="ordered" /> </a></p>
</li>
<li><p>t1 &gt; t2 (disordered):</p>
<p><a href="https://excalidraw.com/#json=L0ZI13yl9EYwWtyQC6hwK,CcVzzXgnznQSGA7x8qLGng"><img src="https://user-images.githubusercontent.com/30018771/201468757-793bc2bb-5d47-4c7d-bcff-32e705e24d1e.png" alt="s5-resolved" /> </a></p>
</li>
</ul>
<h2>Solution Summary</h2>
<p>We believe that there are the following ideas for solving event disordering.</p>
<ol>
<li>Try to keep the business logic of the DistributedEventHandler simple to spot potential event disordering problems.</li>
<li>In some cases, we can make handlers idempotent by recording the entity's states locally, as Case 3 did above.</li>
<li>In some cases, we can disentangle causality by improving the business design, as Case 4 did above.</li>
<li>Entity synchronizers can be designed with EntityVersion to avoid synchronizing to stale data.</li>
</ol>
<h2>Conclusion</h2>
<p>Even if your app is currently monolithic, you should be concerned about the event disordering. That is a preparation for possible architectural changes in the future. Also, please give up implementing linearizability, as it is impossible in microservices or multi-database scenarios.</p>
<p>In several cases mentioned in this article, it seems that it is not difficult for developers to find the hidden dangers of consistency problems. However, the business is more complex in actual production, and the diverse events will be hard-considered. Even if we find out all possible causalities and deal with them during development, can we ensure that nothing will go wrong when the business changes? The answer is probably no.</p>
<p>There is no silver bullet to the distributed consistency problem. It's always there. Developers can only reduce complexity, dissolve the causality or manually implement idempotency.</p>
<h2>References</h2>
<ol>
<li>Herlihy, Maurice P.; Wing, Jeannette M. (1987). &quot;Axioms for Concurrent Objects&quot;. Proceedings of the 14th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages, POPL '87. p. 13</li>
<li>Daniel Wu. (2021). &quot;Messaging Reliability and Ordering&quot;. https://danielw.cn/messaging-reliability-and-order</li>
<li>GitHub abpframework/abp repository. BlogUserSynchronizer.cs. https://github.com/abpframework/abp/blob/1275f2207fc39d850d23472294e456c8504f20d2/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserSynchronizer.cs</li>
<li>GitHub abpframework/abp repository. PR #14197. https://github.com/abpframework/abp/pull/14197</li>
</ol>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/1dcd32ea-25ef-3365-ff74-3a079416a51e" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/1dcd32ea-25ef-3365-ff74-3a079416a51e" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/use-stepping-to-perform-atomic-multistep-operations-4kqu8ewp</guid>
      <link>https://abp.io/community/posts/use-stepping-to-perform-atomic-multistep-operations-4kqu8ewp</link>
      <a10:author>
        <a10:name>gdlcf88</a10:name>
        <a10:uri>https://abp.io/community/members/gdlcf88</a10:uri>
      </a10:author>
      <category>workflow</category>
      <category>stepping</category>
      <category>transaction-management</category>
      <title>Use Stepping To Perform Atomic Multi-Step Operations</title>
      <description>Stepping is a distributed BASE jobs implementation. You can use it as a workflow engine, event outbox/inbox, email/SMS sender, remote invoker, and more.</description>
      <pubDate>Sun, 02 Oct 2022 19:41:46 Z</pubDate>
      <a10:updated>2026-10-26T21:25:57Z</a10:updated>
      <content:encoded><![CDATA[<h1>Use Stepping To Perform Atomic Multi-Step Operations</h1>
<p>Stepping is a distributed <a href="https://en.wikipedia.org/wiki/Eventual_consistency">BASE</a> jobs implementation. You can use it as a workflow engine, event outbox/inbox, email/SMS sender, remote invoker, and more.</p>
<p>We have provided documentation for the following languages: <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/README.md">English</a>, <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/README.zh-CN.md">简体中文</a>.</p>
<h2>What are <code>Job</code> and <code>Step</code> in Stepping?</h2>
<p><code>Job</code> is a distributed transaction unit, and <code>Step</code> is a specific task inside a job.</p>
<p>A job contains one or many steps, and the transaction manager will execute them in order. If step 1 fails, it will be retried until success, and then step 2 starts to execute.</p>
<h2>Scenarios for Using Stepping</h2>
<p><a href="https://excalidraw.com/#json=sSS0SSIWEQ3hLKuEgKQbf,g1ijMIFvKb7L8BuoiQYd0w"><img src="https://user-images.githubusercontent.com/30018771/190894723-dd4f1a17-f8f2-4d81-bea1-32f6ab7d4782.png" alt="WhatCanSteppingDo" /> </a></p>
<h3>Want To Execute Steps and Ensure Atomicity</h3>
<p>When you start a job, Stepping will eventually complete the steps you require. If the app crashes during the executions, the transaction manager will continue to execute the rest steps after it recovers.</p>
<p>Stepping will complete your steps one by one. If a step fails, it will be tried later until success, which makes the job <a href="https://coffeecodeclimb.com/2020/07/26/atomicity-and-idempotency-for-dummies/#atomicity">atomic</a>. Please ensure all your steps can eventually succeed after retrying unless it is a <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Steps.md#saga-step">Saga step</a>.</p>
<p>Stepping may already complete the current step when your app crashes during the execution. When your app recovers, Stepping will execute it redundantly. Therefore, all your steps should be <a href="https://coffeecodeclimb.com/2020/07/26/atomicity-and-idempotency-for-dummies/#idempotence">idempotent</a>.</p>
<h3>Want To Ensure Executing Steps After a DB Transaction Commits</h3>
<p>When you start a job with a DB transaction, Stepping will eventually complete the steps you require after the DB transaction commits.</p>
<p>You don't need to worry about the non-atomicity caused by the app crashes after the transaction commits but before the steps' execution. We have handled this case by using the DTM's <a href="https://en.dtm.pub/practice/msg.html">2-phase messaging</a> pattern.</p>
<p>Stepping also supports the &quot;multi-tenant with multi-DB&quot; scenario, meaning it works no matter how many different databases there are in your app.</p>
<h2>Examples</h2>
<p>The transaction manager will eventually complete the added steps:</p>
<pre><code class="language-csharp">var job = await distributedJobFactory.CreateJobAsync();

job.AddStep(new RequestBank1TransferOutStep(args)); // step with args
job.AddStep&lt;RequestBank2TransferInStep&gt;(); // step without args

await job.StartAsync();
</code></pre>
<p>The <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Steps.md">Steps document</a> shows how to define a step.</p>
<p>If you want to execute the steps after a DB transaction commits and ensure they will eventually be done:</p>
<pre><code class="language-csharp">var db = serviceProvider.GetRequiredService&lt;MyDbContext&gt;(); // example for EF Core
await db.Database.BeginTransactionAsync();

var order = new Order(args);

db.Orders.Add(order);
await db.SaveChangesAsync();

var job = await distributedJobFactory.CreateJobAsync(new EfCoreSteppingDbContext(db));

job.AddStep(new SendOrderCreatedEmailStep(order));
job.AddStep(new SendOrderCreatedSmsStep(order));

await job.StartAsync(); // it will commit the DB transaction
</code></pre>
<p>Stepping supports <code>EF Core</code>, <code>ADO.NET</code>(coming soon), and <code>MongoDB</code>.</p>
<p>For details, please see the <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Usage.md">Usage document</a>.</p>
<h2>Installation</h2>
<p>See the <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Installation.md">Installation document</a>.</p>
<h2>Supported Transaction Managers</h2>
<p>Stepping requires transaction managers. You can choose an implementation you like.</p>
<h3>DTM Server</h3>
<p>DTM is a mature transaction manager you can use as the TM provider for Stepping. DTM allows you to use many other distributed transaction patterns like Saga, TCC, and XA.</p>
<p>See the <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/Dtm.md">DTM document</a>.</p>
<h3>Local-TM</h3>
<p>Stepping provides a simple built-in TM implementation. The local-TM runs with your app as a local transaction manager. Which app starts a job should be the TM of this job.</p>
<p>See the <a href="https://github.com/TeamStepping/Stepping.NET/blob/main/docs/LocalTm.md">Local-TM document</a>.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/1b971026-c027-7d58-f40f-3a06ac64df2d" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/1b971026-c027-7d58-f40f-3a06ac64df2d" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/dtm-support-abp-multitenant-multidatabase-scenario-3q535ctg</guid>
      <link>https://abp.io/community/posts/dtm-support-abp-multitenant-multidatabase-scenario-3q535ctg</link>
      <a10:author>
        <a10:name>gdlcf88</a10:name>
        <a10:uri>https://abp.io/community/members/gdlcf88</a10:uri>
      </a10:author>
      <category>multi-tenancy</category>
      <category>DTM</category>
      <title>DTM support ABP Multi-Tenant Multi-Database Scenario</title>
      <description>这篇文章分享了使用 DTM 二阶段消息模式解决 issue #10036 的方法。</description>
      <pubDate>Thu, 02 Jun 2022 13:24:25 Z</pubDate>
      <a10:updated>2026-10-26T17:39:26Z</a10:updated>
      <content:encoded><![CDATA[<h1>引入 DTM 以支持 ABP 的多租户多数据库场景</h1>
<p>这篇文章分享了使用 DTM 二阶段消息模式解决 <a href="https://github.com/abpframework/abp/issues/10036">issue #10036</a> 的方法。</p>
<p>今天我们要使用 EasyAbp 的 <a href="https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm">Abp.EventBus.Boxes.Dtm</a> 模块。</p>
<h2>DTM 事件箱的介绍</h2>
<p>这个模块使用了 DTM 的 <a href="https://dtm.pub/practice/msg.html">二阶段消息</a> 使得 ABP 的事件箱得以支持 <a href="https://github.com/abpframework/abp/issues/10036">多租户多数据库场景</a>。</p>
<p>你需要先阅读 <a href="https://en.dtm.pub/guide/start.html">DTM 文档</a>，它将帮助你理解这个模块。</p>
<h2>与 ABP 默认事件箱的差异</h2>
<p>|                                  | DTM 二阶段消息事件箱 |            ABP 5.0+ 默认事件箱             |
| :------------------------------: | :------------------: | :----------------------------------------: |
|             收发速度             |  :heavy_check_mark:  |                    :x:                     |
|          更少的数据传输          |         :x:          |             :heavy_check_mark:             |
|  保证事件发出<br>(事务工作单元)  |  :heavy_check_mark:  |             :heavy_check_mark:             |
| 保证事件发出<br>(非事务工作单元) |         :x:          | :heavy_check_mark:<br>(要求消费端解决幂等) |
|    避免重复处理(仅限纯DB操作)    |  :heavy_check_mark:  |             :heavy_check_mark:             |
|        支持多租户多数据库        |  :heavy_check_mark:  |                    :x:                     |
|         没有增加外部设施         |         :x:          |             :heavy_check_mark:             |
|          管理面板和报警          |  :heavy_check_mark:  |                    :x:                     |</p>
<h2>DTM 发件箱是如何工作的？</h2>
<p>假设你正在使用发件箱发布新的事件:</p>
<pre><code class="language-csharp">await _distributedEventBus.PublishAsync(eto1, useOutbox: true);
await _distributedEventBus.PublishAsync(eto2, useOutbox: true);  // useOutbox 的默认值即 true
</code></pre>
<p>DTM 发件箱会临时存储这些事件。接下来我们看看，在你完成当前工作单元时它会怎么做：</p>
<pre><code class="language-CSharp">// UnitOfWork.cs 的代码片段
protected override async Task CommitTransactionsAsync()
{
    // 第 1 步：在事务内插入一条记录到 DTM 屏障表，接着发送一条“prepare”请求到 DTM 服务器
    await DtmMessageManager.InsertBarriersAndPrepareAsync(EventBag);

    // 第 2 步: 提交当前 DB 事务
    await base.CommitTransactionsAsync();

    // 第 3 步: 发送一条&quot;submit&quot;请求到 DTM 服务器
    OnCompleted(async () =&gt; await DtmMessageManager.SubmitAsync(EventBag));
}
</code></pre>
<p>至此，DTM 服务器已经收到了一条“submit”请求。它会调用 app 的 <code>PublishEvents</code> 服务并附上所有事件的数据，后者被调用后会立即发布这些事件到 MQ。</p>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqNVMtOwkAU_ZXJrOEHujAx4sIFC4LuuhnoKE1owXa6MMYEY1BESRUxPoKvRBOjkUBMBMvvdKbyF06dCqWUhK7mzj33nnNP7nQX5ksKhhI08baF9TxOqWjLQJqsA_4RlRQxSK2nAbXP3eGX1-mJBLJISbe0HDZE7LVeWa2fXFqiToufJUCrn6OrD3dQcQdvQDVNCwugyHMgu-yyRoc6FxJgrW9qXwsU8OpfrLIfAU8p4AX9Ia0_gjW_YKOsIIIVQJun4iJLeLxSQPoWv3WdE14kuk01iSiwm7T6wtWyuyeB69mj23Z8nR-zdoPWn-gNn9QPywYuIwNP8ON8clZ9qACw2hmtP8Tz_Fs5Dx9rpX3mOs98as4uUONkMtQyDIttGTV8Dn4Ba0wrp6lkMWdY7Zx2D2nnlHOw48qoXQEoT9SSbsbRRSwSRHEOcaDYT0lsSLCc0UEmyiaNl__oQdKnpYOD8DqNvUpnJOC9n9DG5ywonQnLFPl4hRHrAmLhRbzGSAV7PPrpdL3hBbtvuwNHsMAE1LChIVXhT3zX7yNDUsAalqHEjwreRFaRyFDW9zjU-ntLq4pKSgaUNlHRxAnoP_Xsjp6HEjEs_A8KfhMBau8Xdo4RAw"><img src="https://mermaid.ink/img/pako%3AeNqNVMtOwkAU_ZXJrOEHujAx4sIFC4LuuhnoKE1owXa6MMYEY1BESRUxPoKvRBOjkUBMBMvvdKbyF06dCqWUhK7mzj33nnNP7nQX5ksKhhI08baF9TxOqWjLQJqsA_4RlRQxSK2nAbXP3eGX1-mJBLJISbe0HDZE7LVeWa2fXFqiToufJUCrn6OrD3dQcQdvQDVNCwugyHMgu-yyRoc6FxJgrW9qXwsU8OpfrLIfAU8p4AX9Ia0_gjW_YKOsIIIVQJun4iJLeLxSQPoWv3WdE14kuk01iSiwm7T6wtWyuyeB69mj23Z8nR-zdoPWn-gNn9QPywYuIwNP8ON8clZ9qACw2hmtP8Tz_Fs5Dx9rpX3mOs98as4uUONkMtQyDIttGTV8Dn4Ba0wrp6lkMWdY7Zx2D2nnlHOw48qoXQEoT9SSbsbRRSwSRHEOcaDYT0lsSLCc0UEmyiaNl__oQdKnpYOD8DqNvUpnJOC9n9DG5ywonQnLFPl4hRHrAmLhRbzGSAV7PPrpdL3hBbtvuwNHsMAE1LChIVXhT3zX7yNDUsAalqHEjwreRFaRyFDW9zjU-ntLq4pKSgaUNlHRxAnoP_Xsjp6HEjEs_A8KfhMBau8Xdo4RAw" alt="" /> </a></p>
<details>
<summary>查看更详细的时序图</summary>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNq1VU1PGkEY_iuTPbWJeG5IY9LWHnrwYGxvXFZ2tCSw2GX30BgTqlIRQfzAgnwoNtKQKlSDQeSj_Bc7M7uc-At9l0FcdGk1afc0H88z7_s-z7szi4LbL2HBKQTwBw3LbjzpEecV0eeSEXyqR_ViNPl2CtH4NmlU9fI5cqDJl4jUN2jkiMW3SP1Yb-6y0FfSTNNvy6S1120ecrKoqX5Z881ihc_1RJGFLx0TE7SegLET0VClkyyRWpDUviNPIKBhDuT7AGR7ZyxWpvVdJ2KJKxpPcRTSI1UW_HQHPJQlEC4bNJJHb0zCuwVJVLGE6E6UL8yoMH_1XpTnYRVqARI_beiQOxnEd2ioANmy3BHHncc76aw9z5yzbAw0ovtQqTldUPCCqOBb_GDfcT97CwGx8BaNHNrHuZFyFN5Wyp5r3EG7bO5lr5fWSf0zLbQ6e22WrHaSF-Pj4_bMgbXJYrcZJa280U7RbJFuJaEtWHibrVRG6Ph8Vpl4QlptoJOrDboeuw5mLVVdB3MsUaXhMz29iuY90tN-j3nV4RxopEhaWZa5oO0TDhklA6nVh6nWxKzemt-A57AW2QNxmlV0S8T7Yv6JZBuFG9VthvXTU_QMsVKh21znHtLjc-OiYCpHNw9pJs9SP0xtUGctRo9jYADNHLBG-jbAX4xmqU19vwVe_wouG-19thqn2xU9B_9n2oT0osGuGZCFvxjBEPmZ01erYLDRzhhHUVIrAZPl14zyGcirN3bZQRYG1jSwN4D76th79VgRRij_H71-tOwPagZ-6j_yiosCu6PPu6nshVv1-GW41uFeobUV6204JOfUNNwEJxs0VrEHTk0P6dXDPPiv6CdBy1GgPFyDXqcN2swaDcuSSxbGBB9WfKJHggdu0dxwCep77MMuwQlDCc-Jmld1CS55CaBa75V4LXlUvyI450Ro1DHBfMRmPspuwakqGr4B9R_JPmrpN52bxD0"><img src="https://mermaid.ink/img/pako%3AeNq1VU1PGkEY_iuTPbWJeG5IY9LWHnrwYGxvXFZ2tCSw2GX30BgTqlIRQfzAgnwoNtKQKlSDQeSj_Bc7M7uc-At9l0FcdGk1afc0H88z7_s-z7szi4LbL2HBKQTwBw3LbjzpEecV0eeSEXyqR_ViNPl2CtH4NmlU9fI5cqDJl4jUN2jkiMW3SP1Yb-6y0FfSTNNvy6S1120ecrKoqX5Z881ihc_1RJGFLx0TE7SegLET0VClkyyRWpDUviNPIKBhDuT7AGR7ZyxWpvVdJ2KJKxpPcRTSI1UW_HQHPJQlEC4bNJJHb0zCuwVJVLGE6E6UL8yoMH_1XpTnYRVqARI_beiQOxnEd2ioANmy3BHHncc76aw9z5yzbAw0ovtQqTldUPCCqOBb_GDfcT97CwGx8BaNHNrHuZFyFN5Wyp5r3EG7bO5lr5fWSf0zLbQ6e22WrHaSF-Pj4_bMgbXJYrcZJa280U7RbJFuJaEtWHibrVRG6Ph8Vpl4QlptoJOrDboeuw5mLVVdB3MsUaXhMz29iuY90tN-j3nV4RxopEhaWZa5oO0TDhklA6nVh6nWxKzemt-A57AW2QNxmlV0S8T7Yv6JZBuFG9VthvXTU_QMsVKh21znHtLjc-OiYCpHNw9pJs9SP0xtUGctRo9jYADNHLBG-jbAX4xmqU19vwVe_wouG-19thqn2xU9B_9n2oT0osGuGZCFvxjBEPmZ01erYLDRzhhHUVIrAZPl14zyGcirN3bZQRYG1jSwN4D76th79VgRRij_H71-tOwPagZ-6j_yiosCu6PPu6nshVv1-GW41uFeobUV6204JOfUNNwEJxs0VrEHTk0P6dXDPPiv6CdBy1GgPFyDXqcN2swaDcuSSxbGBB9WfKJHggdu0dxwCep77MMuwQlDCc-Jmld1CS55CaBa75V4LXlUvyI450Ro1DHBfMRmPspuwakqGr4B9R_JPmrpN52bxD0" alt="" /> </a></p>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFVMtuGjEU_RVrVq0U-ABUsWjpoouu0u7YTBiHIMFAh5lFFUWiKaSEQIEEyiOQgBQqVAkEIiJkgPIvqe0ZVvmF3okTCmFQvfK1z7k-91zb-4IvLGHBJUTxJw3LPuwJiH5FDHllBEMNqEGMPB_eI5rNk_HQ6PaRA3leI6Kf0FSTZXNEv6JXffO6RSZV-vOQTIv3k0tOFjU1LGuhHazw2Ci0WfLG4XZTvQBzF6KJwbzUIaMYGf1CgWhUwxzI9wHIij2W6VL9zIVY4ZZmyxyFjNSQxb48A6-oBMLNmKYa6J1F-BiRRBVLiJ6m-cK2CvGbPVH2wyrUAiSebSXJMwXZU5pogVpWb3JcPzuv1ux5VsxqGfCIVqBSK4woOCIq-B9-se9YV79EQCyZo6lL-3OerNyEt7XyoWu8gxy12HQspVxv8f0kTc8v2Li6khsovLOQ-SxDpjUOtqtzzRejc0z0I9qazoszVhrOS9dOp9Oeubg0pTboINOGOSvTWpvmSnDhWDLPvg42dOjVjuJ-QaYzoJPbE3qcuYvVlvy6i9VZYUiTPaMaR_6A9HKzc2Skr-paPnH5OtgaygGcYtOjdXc2Ef5jKit_NypT8PVP7NCcVVg8S_MDow6vrGpB-GvVjyxbWPKHGUuQ33UjPgQzzdm52UyTUQeYrPHN7PagYmN8xi5qMOG9F7aEEFZCYkCCT2PfEuQV1D0cwl7BBVMJ74paUPUKXvkAoNrDy3srBdSwIrh2xWAUbwnWx7D9WfYJLlXR8BPo8eN5RB38BaxhZok"><img src="https://mermaid.ink/img/pako%3AeNqFVMtuGjEU_RVrVq0U-ABUsWjpoouu0u7YTBiHIMFAh5lFFUWiKaSEQIEEyiOQgBQqVAkEIiJkgPIvqe0ZVvmF3okTCmFQvfK1z7k-91zb-4IvLGHBJUTxJw3LPuwJiH5FDHllBEMNqEGMPB_eI5rNk_HQ6PaRA3leI6Kf0FSTZXNEv6JXffO6RSZV-vOQTIv3k0tOFjU1LGuhHazw2Ci0WfLG4XZTvQBzF6KJwbzUIaMYGf1CgWhUwxzI9wHIij2W6VL9zIVY4ZZmyxyFjNSQxb48A6-oBMLNmKYa6J1F-BiRRBVLiJ6m-cK2CvGbPVH2wyrUAiSebSXJMwXZU5pogVpWb3JcPzuv1ux5VsxqGfCIVqBSK4woOCIq-B9-se9YV79EQCyZo6lL-3OerNyEt7XyoWu8gxy12HQspVxv8f0kTc8v2Li6khsovLOQ-SxDpjUOtqtzzRejc0z0I9qazoszVhrOS9dOp9Oeubg0pTboINOGOSvTWpvmSnDhWDLPvg42dOjVjuJ-QaYzoJPbE3qcuYvVlvy6i9VZYUiTPaMaR_6A9HKzc2Skr-paPnH5OtgaygGcYtOjdXc2Ef5jKit_NypT8PVP7NCcVVg8S_MDow6vrGpB-GvVjyxbWPKHGUuQ33UjPgQzzdm52UyTUQeYrPHN7PagYmN8xi5qMOG9F7aEEFZCYkCCT2PfEuQV1D0cwl7BBVMJ74paUPUKXvkAoNrDy3srBdSwIrh2xWAUbwnWx7D9WfYJLlXR8BPo8eN5RB38BaxhZok" alt="" /> </a></p>
</details>
<blockquote>
<p>如果你依然对这个模式“如何确保发送”困惑，请查看 DTM 的 <a href="https://en.dtm.pub/practice/msg.html">二阶段消息文档</a> 以了解更多。</p>
</blockquote>
<h2>DTM 收件箱是如何工作的？</h2>
<p>与 ABP 的默认实现不同，DTM 收件箱从 MQ 收到一个事件后会立即处理（handle）。在所有的 handler 完成他们的工作后，收件箱会沿用当前事务，向 DTM 屏障表插入一条记录。最后它提交了事务并向 MQ 返回 ACK。</p>
<p>所有入箱的事件都拥有一条唯一的 MessageId。拥有相同 MessageId 的事件只会被处理一次，因为我们不能插入 gid (MessageId) 重复的记录到 DTM 屏障表。</p>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFk8tKw0AUhl9lmLV9gaAFURciXYgus4nNtAaaVHNZSClUULEt0qgRRFJKQSUI9uKitvH2MjkTu_IVnDptMRptVknOf775zz8zBZzOywQL2CC7FtHSZFmRsrqkihpij6mYOYKWN1OIOr3gqRe2urwgWWZes9QtovPv1HoimYzoBDQsnQ1L-0G_FPTvaPWGuuX5LT0JTpv9QiliGFKWrMoovDoI_Cpr4qQIhEHpRYeetMA_F75K0K0Nr9z3pkcv22Dfwv0luN6Im1VktBDFvrc68HLx8dzg5Ckp8dsrx1L3jpZf4bgTbwV8J3S86WDfTPNK1C1cH4T2EVfBoBe81ZlZ6gyg4sX4mbC5AB4fuD6C_217TP9aiR7bUGnMDBHs0x9B1s7g8IbtCa03J0EGfT8uytk5xtr_xw2t2YF_zcaASnM2net4z9_jsr7UuoAWl9bwHFaJrkqKzA54YaQVsblNVCJigb3KJCNZOVPEolZkUmtHlkyyIitmXsdCRsoZZA6PDvrGnpbGgqlbZCIaX5KxqvgJeeqquw"><img src="https://mermaid.ink/img/pako%3AeNqFk8tKw0AUhl9lmLV9gaAFURciXYgus4nNtAaaVHNZSClUULEt0qgRRFJKQSUI9uKitvH2MjkTu_IVnDptMRptVknOf775zz8zBZzOywQL2CC7FtHSZFmRsrqkihpij6mYOYKWN1OIOr3gqRe2urwgWWZes9QtovPv1HoimYzoBDQsnQ1L-0G_FPTvaPWGuuX5LT0JTpv9QiliGFKWrMoovDoI_Cpr4qQIhEHpRYeetMA_F75K0K0Nr9z3pkcv22Dfwv0luN6Im1VktBDFvrc68HLx8dzg5Ckp8dsrx1L3jpZf4bgTbwV8J3S86WDfTPNK1C1cH4T2EVfBoBe81ZlZ6gyg4sX4mbC5AB4fuD6C_217TP9aiR7bUGnMDBHs0x9B1s7g8IbtCa03J0EGfT8uytk5xtr_xw2t2YF_zcaASnM2net4z9_jsr7UuoAWl9bwHFaJrkqKzA54YaQVsblNVCJigb3KJCNZOVPEolZkUmtHlkyyIitmXsdCRsoZZA6PDvrGnpbGgqlbZCIaX5KxqvgJeeqquw" alt="" /> </a></p>
<details>
<summary>查看更详细的时序图</summary>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqVVNFKG0EU_ZVhnvUHljYgtQ-lpCD1cV_W7JguZDd1M_tQREjBFhMrWXUFibE2kEgQGpNS0rjW9mf2TpKn_kJnczfGrWti52lm7rnnnjvnMps0k9cZVWiBbTjMyrBlQ8vamqlaRC5u8Bwjy6tpIrxecN0btLsY0Byetxxzjdl4Tq8splIxnEJGxYNR8X3QLwb9C7HbFLXSkzU7Bd6lvCJpVihoWfZCJ4PqduDvyiRkipFIUnHUEXtt8A-VcQi6lVG1Nqy3xPEluOfw9RhqrZA3a-jkaZx22O7AzdGfn2eR5hwniMcA3obrtsbi_S5E6RfsdLDsNCOuUmalVxSy9OwlIpil42YWMVKK2gVWSO4efG_gtW7f8s47YST-QNDYHrgfEQVXveD3qexXeFdQbiXomXAjAH58Q3yM_r7siH1cSey4UD6b6xu4-_94VzmAD005BuK0PvEu6PtJ7k2twyRodIffm4-zDk6-kJBathb5XvyUzDJL_MlncV2Fm0Mo7cneoVx_XHHEhqXHBHNG59X_zU6iZTOaEBU38Bt35c-XjjkPWzwderpATWabmqHLf2QzxKqUv2EmU6kitzpb15wcV6lqbUmo81bXOHuuGzxvU2VdyxXYAg3_k9fvrAxVuO2wCSj6iyLU1l-RFVkc"><img src="https://mermaid.ink/img/pako%3AeNqVVNFKG0EU_ZVhnvUHljYgtQ-lpCD1cV_W7JguZDd1M_tQREjBFhMrWXUFibE2kEgQGpNS0rjW9mf2TpKn_kJnczfGrWti52lm7rnnnjvnMps0k9cZVWiBbTjMyrBlQ8vamqlaRC5u8Bwjy6tpIrxecN0btLsY0Byetxxzjdl4Tq8splIxnEJGxYNR8X3QLwb9C7HbFLXSkzU7Bd6lvCJpVihoWfZCJ4PqduDvyiRkipFIUnHUEXtt8A-VcQi6lVG1Nqy3xPEluOfw9RhqrZA3a-jkaZx22O7AzdGfn2eR5hwniMcA3obrtsbi_S5E6RfsdLDsNCOuUmalVxSy9OwlIpil42YWMVKK2gVWSO4efG_gtW7f8s47YST-QNDYHrgfEQVXveD3qexXeFdQbiXomXAjAH58Q3yM_r7siH1cSey4UD6b6xu4-_94VzmAD005BuK0PvEu6PtJ7k2twyRodIffm4-zDk6-kJBathb5XvyUzDJL_MlncV2Fm0Mo7cneoVx_XHHEhqXHBHNG59X_zU6iZTOaEBU38Bt35c-XjjkPWzwderpATWabmqHLf2QzxKqUv2EmU6kitzpb15wcV6lqbUmo81bXOHuuGzxvU2VdyxXYAg3_k9fvrAxVuO2wCSj6iyLU1l-RFVkc" alt="" /> </a></p>
</details>
<blockquote>
<p>正如你注意到的，DTM 服务器没有参与收件箱的工作。🤭</p>
</blockquote>
<h2>安装和使用</h2>
<p>请阅读 https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm/tree/main#installation.</p>
<h2>后记</h2>
<p>这样的一个 DTM 事件箱实现并不是完美的解决方案。这里最大的成本是，你需要额外关心 DTM 服务器的可用性。但是如果你遇到了多租户多数据库的场景，它就是现在最佳的选择。</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/ae0312f4-40eb-873f-9349-3a0436c38c1b" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/ae0312f4-40eb-873f-9349-3a0436c38c1b" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/introduce-dtm-for-multitenant-multidatabase-scene-23ziikbe</guid>
      <link>https://abp.io/community/posts/introduce-dtm-for-multitenant-multidatabase-scene-23ziikbe</link>
      <a10:author>
        <a10:name>gdlcf88</a10:name>
        <a10:uri>https://abp.io/community/members/gdlcf88</a10:uri>
      </a10:author>
      <category>EasyAbp</category>
      <category>transaction-management</category>
      <title>Introduce DTM for Multi-Tenant Multi-Database Scene</title>
      <description>This article shares a new way to solve issue #10036 using the DTM's 2-phase messages pattern.</description>
      <pubDate>Thu, 02 Jun 2022 12:41:27 Z</pubDate>
      <a10:updated>2026-10-26T19:52:10Z</a10:updated>
      <content:encoded><![CDATA[<h1>Introduce DTM for Multi-Tenant Multi-Database Scene</h1>
<p>This article shares a new way to solve <a href="https://github.com/abpframework/abp/issues/10036">issue #10036</a> using the DTM's 2-phase messages pattern.</p>
<p>The module to be used today is EasyAbp's <a href="https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm">Abp.EventBus.Boxes.Dtm</a>.</p>
<h2>Introduction of the DTM Event Boxes Module</h2>
<p>This implementation uses DTM's <a href="https://en.dtm.pub/practice/msg.html">2-phase messages</a> to support ABP event boxes in the <a href="https://github.com/abpframework/abp/issues/10036">multi-tenant &amp; multi-database scene</a>.</p>
<p>You should see the <a href="https://en.dtm.pub/guide/start.html">DTM docs</a>, which helps to understand this module.</p>
<h2>Differences From the ABP's Default Event Boxes</h2>
<p>|                                                                            | DTM 2-phase Message Boxes |                 ABP 5.0+ Default Boxes                 |
|:--------------------------------------------------------------------------:| :-----------------------: | :----------------------------------------------------: |
|                                 Speediness                                 |    :heavy_check_mark:     |                          :x:                           |
|                             Less data transfer                             |            :x:            |                   :heavy_check_mark:                   |
|              Be guaranteed to publish<br>(transactional UOW)               |    :heavy_check_mark:     |                   :heavy_check_mark:                   |
|            Be guaranteed to publish<br>(non-transactional UOW)             |            :x:            | :heavy_check_mark:<br>(consumers idempotency required) |
|           Avoid duplicate handling<br>(with only DB operations)            |    :heavy_check_mark:     |                   :heavy_check_mark:                   |
|                       Multi-tenant-database support                        |    :heavy_check_mark:     |                          :x:                           |
|                   No additional external infrastructure                    |            :x:            |                   :heavy_check_mark:                   |
|                            Dashboard and Alarm                             |    :heavy_check_mark:     |                          :x:                           |</p>
<h2>How Does the DTM Outbox Work?</h2>
<p>You are publishing events using the ABP event outbox:</p>
<pre><code class="language-csharp">await _distributedEventBus.PublishAsync(eto1, useOutbox: true);
await _distributedEventBus.PublishAsync(eto2, useOutbox: true);  // The useOutbox is true by default.
</code></pre>
<p>The DTM outbox collects them temporarily. Let's see what it will do when you complete the current unit of work:</p>
<pre><code class="language-CSharp">// Code snippet for UnitOfWork.cs
protected override async Task CommitTransactionsAsync()
{
    // Step 1: inserting a record to the DTM barrier table within the current DB transaction,
    //         and then it sends a &quot;prepare&quot; request to the DTM server.
    await DtmMessageManager.InsertBarriersAndPrepareAsync(EventBag);

    // Step 2: committing the current DB transaction.
    await base.CommitTransactionsAsync();

    // Step 3: sending a &quot;submit&quot; request to the DTM server.
    OnCompleted(async () =&gt; await DtmMessageManager.SubmitAsync(EventBag));
}
</code></pre>
<p>Now, the DTM server has received a &quot;submit&quot; request. It invokes the app's <code>PublishEvents</code> service with the events' data, and the latter will publish the events to the MQ provider immediately.</p>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFk89uwjAMxl_Fypm-QA5IaOzAAW2IcevFbVyIlqRd_rAhxLsvbVooUG09NfHPX7449pmVtSDGmaOvQKakpcS9RZ0biJ-XXhEsP9bwFnxR_6RdDL42QRdk03rnyGbz-aJpOLyo2hGgAelcoBSPgRheoscCHUXmgGZPiQDn0d9z19M4LISAVYvtGhExEYX7jW2bloQE0JGMd0nkln535spEkx6wixdorRzc3yfExZbskSzvAo2lBi3dyBTMHnyOUDGh2lXmmXmqS6219OAPBN6icVh6WZter6eya6ET7EJZEokHyZG1ae7PS7tQxJT_7ryCb6kUVNJId-hMJ7_P7zCuQNKesh2ptpF4el8ou0aasN276TUX3ZmQwXso1GBk3A-pIusNnyBAak1Cxk5Sp0SvN1e3Az5tdVyz3oOoDU3YHJNrtJ-ALmk6VwXFZkyT1ShFnMFzm56zaFBTznj8FVRhUD5nublENHRj8Cqkry3jFSpHM9aO4_ZkSsa9DTRA_Rz31OUXaXxJzw"><img src="https://mermaid.ink/img/pako%3AeNqFk89uwjAMxl_Fypm-QA5IaOzAAW2IcevFbVyIlqRd_rAhxLsvbVooUG09NfHPX7449pmVtSDGmaOvQKakpcS9RZ0biJ-XXhEsP9bwFnxR_6RdDL42QRdk03rnyGbz-aJpOLyo2hGgAelcoBSPgRheoscCHUXmgGZPiQDn0d9z19M4LISAVYvtGhExEYX7jW2bloQE0JGMd0nkln535spEkx6wixdorRzc3yfExZbskSzvAo2lBi3dyBTMHnyOUDGh2lXmmXmqS6219OAPBN6icVh6WZter6eya6ET7EJZEokHyZG1ae7PS7tQxJT_7ryCb6kUVNJId-hMJ7_P7zCuQNKesh2ptpF4el8ou0aasN276TUX3ZmQwXso1GBk3A-pIusNnyBAak1Cxk5Sp0SvN1e3Az5tdVyz3oOoDU3YHJNrtJ-ALmk6VwXFZkyT1ShFnMFzm56zaFBTznj8FVRhUD5nublENHRj8Cqkry3jFSpHM9aO4_ZkSsa9DTRA_Rz31OUXaXxJzw" alt="" /> </a></p>
<details>
<summary>See the more detailed sequence diagram</summary>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqtVU1v2zAM_SuETy2Q5FwYQ4ps6TYDC7YiLXrJhbaYRKgsefpoFhT976O_YidxgK5YTpH5SD4-ktJrlBlBURw5-h1IZzSXuLGYrzTwz0uvCOYPC_gZfGr-wBietuhBrsFv2fAZvEXtMPPSaMhMnkvvpd6AdOCU2d3WYTB4o0Oekq3Pj47seDqdFUUMX5RxBKjZxQWq7Wxg8xw9puiIMVvUG6oR4Dz6Y9yBXgwzISApYY-FYJjgwM2HZelWBxJAL6S9q4N07kc5E80kPWBlT9Fa2bI_duDDkuwL2bgyFJYKtNQha-P4hGcPKgaiVsqcY850qQSvWtHrw1nuY5ZPyC5rY7k0ZbhVXuY0mUwGvCoW32k_AqYAexNgS5ZuIYEdak5rQA6I9Cm106ud9NuKl8Oc4FsyB3TVeRX1VYrAlmPnPDhuyHUzLcr3eJSjtEbnW_WHdDj0qqFQldeF6Bxbl3FbXePpQpYRiVbnNseJcpfBZ3EfemtxxeQLuLlu9gO-zpIfd_NSJsHjzCqKUCiZ8XjCRooRWKMUz2iK2XMvw6V2JiwdTeChXMucOOvpONQCSg5ZpmRhnJdKcQtMRs7xsk5ggfa5bFCZuUtLiheTqxnqwD8VfEHd_9rBD4l6uce1YB-Rv6Rw2oGhiTlbtFmNHcOvkCrp6vXpX1Sdbov7eAAFMs9JSC5Z7TuPxf1BqtblfdPe8BFG0zt0aEeoiu3cOqhmiDRniUZRTjZHKfiheS0Nq4h553wBxPxX0BqD8qtopd8YGqqr-05Ib2wUr5HHcBSVT8hyr7Mo9jZQC2oeqwb19he08zVf"><img src="https://mermaid.ink/img/pako%3AeNqtVU1v2zAM_SuETy2Q5FwYQ4ps6TYDC7YiLXrJhbaYRKgsefpoFhT976O_YidxgK5YTpH5SD4-ktJrlBlBURw5-h1IZzSXuLGYrzTwz0uvCOYPC_gZfGr-wBietuhBrsFv2fAZvEXtMPPSaMhMnkvvpd6AdOCU2d3WYTB4o0Oekq3Pj47seDqdFUUMX5RxBKjZxQWq7Wxg8xw9puiIMVvUG6oR4Dz6Y9yBXgwzISApYY-FYJjgwM2HZelWBxJAL6S9q4N07kc5E80kPWBlT9Fa2bI_duDDkuwL2bgyFJYKtNQha-P4hGcPKgaiVsqcY850qQSvWtHrw1nuY5ZPyC5rY7k0ZbhVXuY0mUwGvCoW32k_AqYAexNgS5ZuIYEdak5rQA6I9Cm106ud9NuKl8Oc4FsyB3TVeRX1VYrAlmPnPDhuyHUzLcr3eJSjtEbnW_WHdDj0qqFQldeF6Bxbl3FbXePpQpYRiVbnNseJcpfBZ3EfemtxxeQLuLlu9gO-zpIfd_NSJsHjzCqKUCiZ8XjCRooRWKMUz2iK2XMvw6V2JiwdTeChXMucOOvpONQCSg5ZpmRhnJdKcQtMRs7xsk5ggfa5bFCZuUtLiheTqxnqwD8VfEHd_9rBD4l6uce1YB-Rv6Rw2oGhiTlbtFmNHcOvkCrp6vXpX1Sdbov7eAAFMs9JSC5Z7TuPxf1BqtblfdPe8BFG0zt0aEeoiu3cOqhmiDRniUZRTjZHKfiheS0Nq4h553wBxPxX0BqD8qtopd8YGqqr-05Ib2wUr5HHcBSVT8hyr7Mo9jZQC2oeqwb19he08zVf" alt="" /> </a></p>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNp1VMtu2zAQ_JUFTy1g6wOEwoFbp62ABjk4QS66rMW1TUQkVXKV1Ajy711Sit_RSdTODmdnSL2pxmtSpYr0tyfX0MLgJqCtHcjDhluCxcMd3Pe88v9gCk9bZDBr4K0UvgMHdBEbNt5B4601zKRhjaYlfTOQYM_e9XZFYVg_RgrT2WzedSX8aH0kQAcmxp6GuhSkvEDGFUYSzBbdhgYEREY-xe3FlTDXGqoEe-w0Jh3oxg_L1DYQaaAXchwHkkP7yZ6VE5EMmOsrDMF8qD9tkMWSwguFMhe6QB0GOiCH4vRM5xFUX2HNzlxiLnzJducgjlIY-UbUdG_0AP45r_7cLiYQfCsByWTN84FcsCmbEu47CpgjHYK8mOd08icU5rUPYlfr3UZOjaWiKK50ZSm_aTcBGQt2voctBbqBCl7RySgezBXjv63C7Mur4W2eNaIl-FUtAGNe1-rYeQUhHeTIECXkr9ed26c7bpDFH4R-YuHYFfumIdLHqZz5cR34qX2VSKUCHtLFsiRBnkcqR38MIjkhUiObtpWRfUMxGrcp4A7DczIk5ZpCVRNlKVg0Wq72WxJQK2G1YlApr5rW2Ldcq9q9C7TP1-VWG_ZBlWtsI01UurbLnWtUyaGnD9D4exhR7_8BefBo_w"><img src="https://mermaid.ink/img/pako%3AeNp1VMtu2zAQ_JUFTy1g6wOEwoFbp62ABjk4QS66rMW1TUQkVXKV1Ajy711Sit_RSdTODmdnSL2pxmtSpYr0tyfX0MLgJqCtHcjDhluCxcMd3Pe88v9gCk9bZDBr4K0UvgMHdBEbNt5B4601zKRhjaYlfTOQYM_e9XZFYVg_RgrT2WzedSX8aH0kQAcmxp6GuhSkvEDGFUYSzBbdhgYEREY-xe3FlTDXGqoEe-w0Jh3oxg_L1DYQaaAXchwHkkP7yZ6VE5EMmOsrDMF8qD9tkMWSwguFMhe6QB0GOiCH4vRM5xFUX2HNzlxiLnzJducgjlIY-UbUdG_0AP45r_7cLiYQfCsByWTN84FcsCmbEu47CpgjHYK8mOd08icU5rUPYlfr3UZOjaWiKK50ZSm_aTcBGQt2voctBbqBCl7RySgezBXjv63C7Mur4W2eNaIl-FUtAGNe1-rYeQUhHeTIECXkr9ed26c7bpDFH4R-YuHYFfumIdLHqZz5cR34qX2VSKUCHtLFsiRBnkcqR38MIjkhUiObtpWRfUMxGrcp4A7DczIk5ZpCVRNlKVg0Wq72WxJQK2G1YlApr5rW2Ldcq9q9C7TP1-VWG_ZBlWtsI01UurbLnWtUyaGnD9D4exhR7_8BefBo_w" alt="" /> </a></p>
</details>
<blockquote>
<p>If you are still confused about how it is guaranteed to publish, see DTM's <a href="https://en.dtm.pub/practice/msg.html">2-phase messages doc</a> for more information.</p>
</blockquote>
<h2>How Does the DTM Inbox Work?</h2>
<p>Unlike ABP's default implementation, the DTM inbox gets an event from MQ and handles it at once. After the handlers finish their work, the inbox inserts a barrier within the current DB transaction. Finally, it commits the transaction and returns ACK to MQ.</p>
<p>All the incoming events have a unique MessageId. Events with the same MessageId only are handled once since we cannot insert a barrier with a duplicate gid (MessageId).</p>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNp9UstuwjAQ_JWVz_ADUUtFAakI5YDaYy6beAmW4jW115QK8e91HgWh0vhke2dmZ0d7VpXTpDIV6DMSV7Q0WHu0BUM6YqQhWH7ksObSnfpPjOI42pJ8_86309nsislgSY05kgdkoCOxPJV-9mVkDwiRTeoCOYWANa11L3DltjooWGKgDFYnEyRxSvTeJLlOojYanm_8l0FgIE3vjbwOTHYCOxf5Qbv54XAzLHvqHfe4VLsz9IasUxpXVBpQQ8AjQbVHrin8NdPJL_pqB9U36Xuvg3iIVUWkaTSZNQfybTRteSye0XQe2HrcbuGsNdLNLR45YCXG8bh2T_l3moTOtxnMFxs1UZa8RaPTDp5bXKFSJ0uFytJV0w5jI4Uq-JKg8aBRaKWNOK-yHTaBJqrdx_dvrlQmPtIvaNjjAXX5AUn_9To"><img src="https://mermaid.ink/img/pako%3AeNp9UstuwjAQ_JWVz_ADUUtFAakI5YDaYy6beAmW4jW115QK8e91HgWh0vhke2dmZ0d7VpXTpDIV6DMSV7Q0WHu0BUM6YqQhWH7ksObSnfpPjOI42pJ8_86309nsislgSY05kgdkoCOxPJV-9mVkDwiRTeoCOYWANa11L3DltjooWGKgDFYnEyRxSvTeJLlOojYanm_8l0FgIE3vjbwOTHYCOxf5Qbv54XAzLHvqHfe4VLsz9IasUxpXVBpQQ8AjQbVHrin8NdPJL_pqB9U36Xuvg3iIVUWkaTSZNQfybTRteSye0XQe2HrcbuGsNdLNLR45YCXG8bh2T_l3moTOtxnMFxs1UZa8RaPTDp5bXKFSJ0uFytJV0w5jI4Uq-JKg8aBRaKWNOK-yHTaBJqrdx_dvrlQmPtIvaNjjAXX5AUn_9To" alt="" /> </a></p>
<details>
<summary>See the more detailed sequence diagram</summary>
<p><a href="https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqNlN1uwjAMhV8lyjW8QLUxMWAampiExmVv3MSUaGnCEocxId59aVMoaPysV218_PnETrrjwkrkGff4FdAIHCsoHVS5YfEhRRrZeDFjU1PYbVqEQNaEqkCXvmfz_mBw1GRsjFpt0DEwDDdo6KFwg29FKwYsGBWrsBl6DyVOZQIcc2sOEBTgMWOTrfIUcwpwTkVcgyiVZI9d_lPrSFOS-4M6rTfwFtg_N_ncUpc2GHmiPlqJ8tk8Y8PRW4riQXYHaCydQk_3Nlyvu-7QClN7ki7Gznb_CkbG1h9VsZuSedggEyswJfq_Zhr8KEUbqezQ515buA9CIEq8OYap8ejqOdThW7PoRtGmLEFplHcn8RJlCSjDWisBhDX60kxOfTmrdfQjPpsekQPjQZCy5m7BRadtKCgbzo1D8P7vU3Ch_Zftj2xVKbrs_Ro7pVydWndieY9X6CpQMl7sXa3LeaxUYc6z-CpxCUFTznOzj9KwlrHnE6nIOp4tQXvs8fqSf_wYwTNyAQ-i9ufQqva_CSldXw"><img src="https://mermaid.ink/img/pako%3AeNqNlN1uwjAMhV8lyjW8QLUxMWAampiExmVv3MSUaGnCEocxId59aVMoaPysV218_PnETrrjwkrkGff4FdAIHCsoHVS5YfEhRRrZeDFjU1PYbVqEQNaEqkCXvmfz_mBw1GRsjFpt0DEwDDdo6KFwg29FKwYsGBWrsBl6DyVOZQIcc2sOEBTgMWOTrfIUcwpwTkVcgyiVZI9d_lPrSFOS-4M6rTfwFtg_N_ncUpc2GHmiPlqJ8tk8Y8PRW4riQXYHaCydQk_3Nlyvu-7QClN7ki7Gznb_CkbG1h9VsZuSedggEyswJfq_Zhr8KEUbqezQ515buA9CIEq8OYap8ejqOdThW7PoRtGmLEFplHcn8RJlCSjDWisBhDX60kxOfTmrdfQjPpsekQPjQZCy5m7BRadtKCgbzo1D8P7vU3Ch_Zftj2xVKbrs_Ro7pVydWndieY9X6CpQMl7sXa3LeaxUYc6z-CpxCUFTznOzj9KwlrHnE6nIOp4tQXvs8fqSf_wYwTNyAQ-i9ufQqva_CSldXw" alt="" /> </a></p>
</details>
<blockquote>
<p>As you may have noticed, the inbox has nothing to do with the DTM Server.🤭</p>
</blockquote>
<h2>Installation and Usage</h2>
<p>Please see https://github.com/EasyAbp/Abp.EventBus.Boxes.Dtm/tree/main#installation.</p>
<h2>Postscript</h2>
<p>This DTM event-boxes implementation is not a perfect solution. The main cost is that you need to care about the availability of the DTM Server. But if you run into the multi-tenant multi-database scene, it should be the best choice for now.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/6c04fe65-831a-118e-01c6-3a04369c3627" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/6c04fe65-831a-118e-01c6-3a04369c3627" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/install-modules-with-one-click-via-abphelper-gui-8uuqwwa9</guid>
      <link>https://abp.io/community/posts/install-modules-with-one-click-via-abphelper-gui-8uuqwwa9</link>
      <a10:author>
        <a10:name>gdlcf88</a10:name>
        <a10:uri>https://abp.io/community/members/gdlcf88</a10:uri>
      </a10:author>
      <category>module-installation</category>
      <category>AbpHelper</category>
      <title>Install modules with one click via AbpHelper GUI</title>
      <description>In this article, we will use the AbpHelper GUI to install the EasyAbp.Abp.SettingUi module into your ABP application.</description>
      <pubDate>Tue, 06 Apr 2021 13:12:54 Z</pubDate>
      <a10:updated>2026-10-26T21:59:17Z</a10:updated>
      <content:encoded><![CDATA[<h1>Install Modules With One Click via AbpHelper GUI</h1>
<h2>Introduction</h2>
<p>In this article, we will use the AbpHelper GUI to install the <a href="https://github.com/EasyAbp/Abp.SettingUi">EasyAbp.Abp.SettingUi</a> module into your ABP application.</p>
<h2>Pre-Requirements</h2>
<p>The following tools should be installed on your development machine:</p>
<ul>
<li><a href="https://www.microsoft.com/net/download/dotnet-core/">.NET Core 5.0+</a></li>
<li><a href="https://code.visualstudio.com/">VS Code</a> or another IDE</li>
<li><a href="https://github.com/EasyAbp/AbpHelper.GUI/releases">AbpHelper GUI 1.0+</a></li>
</ul>
<h2>Creating a New Solution</h2>
<p>Open the AbpHelper GUI, switch to the &quot;ABP CLI&quot; tab, and create a new app:</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Create-An-App.gif" alt="CreateAnApp" /></p>
<h2>Install the SettingUi Modules</h2>
<p>Open the new app solution in the AbpHelper GUI and install the modules:</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Install-Modules.gif" alt="InstallModules" /></p>
<p>Then we will find that the <code>DependsOn</code>s have been added:</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Find-Depends-On.png" alt="FindDependsOn" /></p>
<h2>Run the App</h2>
<blockquote>
<p>See https://docs.abp.io/en/abp/latest/Getting-Started-Running-Solution to learn more about how to run an ABP solution.</p>
</blockquote>
<p>Run the commands:</p>
<pre><code class="language-bash">cd C:\Temp\TryAbpHelper\src\TryAbpHelper.DbMigrator
dotnet run
cd ..\TryAbpHelper.Web
dotnet run
</code></pre>
<p>Log in and try the Setting UI:</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Install-Modules-With-One-Click-Via-AbpHelper-GUI/en/images/Use-Setting-Ui.png" alt="UseSettingUi" /></p>
<blockquote>
<p>Use the default username <code>admin</code> and password <code>1q2w3E*</code> to log in to the app.</p>
</blockquote>
<h2>Conclusion</h2>
<p>AbpHelper is helpful for us to manage the application's modules.</p>
<p>We can use it to install not only Volosoft's modules but also the community modules.</p>
<p>The module management feature of the AbpHelper depends on EasyAbp's <a href="https://github.com/EasyAbp/ModuleLibrary">module library</a>, welcome to register your developed modules through PR.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/381f4b73-2f9a-3169-e949-39fbb97d57c1" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/381f4b73-2f9a-3169-e949-39fbb97d57c1" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/reuse-abp-vnext-modules-to-quickly-implement-application-features-tdtmwd9w</guid>
      <link>https://abp.io/community/posts/reuse-abp-vnext-modules-to-quickly-implement-application-features-tdtmwd9w</link>
      <a10:author>
        <a10:name>gdlcf88</a10:name>
        <a10:uri>https://abp.io/community/members/gdlcf88</a10:uri>
      </a10:author>
      <category>reusing-modules</category>
      <title>Reuse ABP vNext Modules to Quickly Implement Application Features</title>
      <description>Using the ABP framework, we can implement some features we want without coding every time.</description>
      <pubDate>Sun, 30 Aug 2020 20:22:27 Z</pubDate>
      <a10:updated>2026-10-26T19:59:39Z</a10:updated>
      <content:encoded><![CDATA[<h1>Reuse ABP vNext Modules to Quickly Implement Application Features</h1>
<p>In the <a href="https://github.com/gdlcf88/Articles/blob/master/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/article.md">previous article</a>, we have completed the basic features of the address book application.</p>
<p>In this article, we will install the <strong>PrivateMessaging</strong> module to our address book application, so that users can send private messages to each other and get new message notifications on the website.</p>
<p>In the design of the private messaging module, users can send private messages to themselves, but we will override the design to prohibit users from sending private messages to themselves at the end of this article.</p>
<h2>What Is a Module in ABP Framework?</h2>
<p>Using the ABP framework, you can make some modules in advance, such as external login implementation, private messaging, file upload, blog, forum, and other modules, and package them for later use. When developing a specific application, you can easily install modules into your application, saving a lot of repetitive work.</p>
<p>In addition to making your own modules, you can also install modules maintained by the open-source community on NuGet. Of course, the community is also waiting for your contribution.</p>
<h2>Install the Private Messaging Module</h2>
<p>The module (EasyAbp.PrivateMessaging) we use is developed and maintained by the <a href="https://easyabp.io">EasyAbp</a> organization, and you can even use it for free in commercial applications.</p>
<p>Now we will start to install the module step by step.</p>
<h3>Step 1: Install the Module's NuGet Packages</h3>
<ol>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.Application</code> to the <code>AddressBook.Application</code> project.</li>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.Application.Contracts</code> to the <code>AddressBook.Application.Contracts</code> project.</li>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.Domain</code> to the <code>AddressBook.Domain</code> project.</li>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.Domain.Shared</code> to the <code>AddressBook.Domain.Shared</code> project.</li>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.EntityFrameworkCore</code> to the <code>AddressBook.EntityFrameworkCore</code> project.</li>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.HttpApi</code> into the <code>AddressBook.HttpApi</code> project.</li>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.HttpApi.Client</code> into the <code>AddressBook.HttpApi.Client</code> project.</li>
<li>Install the NuGet package <code>EasyAbp.PrivateMessaging.Web</code> into the <code>AddressBook.Web</code> project (you can skip this step if you do not need the UI).</li>
</ol>
<h3>Step 2: Add Module Dependencies and Configurations</h3>
<ol>
<li><p>Add the dependency of the module to the module class of the above projects, for example, we add the attribute <code>[DependsOn(PrivateMessagingApplicationModule)]</code> to the class in the <strong>AddressBookApplicationModule.cs</strong> of the AddressBook.Application project, and so on.</p>
</li>
<li><p>Find the <strong>OnModelCreating</strong> method in <strong>AddressBookMigrationsDbContext.cs</strong> of the AddressBook.EntityFrameworkCore.DbMigrations project and add the code <code>builder.ConfigurePrivateMessaging();</code> to it to configure the EF Core migration of the module.</p>
</li>
</ol>
<h3>Step 3: Create EF Core Migration and Update the Database</h3>
<ol>
<li><p>Run the command <code>dotnet ef migrations add Installed_Pm_Module -s ../AddressBook.DbMigrator</code> in the directory of AddressBook.EntityFrameworkCore.DbMigrations project.</p>
</li>
<li><p>Run the AddressBook.DbMigrator project, it will automatically update the database.</p>
</li>
</ol>
<p>If you want to learn more about this step, you can read the <a href="https://docs.abp.io/en/abp/latest/Tutorials/Part-1#add-database-migration">ABP official documentation</a>.</p>
<h3>Step 4: Run the Application</h3>
<p>Now run the AddressBook.Web project, we can see that the private messaging module has been installed successfully.</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/en/images/HomePage.png" alt="HomePage" /></p>
<h2>Improvement: Prohibit Users From Sending Private Messages to Themselves</h2>
<p>The ABP framework allows us to override the code in the module. Please create a new file <code>MyPrivateMessageAppService.cs</code> in the AddressBook.Application project:</p>
<pre><code class="language-csharp">[Dependency(ReplaceServices = true)]
public class MyPrivateMessageAppService : PrivateMessageAppService
{
    // ctor

    public override Task&lt;PrivateMessageDto&gt; CreateAsync(CreateUpdatePrivateMessageDto input)
    {
        if (input.ToUserName == CurrentUser.UserName)
        {
            throw new UserFriendlyException(&quot;Don't send messages to yourself&quot;);
        }
        
        return base.CreateAsync(input);
    }
}
</code></pre>
<p>If a user sends a message to himself, he will see the error message:</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Reuse-ABP-VNext-Modules-To-Quickly-Implement-Application-Features/en/images/SendToSelf.png" alt="SendToSelf" /></p>
<p>Thanks to the extensible modular design of the ABP framework, static files (such as JS and CSS files) and pages can be overridden. Please read the ABP official document for more information: <a href="https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide">Customizing the Existing Modules</a></p>
<h2>Postscript</h2>
<p>If you want to learn more about the private messaging module, please read its <a href="https://easyabp.io/modules/PrivateMessaging">documentation</a>.</p>
<p>By the way, the EasyAbp organization has many powerful and useful modules, such as EShop, PaymentService, GiftCardManagement, which can improve your development efficiency and enhance the reliability of applications. If you are interested, please read <a href="https://github.com/EasyAbp/EasyAbpGuide">EasyAbp Guide</a> to learn more.</p>
<p>I look forward to the launch of ABP's official module market (market.abp.io) in the future, providing community modules list, search, and automatic installation features, making it easier for us to install modules.</p>
<h2>Next</h2>
<p>In the next article, we will upgrade the address book application to a multi-tenant SaaS application with a minor change.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-abp-vnext-to-develop-an-address-book-application-in-5-minutes-kro0mfrt</guid>
      <link>https://abp.io/community/posts/using-abp-vnext-to-develop-an-address-book-application-in-5-minutes-kro0mfrt</link>
      <a10:author>
        <a10:name>gdlcf88</a10:name>
        <a10:uri>https://abp.io/community/members/gdlcf88</a10:uri>
      </a10:author>
      <category>application-development</category>
      <category>crud-page-generator</category>
      <category>EasyAbp</category>
      <title>Using ABP vNext to Develop an Address Book Application in 5 Minutes</title>
      <description>A step by step guide on how to use ABP vNext framework to develop an address book application in 5 minutes.</description>
      <pubDate>Thu, 20 Aug 2020 21:18:51 Z</pubDate>
      <a10:updated>2026-10-26T21:23:37Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using ABP vNext to Develop an Address Book Application in 5 Minutes</h1>
<p>ABP vNext framework (hereinafter called &quot;ABP&quot;) is an artwork-level application development framework designed by Volosoft. It is based on Domain-Driven-Design (DDD) and innovatively adopts modular development design. ABP is undoubtedly the most advanced and elegant existence in ASP.NET Core development frameworks.</p>
<p>I believe that with the excellent modular development design and smooth development experience, ABP's position in ASP.NET Core is expected to reach the position of Spring in Java.</p>
<h2>Module Development and Application Development</h2>
<p>Using the ABP framework, you can make some modules in advance, such as external login implementation, private messaging, file upload, blog, forum, and other modules, and package them for later use. When developing a specific application, you can easily install modules into your application, saving a lot of repetitive work.</p>
<p>In addition to making your own modules, you can also install modules maintained by the open-source community on NuGet. Of course, the community is also waiting for your contribution.</p>
<h2>Start to Develop the Address Book Application</h2>
<p>Today we will not discuss module development, but start with the simplest application development. I will follow the ABP best practices and show you how to develop an address book application using Abp vNext framework in 5 minutes.</p>
<h3>Step 1: Use ABP CLI to Generate the Application Solution</h3>
<ol>
<li><p>Use the command line to install ABP CLI: <code>dotnet tool install -g Volo.Abp.Cli</code>.</p>
</li>
<li><p>Use the command line to generate a solution: <code>abp new AddressBook</code> (the solution will be generated in the current directory).</p>
</li>
</ol>
<h3>Step 2: Create a &quot;Contact&quot; Entity</h3>
<p>For ABP, the contact entity should be an AggregateRoot. For details, please refer to the introduction of Domain-Driven-Design (DDD) in the <a href="https://docs.abp.io/en/abp/latest/Domain-Driven-Design">ABP official document</a>.</p>
<ol>
<li><p>Create a new directory: <code>aspnet-core/src/AddressBook.Domain/Contacts</code>.</p>
</li>
<li><p>Manually create the <code>Contact.cs</code> file in the above directory.</p>
<pre><code class="language-csharp">public class Contact : AggregateRoot&lt;Guid&gt;
{
    public virtual string Name { get; protected set; }

    public virtual string PhoneNumber { get; protected set; }

    public virtual string Address { get; protected set; }

    public virtual byte? Age { get; protected set; }

    public virtual DateTime? Birthday { get; protected set; }

    // The constructors will be generated later, you don’t need to add them manually here.
}
</code></pre>
</li>
<li><p>Run the <code>AddressBook.DbMigrator</code> project, which is to establish the basic structure and data in the database for our application.</p>
</li>
</ol>
<h3>Step 3: Generate the Remaining Code</h3>
<p>This article uses EasyAbp's open-source <a href="https://easyabp.io/abphelper/AbpHelper.GUI">AbpHelper GUI</a> to generate code. If you are an ABP commercial user, you can also choose <a href="https://commercial.abp.io/tools/suite">ABP Suite</a>.</p>
<ol>
<li><p>Download AbpHelper GUI: https://github.com/EasyAbp/AbpHelper.GUI/releases</p>
</li>
<li><p>Use the <strong>CRUD Code Generator</strong> to generate all codes related to the Contact entity.</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/CrudCodeGenerator.png" alt="CrudCodeGenerator" /></p>
</li>
</ol>
<p>If you are using the AbpHelper GUI for the first time, please install the AbpHelper CLI through the <code>Install or update AbpHelper CLI</code> on the left navigation menu. You can also directly use <a href="https://easyabp.io/abphelper/AbpHelper.CLI">AbpHelper CLI</a> to complete the above work.</p>
<h3>Step 4: Run the Application</h3>
<ol>
<li><p>Run the AddressBook.Web project.</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/HomePage.png" alt="HomePage" /></p>
</li>
<li><p>Log in and explore your application (the default password of the admin user is <code>1q2w3E*</code>).</p>
<p><img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/CreateContact.png" alt="CreateContact" />
<img src="https://raw.githubusercontent.com/gdlcf88/Articles/master/Using-ABP-VNext-To-Develop-An-Address-Book-Application-In-5-Minutes/en/images/ContactList.png" alt="ContactList" /></p>
<p>You must have noticed that the form has been automatically generated by the <a href="https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Dynamic-Forms">abp-dynamic-form</a> tag helper. Moreover, you only need to modify the localization file to customize the words, but I will not demonstrate it here.</p>
</li>
<li><p>Contact entity's RESTful API has also been automatically generated, if you need them, visit the route <code>/swagger</code>.</p>
</li>
</ol>
<h2>Postscript</h2>
<p>Our address book application naturally includes identity management, multi-tenant support, if you plan to learn the ABP framework systematically, please read the <a href="https://docs.abp.io">official document</a>.</p>
<p>The AbpHelper used in this article is a tool produced by the EasyAbp organization, which can significantly improve your development efficiency and is completely free. In addition, EasyAbp also provides a lot of useful modules, you can read <a href="https://github.com/EasyAbp/EasyAbpGuide">EasyAbp Guide</a> to learn more.</p>
<h2>Next</h2>
<p>In the next article, I will introduce how to install the <strong>PrivateMessaging</strong> module to our address book application. This module is also developed and maintained by the EasyAbp organization, and you can even use it for free in commercial applications.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
  </channel>
</rss>