<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ABP.IO</title>
    <description>The latest articles on DEV Community by ABP.IO (@abp_io).</description>
    <link>https://dev.to/abp_io</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3723489%2Ffed2d127-60d2-4db0-a199-b848ed38b079.png</url>
      <title>DEV Community: ABP.IO</title>
      <link>https://dev.to/abp_io</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abp_io"/>
    <language>en</language>
    <item>
      <title>ABP.IO Platform 10.6 Final Has Been Released!</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Mon, 27 Jul 2026 11:30:06 +0000</pubDate>
      <link>https://dev.to/abp_io/abpio-platform-106-final-has-been-released-59b3</link>
      <guid>https://dev.to/abp_io/abpio-platform-106-final-has-been-released-59b3</guid>
      <description>&lt;h1&gt;
  
  
  ABP.IO Platform 10.6 Final Has Been Released!
&lt;/h1&gt;

&lt;p&gt;We are glad to announce that &lt;a href="https://abp.io/" rel="noopener noreferrer"&gt;ABP&lt;/a&gt; 10.6 stable version has been released.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New With Version 10.6?
&lt;/h2&gt;

&lt;p&gt;All the new features were explained in detail in the &lt;a href="https://abp.io/community/announcements/abp-platform-10.6-rc-has-been-released-reoq6kzw" rel="noopener noreferrer"&gt;10.6 RC Announcement Post&lt;/a&gt;, so there is no need to review them all again. You can check it out for more details.&lt;/p&gt;

&lt;p&gt;Here are some of the highlights of this version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Background jobs now support dedicated workers, parallel execution, and successful job retention.&lt;/li&gt;
&lt;li&gt;API definition and generated proxies have better support for response content types, remote streams, and multipart uploads.&lt;/li&gt;
&lt;li&gt;Angular UI packages and templates have been upgraded to Angular 22.&lt;/li&gt;
&lt;li&gt;Antiforgery and OpenIddict flows include important security and reliability improvements.&lt;/li&gt;
&lt;li&gt;ABP Commercial adds OpenIddict access-token generation from the UI and React CRUD page generation support in ABP Suite.&lt;/li&gt;
&lt;li&gt;AI Management indexing is more resilient for large or memory-constrained workloads.&lt;/li&gt;
&lt;li&gt;The final release also includes dependency updates and stability fixes collected during the RC period.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started with 10.6
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How to Upgrade an Existing Solution
&lt;/h3&gt;

&lt;p&gt;You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained:&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading via ABP Studio
&lt;/h3&gt;

&lt;p&gt;If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See &lt;a href="https://abp.io/docs/latest/studio/installation#upgrading" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; for more info.&lt;/p&gt;

&lt;p&gt;After upgrading the ABP Studio, then you can open your solution in the application, and simply click the &lt;strong&gt;Upgrade ABP Packages&lt;/strong&gt; action button to instantly upgrade your solution:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdocs%2Fen%2FBlog-Posts%2F2026-07-27%2520v10_6_Release_Stable%2Fupgrade-abp-packages.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdocs%2Fen%2FBlog-Posts%2F2026-07-27%2520v10_6_Release_Stable%2Fupgrade-abp-packages.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading via ABP CLI
&lt;/h3&gt;

&lt;p&gt;Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version.&lt;/p&gt;

&lt;p&gt;If you haven't installed it yet, you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; Volo.Abp.Studio.Cli

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or to update the existing CLI, you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool update &lt;span class="nt"&gt;-g&lt;/span&gt; Volo.Abp.Studio.Cli

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing/updating the ABP CLI, you can use the &lt;a href="https://abp.io/docs/latest/CLI#update" rel="noopener noreferrer"&gt;&lt;code&gt;update&lt;/code&gt; command&lt;/a&gt; to update all the ABP related NuGet and NPM packages in your solution as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;abp update

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run this command in the root folder of your solution to update all ABP related packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Guides
&lt;/h2&gt;

&lt;p&gt;This version includes explicitly marked migration-impacting changes for specific customization scenarios, especially custom background job stores/workers and custom AI Management document chunk repositories. The new background job runtime features are opt-in and existing applications keep the current behavior unless they enable them explicitly.&lt;/p&gt;

&lt;p&gt;Please read the migration guide carefully, if you are upgrading from v10.5 or earlier versions: &lt;a href="https://abp.io/docs/10.6/release-info/migration-guides/abp-10-6" rel="noopener noreferrer"&gt;ABP Version 10.6 Migration Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you use the Angular UI, also check the dedicated &lt;a href="https://abp.io/docs/10.6/release-info/migration-guides/abp-10-6-angular-22" rel="noopener noreferrer"&gt;Angular 22 and ABP 10.6 Upgrade Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Community News
&lt;/h2&gt;

&lt;h3&gt;
  
  
  New ABP Community Articles
&lt;/h3&gt;

&lt;p&gt;As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/announcements/introducing-abp-lowcode-build-real-abp-apps-in-minutes-647ymozi" rel="noopener noreferrer"&gt;Introducing ABP Low-Code: Build Real ABP Apps in Minutes&lt;/a&gt; by &lt;a href="https://abp.io/community/members/salih" rel="noopener noreferrer"&gt;Salih Ozkara&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/building-a-vendor-onboarding-workflow-with-abp-lowcode-1wx0ckzc" rel="noopener noreferrer"&gt;Building a Vendor Onboarding Workflow with ABP Low-Code&lt;/a&gt; by &lt;a href="https://abp.io/community/members/salih" rel="noopener noreferrer"&gt;Salih Ozkara&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/event-recap-wearedevelopers-world-congress-2026-v59t8vfn" rel="noopener noreferrer"&gt;Event Recap - WeAreDevelopers World Congress 2026&lt;/a&gt; by &lt;a href="https://abp.io/community/members/iremdemirci" rel="noopener noreferrer"&gt;Irem Demirci&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/empathy-in-the-workplace-for-software-companies-wsjjw9we" rel="noopener noreferrer"&gt;Empathy in the Workplace for Software Companies&lt;/a&gt; by &lt;a href="https://abp.io/community/members/alper" rel="noopener noreferrer"&gt;Alper Ebicoglu&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the ABP Community for all the content they have published. You can also &lt;a href="https://abp.io/community/posts/create" rel="noopener noreferrer"&gt;post your ABP related (text or video) content&lt;/a&gt; to the ABP Community.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the Next Version
&lt;/h2&gt;

&lt;p&gt;The next feature version will be 10.7. You can follow the &lt;a href="https://github.com/abpframework/abp/milestones" rel="noopener noreferrer"&gt;release planning here&lt;/a&gt;. Please &lt;a href="https://github.com/abpframework/abp/issues/new" rel="noopener noreferrer"&gt;submit an issue&lt;/a&gt; if you have any problems with this version.&lt;/p&gt;

</description>
      <category>update</category>
      <category>version</category>
      <category>newversion</category>
      <category>release</category>
    </item>
    <item>
      <title>Event Recap - WeAreDevelopers World Congress 2026</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Fri, 17 Jul 2026 12:26:38 +0000</pubDate>
      <link>https://dev.to/abp_io/event-recap-wearedevelopers-world-congress-2026-3e58</link>
      <guid>https://dev.to/abp_io/event-recap-wearedevelopers-world-congress-2026-3e58</guid>
      <description>&lt;p&gt;WeAreDevelopers World Congress 2026 has come to an end, and we'd like to thank everyone who stopped by the ABP booth in Berlin!&lt;/p&gt;

&lt;p&gt;We had the opportunity to meet developers, architects, engineering leaders, and technology enthusiasts from around the world. It was a pleasure connecting with so many members of the developer community, hearing about the projects you're building, and discussing the challenges and opportunities shaping modern software development.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS4zHy%252BR4dF3%252BYf%252FtQCpm6USG%252BPfkEFUE0giWmqZzA%252FqHbedsADTkW5jxPZVxyJjD1ZxYkCFVpgySB8KSYBTBAdDql%252FEFmCA8GZ7%252F1p0W2Y5V2ob%252F5I77rotPvY3K2lPaKkH4WPQnNbou02%252BJVW6wPxN" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS4zHy%252BR4dF3%252BYf%252FtQCpm6USG%252BPfkEFUE0giWmqZzA%252FqHbedsADTkW5jxPZVxyJjD1ZxYkCFVpgySB8KSYBTBAdDql%252FEFmCA8GZ7%252F1p0W2Y5V2ob%252F5I77rotPvY3K2lPaKkH4WPQnNbou02%252BJVW6wPxN" alt="ABP team at WeAreDevelopers World Congress 2026.1" width="600" height="800"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS69OYPeQbcVnawHZSL4ohAgZW3zQ%252BLnKM2ZJcj3mVMXlqXqyI7JQNCfbjJODmfRzdEaEyjXI2Afgx4q9gnOTM%252F8jfISltQ%252FhJIlLt8cyKzG4t%252FLWhjP4K0olwpw2AxU1FHx30pKKKb03NCGPF%252BXoq67" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS69OYPeQbcVnawHZSL4ohAgZW3zQ%252BLnKM2ZJcj3mVMXlqXqyI7JQNCfbjJODmfRzdEaEyjXI2Afgx4q9gnOTM%252F8jfISltQ%252FhJIlLt8cyKzG4t%252FLWhjP4K0olwpw2AxU1FHx30pKKKb03NCGPF%252BXoq67" alt="ABP team at WeAreDevelopers World Congress 2026.2" width="720" height="886"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Great Conversations and Product Demos&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Throughout the event, our team showcased the latest developments across the ABP ecosystem, including ABP Framework, ABP Studio, and our AI-powered development capabilities.&lt;/p&gt;

&lt;p&gt;We had countless conversations about modular application development, clean architecture, microservices, AI-assisted development, and how teams can build enterprise applications faster while maintaining long-term quality and maintainability.&lt;/p&gt;

&lt;p&gt;Thank you to everyone who shared feedback, asked questions, and explored how ABP can support your development journey.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS5Lh7%252Fs%252BJm9dBfa89UIM8DtdlERFkfiUAx2DzqxE0v8z5hf%252BDWpFiiYVOjWq5NTdFxaqqzM079kwIYLEGcL7LljvWtjm5EZtbFYBlqXt8P4stnEmMSclaZocHZp4OgS%252BfN0caba4RQpLQgjIESBICFU" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS5Lh7%252Fs%252BJm9dBfa89UIM8DtdlERFkfiUAx2DzqxE0v8z5hf%252BDWpFiiYVOjWq5NTdFxaqqzM079kwIYLEGcL7LljvWtjm5EZtbFYBlqXt8P4stnEmMSclaZocHZp4OgS%252BfN0caba4RQpLQgjIESBICFU" alt="ABP team at WeAreDevelopers World Congress 2026.3" width="720" height="960"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS4MxSzQqrl5ZNL%252BYgzHUxeTjm0uv%252B68PDOkP8P%252BwVJeSiVYFwsyumyB2tI85Ik5nJMbWt5zzaOHUUhYKKSHFZMRg4uAcAVSyT9hIdF3G3DeN6lfzCraFeA7SOvbgPTSiho6VxgzIndricFCd6bfMleD" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS4MxSzQqrl5ZNL%252BYgzHUxeTjm0uv%252B68PDOkP8P%252BwVJeSiVYFwsyumyB2tI85Ik5nJMbWt5zzaOHUUhYKKSHFZMRg4uAcAVSyT9hIdF3G3DeN6lfzCraFeA7SOvbgPTSiho6VxgzIndricFCd6bfMleD" alt="ABP team at WeAreDevelopers World Congress 2026.4" width="720" height="932"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Sharing Our Experience on Stage&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In addition to connecting with attendees at our booth, we were proud to see our Co-founder, &lt;strong&gt;Halil İbrahim Kalkan&lt;/strong&gt; , speak at WeAreDevelopers World Congress 2026.&lt;/p&gt;

&lt;p&gt;His session, &lt;strong&gt;"Dynamic Entities in .NET: Building Low-Code Systems on Top of Entity Framework Core"&lt;/strong&gt; explored how developers can build flexible, dynamic applications while leveraging the power of Entity Framework Core and the .NET ecosystem.&lt;/p&gt;

&lt;p&gt;It was a great opportunity to share the engineering practices and ideas behind ABP with the wider developer community. Thank you to everyone who attended the session and joined the discussion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS6o2J%252FNrkCu8kKnfIGPt4h9zpeS61T2EHZ76ICCnBJlBMiQbXHWjHfPa7ZrWWmSA8om%252F5%252FPGUtcVR9yeGXj7jckumTHqSk1hTDQLDrs8pyYs4K1hz3FOpDmsNo8DBxaf8BDBtYY8RnfSMjUuhTiTMVn" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS6o2J%252FNrkCu8kKnfIGPt4h9zpeS61T2EHZ76ICCnBJlBMiQbXHWjHfPa7ZrWWmSA8om%252F5%252FPGUtcVR9yeGXj7jckumTHqSk1hTDQLDrs8pyYs4K1hz3FOpDmsNo8DBxaf8BDBtYY8RnfSMjUuhTiTMVn" alt="ABP team at WeAreDevelopers World Congress 2026.5" width="1600" height="1709"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;More Than Just a Conference&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;WeAreDevelopers World Congress wasn't only about technical sessions. The event also featured interactive experiences, including a lively arcade gaming area, creating plenty of opportunities for attendees to relax, connect, and enjoy the conference between talks.&lt;/p&gt;

&lt;p&gt;This is the approach I'd recommend. It keeps the ABP story focused while giving you a natural place to include photos or videos of the arcade area.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/K2WzoMfO76k" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS7Zcq8bydK7%252BRRT0moIRkCgYSX6gkXffggpSCpS7%252B%252BZQuC42apGawV4nYr%252FKDuY7UHtlw7AbIFV5cIIevx2UqQ1IPG%252Bp4IWeYKH0isSwi0Jk36jBkH21UQiNlUFnif38Cd8copX22EX1eGV0DGpwjId" alt="Watch the Gaming Area video on YouTube" width="760" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS5E2azupUQeKZCFpz8uOxJfDT%252BoD%252B6sKWEDVrQZLKPgwWauQman6CjAA35QPNHDPaR89CJNnOaqAr%252BDVZNUF5LLpqtkkbhDxL8cs19hRvOozR%252B%252FEMDMLQv05ZicCEswkaH68pIi8Htau91x2j%252B%252FZUNY" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS5E2azupUQeKZCFpz8uOxJfDT%252BoD%252B6sKWEDVrQZLKPgwWauQman6CjAA35QPNHDPaR89CJNnOaqAr%252BDVZNUF5LLpqtkkbhDxL8cs19hRvOozR%252B%252FEMDMLQv05ZicCEswkaH68pIi8Htau91x2j%252B%252FZUNY" alt="ABP team at WeAreDevelopers World Congress 2026.7" width="719" height="941"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Meeting The Developer Community&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the best parts of WeAreDevelopers World Congress is bringing together developers, architects, engineering leaders, and technology experts from around the world. The conference featured inspiring keynotes and technical sessions covering AI, software architecture, cloud, developer productivity, and many other topics that are shaping the future of software development.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS4sS8dtDS3uz8BQ08GHrJhfYezTNm7iIPkGxmwSzAdqu3I5Xm83qEuWbyZyrkz%252FexjK%252BqWZ%252BwC2eUpOcjNJPk7a4RM97Es6Yy1SVC1k08fcpqmbF22enrV9%252FCRwLRaA0693i9TAlo1NBOcQAGDDLgfx" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS4sS8dtDS3uz8BQ08GHrJhfYezTNm7iIPkGxmwSzAdqu3I5Xm83qEuWbyZyrkz%252FexjK%252BqWZ%252BwC2eUpOcjNJPk7a4RM97Es6Yy1SVC1k08fcpqmbF22enrV9%252FCRwLRaA0693i9TAlo1NBOcQAGDDLgfx" alt="ABP team at WeAreDevelopers World Congress 2026.8" width="720" height="960"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS6R8ZtcNmxyjBlV%252Fqiov6VfF0%252Fz9bl4VCdmSDfkDK%252Bmx8dmbrxOiiw8bIfymVWjEbIw9wzujp0R90K%252FgC7s3n7UaPSROiKcLwOBms3JB8To9G3wywNHRC3uKOLiFdszXLwNyZmqRemsUm5%252FTXTfAzy5" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS6R8ZtcNmxyjBlV%252Fqiov6VfF0%252Fz9bl4VCdmSDfkDK%252Bmx8dmbrxOiiw8bIfymVWjEbIw9wzujp0R90K%252FgC7s3n7UaPSROiKcLwOBms3JB8To9G3wywNHRC3uKOLiFdszXLwNyZmqRemsUm5%252FTXTfAzy5" alt="ABP team at WeAreDevelopers World Congress 2026.10" width="720" height="939"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS7FG5qcTtOkfnSg7C9sq9zTrhQ%252FlW5Qe3YPb1WUnp6Yg%252BSXIPdg8B7L%252BiAtW7wqVK6%252FLU3EFpxIlEA1zYa23xwlggpo6V8%252BWFppT88NGxQ5Kn6vBV8S2vX1rytjS7RUjBHVXxJR8X5Vk49WYv5kCER9" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS7FG5qcTtOkfnSg7C9sq9zTrhQ%252FlW5Qe3YPb1WUnp6Yg%252BSXIPdg8B7L%252BiAtW7wqVK6%252FLU3EFpxIlEA1zYa23xwlggpo6V8%252BWFppT88NGxQ5Kn6vBV8S2vX1rytjS7RUjBHVXxJR8X5Vk49WYv5kCER9" alt="ABP team at WeAreDevelopers World Congress 2026.11" width="720" height="803"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Until Next Time&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A big thank you to the WeAreDevelopers team for organizing another fantastic event and to everyone who visited us at Hall A, Booth A-41.&lt;/p&gt;

&lt;p&gt;If we didn't get the chance to meet in Berlin, you can always explore ABP online, join our community, or reach out to us with your questions and feedback.&lt;/p&gt;

&lt;p&gt;We appreciate everyone who made WeAreDevelopers World Congress 2026 such a memorable experience, and we look forward to seeing you again at future events!&lt;/p&gt;

&lt;p&gt;​ &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS60ifxXi0WKAOMXk3uWgymwuMfSRR441sbaecJaHrzvlKokwVAQHsFtcr%252ByT9WJlUME5VTvE3iny0Rx9tVmSqlKRfKrAgvsEsyl1ACFjqjUvzqlXvswIpLWXNxoqIm%252BGylB6JFSA1cASNXNIs21tq9P" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS60ifxXi0WKAOMXk3uWgymwuMfSRR441sbaecJaHrzvlKokwVAQHsFtcr%252ByT9WJlUME5VTvE3iny0Rx9tVmSqlKRfKrAgvsEsyl1ACFjqjUvzqlXvswIpLWXNxoqIm%252BGylB6JFSA1cASNXNIs21tq9P" alt="ABP team at WeAreDevelopers World Congress 2026.12" width="719" height="808"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS734IO%252BudVyD2%252FGzg7qP1XkRTg7ZT5zZ2pLv5%252FoDqvo8sB%252FGEfkshjTmD0YBjDCeWKrpTHI6pK1TEIg%252FQoMX6SGoudWzymr2pQMFpzF2ATCE%252FdB3XRNSITLSaS6L40V8UfryzP8DBjDwlNizxPI5tGw" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fabp.io%2Fapi%2Ffile-management%2Ffile-descriptor%2Fshare%3FshareToken%3DCfDJ8BFJWl%252BAUE9Cj%252FpQ216BBS734IO%252BudVyD2%252FGzg7qP1XkRTg7ZT5zZ2pLv5%252FoDqvo8sB%252FGEfkshjTmD0YBjDCeWKrpTHI6pK1TEIg%252FQoMX6SGoudWzymr2pQMFpzF2ATCE%252FdB3XRNSITLSaS6L40V8UfryzP8DBjDwlNizxPI5tGw" alt="ABP team at WeAreDevelopers World Congress 2026.13" width="600" height="800"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;​&lt;/p&gt;

</description>
      <category>conference</category>
      <category>conferences</category>
      <category>events</category>
    </item>
    <item>
      <title>Building a Vendor Onboarding Workflow with ABP Low-Code</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:51:56 +0000</pubDate>
      <link>https://dev.to/abp_io/building-a-vendor-onboarding-workflow-with-abp-low-code-1604</link>
      <guid>https://dev.to/abp_io/building-a-vendor-onboarding-workflow-with-abp-low-code-1604</guid>
      <description>&lt;h1&gt;
  
  
  Building a Vendor Onboarding Workflow with ABP Low-Code
&lt;/h1&gt;

&lt;p&gt;Vendor onboarding usually starts with a few familiar steps.&lt;/p&gt;

&lt;p&gt;A company sends its details, someone checks the documents, another person reviews the score, and the team either approves the vendor or asks for more information. After a while, the process turns into a mix of spreadsheets, uploaded files, status notes, and "who is waiting on this one?" messages.&lt;/p&gt;

&lt;p&gt;In this article, we'll build that workflow with the &lt;a href="https://abp.io/docs/latest/low-code/index" rel="noopener noreferrer"&gt;Low-Code System&lt;/a&gt;. We'll model the data in the &lt;a href="https://abp.io/docs/latest/low-code/designer" rel="noopener noreferrer"&gt;Low-Code Designer&lt;/a&gt;, let the &lt;a href="https://abp.io/docs/latest/low-code/react-runtime" rel="noopener noreferrer"&gt;React runtime&lt;/a&gt; render the page, and then add one &lt;a href="https://abp.io/docs/latest/low-code/custom-endpoints" rel="noopener noreferrer"&gt;custom endpoint&lt;/a&gt; for a summary that does not belong to normal CRUD.&lt;/p&gt;

&lt;p&gt;The example is an internal operations page where a team receives vendor applications, reviews compliance documents, tracks deadlines, and follows rejected or priority vendors from one place.&lt;/p&gt;

&lt;p&gt;That is a good place to try ABP Low-Code, because the first version of the workflow is mostly data, screens, validation rules, and a few process-specific actions. You do not need to hand-write a React page only to list vendor applications, upload a compliance document, or show a rejection reason when the status is rejected.&lt;/p&gt;

&lt;p&gt;We will start from an already running ABP React + EF Core application with Low-Code enabled, so the article can stay focused on the Admin Console, the Designer, and the runtime flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;The workflow has one main record: &lt;code&gt;VendorApplication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A reviewer should be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a vendor application with company and contact details.&lt;/li&gt;
&lt;li&gt;Track whether the vendor is &lt;code&gt;Submitted&lt;/code&gt;, &lt;code&gt;InReview&lt;/code&gt;, &lt;code&gt;Approved&lt;/code&gt;, or &lt;code&gt;Rejected&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set the requested date and approval deadline.&lt;/li&gt;
&lt;li&gt;Mark priority vendors.&lt;/li&gt;
&lt;li&gt;Assign a category such as &lt;code&gt;Software&lt;/code&gt;, &lt;code&gt;Services&lt;/code&gt;, or &lt;code&gt;Hardware&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Upload a logo and a compliance document.&lt;/li&gt;
&lt;li&gt;Fill in a rejection reason only when the application is rejected.&lt;/li&gt;
&lt;li&gt;Filter the generated grid by status, requested date, priority, and category.&lt;/li&gt;
&lt;li&gt;Call a summary endpoint that returns counts for dashboard-like use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll also touch two extra pieces around that main record. &lt;code&gt;VendorReviewTemplate&lt;/code&gt; comes from C# so you can see how code-defined metadata appears in the Designer. Later, a &lt;code&gt;VendorEscalation&lt;/code&gt; model is added while the Designer is switched to &lt;code&gt;Runtime JSON&lt;/code&gt;. You could build the whole workflow with one entry point, but using these three entry points makes the hybrid model visible without turning the article into three separate implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Note on How Low-Code Fits Together
&lt;/h2&gt;

&lt;p&gt;The Low-Code Designer is where you describe the model and the UI metadata. In this article we use four areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Data&lt;/code&gt; for enums and entities.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Pages&lt;/code&gt; for the generated grid route.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Forms&lt;/code&gt; for the create/edit form layout.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Actions&lt;/code&gt; for the custom HTTP endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Designer stores metadata. The React runtime reads that metadata and renders the page at runtime. That is the important mental model: when we add a field to the entity, the field can become a grid column, a filter, a validation rule, or a form input depending on how we configure the metadata around it.&lt;/p&gt;

&lt;p&gt;There is also one database detail to keep in mind. Metadata that comes from C# code or from &lt;code&gt;Dev JSON&lt;/code&gt; is source-controlled application metadata. When it introduces or changes a persisted entity, run the normal EF Core migration and database update flow before using the generated runtime page. In the validated demo for this article I used SQLite, so the migration updated the local SQLite database. &lt;code&gt;Runtime JSON&lt;/code&gt; is different: it is authored at runtime, so I do not run a C# migration in that section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a Code-Defined Review Template
&lt;/h2&gt;

&lt;p&gt;Let's start with one model that does not come from the Designer.&lt;/p&gt;

&lt;p&gt;In this workflow, vendor reviewers can use review templates. The template itself is not the center of the workflow, so I kept it focused on the review rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DynamicEnum&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;VendorReviewTemplateType&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Standard&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Security&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Finance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DefaultDisplayPropertyName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicEntityUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Vendor Review Templates"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VendorReviewTemplate&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DynamicEntityBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;StringLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DynamicPropertyUnique&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;VendorReviewTemplateType&lt;/span&gt; &lt;span class="n"&gt;TemplateType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MinimumComplianceScore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;RequiresDocumentReview&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Notes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then include the entity in your EF Core DbContext. This is the part that makes the migration create a real backing table for the code-defined model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DbSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VendorReviewTemplate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;VendorReviewTemplates&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VendorReviewTemplate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;VendorOnboardingLowCodeConsts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DbTablePrefix&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"VendorReviewTemplates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;VendorOnboardingLowCodeConsts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DbSchema&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureByConvention&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;512&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsUnique&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this model is defined in C#, treat it like the rest of your application schema changes: add the entity, add the DbSet/mapping, create/apply the EF Core migration, and then start the application.&lt;/p&gt;

&lt;p&gt;After the app starts, open &lt;strong&gt;Admin Console &amp;gt; Low-Code Designer &amp;gt; Data&lt;/strong&gt;. The model is visible there, but it is read-only because it was defined in code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcr32zqr8ju7dxqh86fb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcr32zqr8ju7dxqh86fb.png" alt="Code-defined VendorReviewTemplate shown as read-only in the Low-Code Designer" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the &lt;strong&gt;Properties&lt;/strong&gt; tab and you can see the fields that came from the C# class. They are available to the Low-Code System, but the Designer marks them as code-owned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk25hu6z965nfdwqs6s0g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk25hu6z965nfdwqs6s0g.png" alt="The Properties tab for the code-defined VendorReviewTemplate entity" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is useful in real projects. Some metadata can be shipped with the application, while the rest of the workflow can still be designed through the Admin Console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the Vendor Enums
&lt;/h2&gt;

&lt;p&gt;Now move to the part we actually build in the Designer.&lt;/p&gt;

&lt;p&gt;The animation below shows the Designer path in one pass. The next sections slow it down and explain the enum, entity, page, and form steps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffsb1ortvdzeazf5gcd4k.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffsb1ortvdzeazf5gcd4k.gif" alt="Creating the Designer metadata for the vendor onboarding workflow" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;Data &amp;gt; Enums&lt;/code&gt; and create the status enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VendorApplicationStatus
Submitted
InReview
Approved
Rejected

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before saving, the enum modal should contain the name and the four values:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6wftxr50uwwet8u62rnb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6wftxr50uwwet8u62rnb.png" alt="The VendorApplicationStatus enum creation modal before saving" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then create the category enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VendorCategory
Software
Services
Hardware

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order of the status values matters for the custom endpoint later, because the script checks the enum values by their numeric indexes. In this example &lt;code&gt;Submitted&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;Approved&lt;/code&gt; is &lt;code&gt;2&lt;/code&gt;, and &lt;code&gt;Rejected&lt;/code&gt; is &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After saving, the enum detail page shows the numeric values that the runtime and scripts will use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd50cqq5bob9w1ju1xygm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd50cqq5bob9w1ju1xygm.png" alt="The saved VendorApplicationStatus enum values in the Designer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the VendorApplication Entity
&lt;/h2&gt;

&lt;p&gt;Go to &lt;code&gt;Data &amp;gt; Entities&lt;/code&gt; and create &lt;code&gt;VendorApplication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the model that drives the rest of the article. Add these fields:&lt;/p&gt;

&lt;p&gt;| Field | Type | Configuration | | --- | --- | --- | | &lt;code&gt;CompanyName&lt;/code&gt; | &lt;code&gt;String&lt;/code&gt; | Required and unique | | &lt;code&gt;ContactEmail&lt;/code&gt; | &lt;code&gt;String&lt;/code&gt; | Required, email validation | | &lt;code&gt;Status&lt;/code&gt; | &lt;code&gt;Enum&lt;/code&gt; | &lt;code&gt;VendorApplicationStatus&lt;/code&gt; | | &lt;code&gt;RequestedOn&lt;/code&gt; | &lt;code&gt;Date&lt;/code&gt; | Application date | | &lt;code&gt;ApprovalDeadline&lt;/code&gt; | &lt;code&gt;Date&lt;/code&gt; | Review deadline | | &lt;code&gt;IsPriority&lt;/code&gt; | &lt;code&gt;Boolean&lt;/code&gt; | Priority flag | | &lt;code&gt;Category&lt;/code&gt; | &lt;code&gt;Enum&lt;/code&gt; | &lt;code&gt;VendorCategory&lt;/code&gt; | | &lt;code&gt;ComplianceScore&lt;/code&gt; | &lt;code&gt;Int&lt;/code&gt; | Review score | | &lt;code&gt;Logo&lt;/code&gt; | &lt;code&gt;Image&lt;/code&gt; | Logo upload | | &lt;code&gt;ComplianceDocument&lt;/code&gt; | &lt;code&gt;File&lt;/code&gt; | Document upload | | &lt;code&gt;RejectionReason&lt;/code&gt; | &lt;code&gt;String&lt;/code&gt; | Optional |&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuddrhmi94s4gm5ogrz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvuddrhmi94s4gm5ogrz6.png" alt="VendorApplication entity definition in the Designer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Properties&lt;/strong&gt; tab is where the entity becomes more than a name. The table shows the field types, enum bindings, and source layer. Scroll down and the upload-related fields are visible with their &lt;code&gt;Image&lt;/code&gt; and &lt;code&gt;File&lt;/code&gt; types:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0xma9nasta14l3x3x8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0xma9nasta14l3x3x8q.png" alt="The VendorApplication properties table in the Designer" width="799" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is no React code yet, but we already have a lot of behavior described: required fields, uniqueness, email validation, enum fields, upload fields, and the data shape that the runtime will use.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Image&lt;/code&gt; and &lt;code&gt;File&lt;/code&gt; types are worth calling out. They are not plain strings with a path. In the generated form they become upload controls, which is exactly what we need for vendor logos and compliance documents.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;VendorApplication&lt;/code&gt; is authored in the &lt;code&gt;Dev JSON&lt;/code&gt; layer, it also belongs to the source-controlled model. After saving the entity metadata, create/apply the EF Core migration before you open the generated page in the runtime. This is the step that creates the backing table for the low-code entity in the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate a Grid Page
&lt;/h2&gt;

&lt;p&gt;The reviewers need a page where they can work with applications, so go to &lt;code&gt;Pages&lt;/code&gt; and create a &lt;code&gt;dataGrid&lt;/code&gt; page named &lt;code&gt;vendor-onboarding&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bind it to &lt;code&gt;VendorApplication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before saving the page, the modal connects the route name, title, icon, and entity:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi16uqj84wfhwaw645lvp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi16uqj84wfhwaw645lvp.png" alt="The vendor-onboarding data grid page modal before saving" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the page is created, set &lt;code&gt;RequestedOn&lt;/code&gt; as the default sort field, keep it descending, adjust the icon if you want, and assign &lt;code&gt;vendor-application-form&lt;/code&gt; as the create/edit form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh7zzdsuwqofnwyjcxwx8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh7zzdsuwqofnwyjcxwx8.png" alt="The vendor-onboarding data grid page bound to VendorApplication" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the review workflow, keep the configured columns focused on the fields reviewers use most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Company name&lt;/li&gt;
&lt;li&gt;Status&lt;/li&gt;
&lt;li&gt;Requested date&lt;/li&gt;
&lt;li&gt;Priority&lt;/li&gt;
&lt;li&gt;Category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then configure the filters you want reviewers to use most often. In this workflow, the important filters are company, status, requested date, priority, and category. Depending on the runtime defaults, the generated grid may still expose additional fields such as contact email; the workflow is still driven by the focused page metadata above.&lt;/p&gt;

&lt;p&gt;Once the page is saved, the React runtime can resolve the route from the page metadata. The grid is generated from the entity and page configuration rather than from a hand-written React component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Create/Edit Form
&lt;/h2&gt;

&lt;p&gt;A grid is not enough. We also need a form that feels like the workflow.&lt;/p&gt;

&lt;p&gt;Go to &lt;code&gt;Forms&lt;/code&gt; and create &lt;code&gt;vendor-application-form&lt;/code&gt; for &lt;code&gt;VendorApplication&lt;/code&gt;. Split the fields into three tabs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxv9ajbycvdc0eac8er3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxv9ajbycvdc0eac8er3d.png" alt="The vendor-application-form creation modal before saving" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Company&lt;/strong&gt; : &lt;code&gt;CompanyName&lt;/code&gt;, &lt;code&gt;ContactEmail&lt;/code&gt;, &lt;code&gt;Category&lt;/code&gt;, &lt;code&gt;IsPriority&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; : &lt;code&gt;Status&lt;/code&gt;, &lt;code&gt;RequestedOn&lt;/code&gt;, &lt;code&gt;ApprovalDeadline&lt;/code&gt;, &lt;code&gt;ComplianceScore&lt;/code&gt;, &lt;code&gt;RejectionReason&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documents&lt;/strong&gt; : &lt;code&gt;Logo&lt;/code&gt;, &lt;code&gt;ComplianceDocument&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now add the conditional behavior for &lt;code&gt;RejectionReason&lt;/code&gt;. In this demo I used two complementary rules: one rule shows the field when &lt;code&gt;Status = Rejected&lt;/code&gt;, and the other hides it for non-rejected statuses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphtrhzfhaclkc72dh7ns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphtrhzfhaclkc72dh7ns.png" alt="The vendor-application-form with tabs and a conditional rule" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one of the places where Low-Code becomes more than "generate a CRUD page". The runtime does more than render a static form; it evaluates the rule while the user edits the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply the Migration Before Opening the Runtime
&lt;/h2&gt;

&lt;p&gt;Before opening the generated page, apply the database migration for the &lt;code&gt;Dev JSON&lt;/code&gt; changes. We used &lt;code&gt;Dev JSON&lt;/code&gt; for &lt;code&gt;VendorApplication&lt;/code&gt;, so the Designer wrote source-controlled descriptor files under &lt;code&gt;_Dynamic&lt;/code&gt;. The entity shape is now part of the application model, and the database needs the matching backing table before the React runtime can save records.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;Dev JSON&lt;/code&gt; is a good fit during development: the metadata files and the EF Core migration can be reviewed, committed, and reproduced in another environment. If the same entity had been created in the &lt;code&gt;Runtime JSON&lt;/code&gt; layer, you would not create a C# migration for that runtime edit; the metadata change would be stored in the database instead. In practice, use &lt;code&gt;Dev JSON&lt;/code&gt; for development-time, source-controlled changes, and use &lt;code&gt;Runtime JSON&lt;/code&gt; when you want production-time changes to be managed from the Admin Console and persisted in the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It in the React Runtime
&lt;/h2&gt;

&lt;p&gt;Open the generated &lt;code&gt;vendor-onboarding&lt;/code&gt; page in the React runtime and create a vendor application.&lt;/p&gt;

&lt;p&gt;On the &lt;code&gt;Documents&lt;/code&gt; tab, the &lt;code&gt;Logo&lt;/code&gt; and &lt;code&gt;ComplianceDocument&lt;/code&gt; fields are rendered as upload fields:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5n0u6xmbchcmiaau5mf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5n0u6xmbchcmiaau5mf.png" alt="The generated Documents tab rendering image and file upload fields" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now edit a record and change the status to &lt;code&gt;Rejected&lt;/code&gt;. The &lt;code&gt;RejectionReason&lt;/code&gt; field becomes available on the &lt;code&gt;Review&lt;/code&gt; tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxli5q1fk8w25ksh45mep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxli5q1fk8w25ksh45mep.png" alt="The Review tab showing the conditional RejectionReason field" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After saving a few records, use the generated filters to narrow the list to rejected vendors. Depending on the runtime configuration, the filter panel can expose more fields than the small set you configured for the workflow; here we only use the &lt;code&gt;Status = Rejected&lt;/code&gt; filter:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa4tvg0vdd72maeo0mess.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa4tvg0vdd72maeo0mess.png" alt="The generated grid filtered by Status = Rejected" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The short animation below gives a quick pass through the same runtime states: upload fields, the conditional rejection reason, and the filtered grid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjm0o3bd5hc1vgt473oo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjm0o3bd5hc1vgt473oo.gif" alt="Generated upload fields, conditional review field, and grid filters in the React runtime" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point we have a working page, form, validation, uploads, and filters. The important part is that all of it came from the metadata we configured in the Designer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a Custom Summary Endpoint
&lt;/h2&gt;

&lt;p&gt;Generated CRUD is enough for day-to-day record editing, but teams often need one operation that is specific to their process.&lt;/p&gt;

&lt;p&gt;For vendor onboarding, a summary endpoint is a good example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/custom/vendor-onboarding/summary

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Designer, open &lt;code&gt;Actions&lt;/code&gt; and create a custom HTTP action with that route. The script can use the &lt;a href="https://abp.io/docs/latest/low-code/scripting-api" rel="noopener noreferrer"&gt;Scripting API&lt;/a&gt; to query the same &lt;code&gt;VendorApplication&lt;/code&gt; data that the generated grid uses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxpz4serfgq75u4stphu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxpz4serfgq75u4stphu.png" alt="The custom HTTP action configured under Actions in the Designer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the script used in the demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;entityName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme.VendorOnboardingLowCode.Procurement.VendorApplication&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;vendorQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entityName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;totalVendors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entityName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;submittedVendors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vendorQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;approvedVendors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vendorQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;overdueReviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vendorQuery&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApprovalDeadline&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApprovalDeadline&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;totalVendors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;totalVendors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;submittedVendors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submittedVendors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;approvedVendors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;approvedVendors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;overdueReviews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;overdueReviews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;evaluatedOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the entity name shown in your Designer. In the screenshots, it is &lt;code&gt;Acme.VendorOnboardingLowCode.Procurement.VendorApplication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the endpoint runs, it returns the current counts from the low-code records:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07r8i4c20auap4kq7w28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07r8i4c20auap4kq7w28.png" alt="The JSON response of the custom summary endpoint" width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the bridge I like here. The page and form stay metadata-driven, but the process-specific summary is a short script exposed as a custom endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add One Runtime Model
&lt;/h2&gt;

&lt;p&gt;Now switch the Designer layer to &lt;code&gt;Runtime JSON&lt;/code&gt; and add one more entity: &lt;code&gt;VendorEscalation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This model represents the items that need extra attention. It could have been created in the same place as &lt;code&gt;VendorApplication&lt;/code&gt;; I am adding it here only to show that runtime-authored metadata participates in the same Low-Code System.&lt;/p&gt;

&lt;p&gt;Unlike the code and &lt;code&gt;Dev JSON&lt;/code&gt; examples above, this runtime-authored model is not part of the source-controlled migration flow in this walkthrough.&lt;/p&gt;

&lt;p&gt;The create modal is the same Designer experience, but the selected layer is now &lt;code&gt;Runtime JSON&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0uzzjoebnr7yb2ngw14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0uzzjoebnr7yb2ngw14.png" alt="The VendorEscalation entity creation modal in the Runtime JSON layer" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxzyqzscqdgpqpggs4ql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxzyqzscqdgpqpggs4ql.png" alt="The VendorEscalation entity authored in the Runtime JSON layer" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a data grid page for it and open it in the React runtime:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw3l42yfy2av2vpgozef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw3l42yfy2av2vpgozef.png" alt="The runtime-generated Vendor Escalations page" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the user's point of view, it behaves like the first generated page. From the metadata point of view, we have now seen code-defined metadata, Designer-authored metadata, and runtime-authored metadata in the same application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the Same Data from ABP Code
&lt;/h2&gt;

&lt;p&gt;The last bridge is application code.&lt;/p&gt;

&lt;p&gt;Sometimes the generated page is not the only consumer. You may want a typed application service, a scheduled job, or another API to read the same low-code records. The code below shows the idea by returning a backlog summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DynamicEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_vendorApplicationRepository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IAsyncQueryableExecuter&lt;/span&gt; &lt;span class="n"&gt;_queryableExecuter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VendorBacklogDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetBacklogAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entityDescriptor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DynamicModelManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Acme.VendorOnboardingLowCode.Procurement.VendorApplication"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entityDescriptor&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserFriendlyException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"VendorApplication model was not found."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_vendorApplicationRepository&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetEntityName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entityDescriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetQueryableAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;priorityQuery&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"IsPriority"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"IsPriority"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nextPriorityVendor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queryableExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;priorityQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RequestedOn"&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;VendorBacklogDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TotalVendors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queryableExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LongCountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;PriorityVendors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queryableExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LongCountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priorityQuery&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;RejectedVendors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queryableExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LongCountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
        &lt;span class="n"&gt;OverdueReviews&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queryableExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LongCountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ApprovalDeadline"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ApprovalDeadline"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
        &lt;span class="n"&gt;NextPriorityVendor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextPriorityVendor&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;GetData&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"CompanyName"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxkkwnh21zqzrgvfffn6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxkkwnh21zqzrgvfffn6.png" alt="The typed backlog summary returned by the application service" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important detail is that the aggregate operations stay on &lt;code&gt;IQueryable&lt;/code&gt;; the code does not load every vendor into memory just to count them. This is not a replacement for the generated page. It is the other direction: use the generated page for the admin experience, then read the same records from normal ABP code when another part of the application needs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;The workflow we built is intentionally focused, but the same shape can grow in a few directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add permissions around the generated pages and custom endpoint.&lt;/li&gt;
&lt;li&gt;Add more form rules for review-specific fields.&lt;/li&gt;
&lt;li&gt;Add an approval notification after a vendor is accepted.&lt;/li&gt;
&lt;li&gt;Add a scheduled job that checks overdue applications.&lt;/li&gt;
&lt;li&gt;Build a dashboard widget on top of the summary endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main pattern stays the same: model the data in the Low-Code Designer, let the React runtime render the operational page, and add code or scripting only for the parts that are specific to your business process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;ABP Low-Code is useful when the first version of a business workflow is mostly metadata: entities, fields, filters, forms, validation, uploads, and a few custom actions.&lt;/p&gt;

&lt;p&gt;In this vendor onboarding example, the &lt;code&gt;VendorApplication&lt;/code&gt; model gave us a generated grid and form, the runtime handled upload fields and conditional UI, and a custom endpoint added the summary that CRUD would not provide by itself. We also saw that low-code metadata can come from the Designer, from runtime JSON, or from C# code when you need that bridge.&lt;/p&gt;

&lt;p&gt;That is the part worth remembering: you can start with a working admin experience quickly, then extend the workflow where the generated behavior stops being enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further Reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/index" rel="noopener noreferrer"&gt;Low-Code System Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/designer" rel="noopener noreferrer"&gt;Low-Code Designer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/react-runtime" rel="noopener noreferrer"&gt;React Runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/custom-endpoints" rel="noopener noreferrer"&gt;Custom Endpoints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/scripting-api" rel="noopener noreferrer"&gt;Scripting API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>workflow</category>
      <category>abp</category>
      <category>react</category>
      <category>lowcode</category>
    </item>
    <item>
      <title>Introducing ABP Low-Code: Build Real ABP Apps in Minutes</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:44:16 +0000</pubDate>
      <link>https://dev.to/abp_io/introducing-abp-low-code-build-real-abp-apps-in-minutes-2nl0</link>
      <guid>https://dev.to/abp_io/introducing-abp-low-code-build-real-abp-apps-in-minutes-2nl0</guid>
      <description>&lt;h1&gt;
  
  
  Introducing ABP Low-Code: Build Real ABP Apps in Minutes
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Create runtime-managed pages, generated React screens, code-first C# entities, and Script API extensions without leaving the ABP application model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9soq6ld4u9ixkten9scx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9soq6ld4u9ixkten9scx.gif" alt="ABP Low-Code runtime loop — grids, forms, calendars, and pipelines inside one ABP app" width="600" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Want to try the same path?&lt;/strong&gt; Start from ABP Studio, enable the Low-Code runtime and designer, define pages in the Admin Console, and see them resolve inside the running ABP app.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ABP Low-Code at a glance
&lt;/h2&gt;

&lt;p&gt;| Runtime authoring | Generated screens | ABP-native extensibility | One application model | | :---: | :---: | :---: | :---: | | Define and update pages in the Admin Console | Grid, Form, Calendar, Kanban, Gallery, Dashboard | Code-first entities, Script API actions, and C# query paths | Runtime metadata, generated UI, and application code stay together |&lt;/p&gt;




&lt;h2&gt;
  
  
  Built into the ABP Platform
&lt;/h2&gt;

&lt;p&gt;Low-code is most useful when speed does not create a separate stack to maintain later.&lt;/p&gt;

&lt;p&gt;That is where many low-code products start to strain. They move quickly at the beginning, then force a second implementation track when the app needs permissions, auditability, custom logic, or tighter integration with existing application code.&lt;/p&gt;

&lt;p&gt;ABP Low-Code takes a different path. It runs &lt;strong&gt;inside the ABP Platform&lt;/strong&gt; , so runtime-managed pages are part of an application foundation that already includes identity, permissions, audit logging, APIs, and code-level extensibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edit at runtime. See it in the app.
&lt;/h2&gt;

&lt;p&gt;In the Low-Code Designer, you update a runtime-managed page. A few seconds later, the same application surface is visible in the live app. No rebuild loop. No parallel front-end implementation. No "we will wire it later" gap between authoring and runtime.&lt;/p&gt;

&lt;p&gt;ABP Low-Code shortens the cycle from model change to running screen while keeping the output grounded in the same ABP application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97rsiqfm3b3kcdwh90qf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97rsiqfm3b3kcdwh90qf.png" alt="ABP Low-Code designer workspace for a runtime page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this shows:&lt;/strong&gt; authoring and runtime are connected. Pages are defined in the designer and resolved in the running application.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  CRUD is table stakes
&lt;/h2&gt;

&lt;p&gt;If low-code only saves you from drawing a table and a form, it is not enough. Business applications need richer operational surfaces.&lt;/p&gt;

&lt;p&gt;In the generated app, the &lt;code&gt;Events&lt;/code&gt; screen ships with search, actions, filters, and form-driven editing. The form structure already understands tabs, relations, validation, and business-shaped input instead of leaving you with a blank shell to finish by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gio5ho9fcgzinnxm8mh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3gio5ho9fcgzinnxm8mh.gif" alt="Generated event grid and generated event form" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The point is not just generated CRUD. It is generated CRUD that already looks like the operational screens teams maintain in real applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  One model, multiple operational screens
&lt;/h2&gt;

&lt;p&gt;Business users do not think in one view. Operators want a calendar for scheduling, a kanban board for workflow, a grid for bulk operations, a gallery when media matters, and a dashboard when they need the state of the business at a glance.&lt;/p&gt;

&lt;p&gt;ABP Low-Code keeps those surfaces attached to the same underlying model. The same &lt;code&gt;Session&lt;/code&gt; model can appear as a &lt;strong&gt;calendar&lt;/strong&gt; for planning and a &lt;strong&gt;kanban pipeline&lt;/strong&gt; for operational flow. The same generated app can also include a &lt;strong&gt;speaker gallery&lt;/strong&gt; and an &lt;strong&gt;overview dashboard&lt;/strong&gt; for metrics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8iiv77kjbb7k99yxw89n.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8iiv77kjbb7k99yxw89n.gif" alt="The same runtime model shown as calendar and kanban views" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1llpqpm8zokx5fivsoes.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1llpqpm8zokx5fivsoes.png" alt="Overview dashboard in the live runtime app" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where ABP Low-Code starts to feel less like a form generator and more like a runtime application layer: one model, many working screens, no second implementation track for each view type.&lt;/p&gt;




&lt;h2&gt;
  
  
  When low-code needs code
&lt;/h2&gt;

&lt;p&gt;The real differentiator is not that ABP Low-Code can go fast. It is that &lt;strong&gt;speed does not require isolation from the application foundation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When generated CRUD is not enough, you extend the same app instead of throwing the low-code layer away.&lt;/p&gt;

&lt;p&gt;ABP Low-Code exposes a server-side &lt;strong&gt;Script API&lt;/strong&gt; inside the same application model. That scripting surface can back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom endpoints&lt;/strong&gt; when the UI needs an API-shaped response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interceptors&lt;/strong&gt; when create or update commands need validation or mutation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event handlers&lt;/strong&gt; when logic should react to runtime events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background jobs&lt;/strong&gt; when work should continue asynchronously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background workers&lt;/strong&gt; when operational logic should run on a schedule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, the visible proof happens to be &lt;code&gt;GET /api/custom/eventflow/highlights&lt;/code&gt;. The GIF shows an endpoint because it is the easiest proof surface to read. But the broader point is that endpoints are only one consumer of the same low-code scripting layer.&lt;/p&gt;

&lt;p&gt;That hybrid model matters in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code-first ABP entities can be surfaced in low-code flows and runtime pages.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low-code-managed data and screens stay reachable from Script API actions, application services, repository queries, and custom endpoints.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teams do not lose architectural control just because they gained a faster authoring layer.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cdh8c1t9kntbjc4jhbw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cdh8c1t9kntbjc4jhbw.gif" alt="Endpoint result and runtime dashboard reading the same numbers" width="760" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The actual capability is the shared ABP application model behind it: script when runtime logic is enough, C# when typed application services and repository queries are the better fit.&lt;/p&gt;

&lt;p&gt;This is the difference between "low-code as a shortcut" and "low-code as part of your application platform."&lt;/p&gt;




&lt;h2&gt;
  
  
  From code-first entity to generated page
&lt;/h2&gt;

&lt;p&gt;The first direction is code-first to low-code. A &lt;strong&gt;code-first&lt;/strong&gt; &lt;code&gt;SponsorActivation&lt;/code&gt; entity checked into the ASP.NET Core project can still become a working runtime page without forking into a separate low-code-only model.&lt;/p&gt;

&lt;p&gt;The code-first entity carries the same metadata that ABP Low-Code uses to generate the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DefaultDisplayPropertyName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CompanyName&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicEntityUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sponsor Activations"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicEntityAttachments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application/pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MaxFileCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SponsorActivation&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DynamicEntityBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicPropertyUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Sponsor"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CompanyName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EmailAddress&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicPropertyUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Contact Email"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ContactEmail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;SponsorActivationStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EventFlow.Events.Event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Title"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Volo.Abp.Identity.IdentityUser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IdentityUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ForeignAccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;OwnerUserId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicPropertyType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EntityPropertyType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;ActivationBudget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicPropertyImageOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;BrandLogo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DynamicPropertyFileOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application/pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".pptx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".docx"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ActivationBrief&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That class lives as normal C# source, gets migrated like the rest of the application, and is seeded with real records so the runtime page does not open as an empty shell.&lt;/p&gt;

&lt;p&gt;Inside the designer, selecting the &lt;code&gt;SponsorActivation&lt;/code&gt; entity auto-generates the page identity, binds the grid to the entity, and lands on a real runtime route at &lt;code&gt;/dynamic/sponsor-activation&lt;/code&gt;. The generated surface includes sponsor, email, event lookup, owner lookup, budget, image, and file fields directly from the C# model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdclnqvhfxg63sqcx2nbw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdclnqvhfxg63sqcx2nbw.gif" alt="ABP Low-Code selecting the SponsorActivation C# entity, generating the page, and opening the resulting runtime surface" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5u2gg7qk5xdnlqvamqzp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5u2gg7qk5xdnlqvamqzp.png" alt="Generated SponsorActivation form showing lookups, budget, image, and file fields coming directly from the C# entity" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the distinction that matters: code-first ABP entities can move through low-code without becoming throwaway artifacts, and low-code-generated surfaces remain part of the same application story.&lt;/p&gt;




&lt;h2&gt;
  
  
  Low-code data stays reachable from C
&lt;/h2&gt;

&lt;p&gt;The bridge also works in the other direction. A normal ABP application service can query a low-code model through &lt;code&gt;IRepository&amp;lt;DynamicEntity, Guid&amp;gt;&lt;/code&gt;, apply real filters, and combine that result with code-first aggregates.&lt;/p&gt;

&lt;p&gt;The service behind the endpoint in the previous section looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EventFlowLowCodeProofDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHybridSummaryAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;liveSessionQuery&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_dynamicEntityRepository&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetEntityName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EventFlow.Events.Session"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetQueryableAsync&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"int(it[\"Status\"]) == @0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;publicSessionQuery&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;liveSessionQuery&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bool(it[\"IsPublic\"]) == @0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sponsorQuery&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sponsorActivationRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetQueryableAsync&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activation&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;SponsorActivationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Approved&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
            &lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;SponsorActivationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Live&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;liveSessionCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AsyncExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;liveSessionQuery&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;publicSessionCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AsyncExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publicSessionQuery&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;activeSponsorActivationCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AsyncExecuter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sponsorQuery&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EventFlowLowCodeProofDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;LiveSessionCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;liveSessionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PublicSessionCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;publicSessionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ActiveSponsorActivationCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activeSponsorActivationCount&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, low-code-managed &lt;code&gt;Session&lt;/code&gt; rows are filtered from C# with real &lt;code&gt;Where(...)&lt;/code&gt; clauses, then combined with the typed &lt;code&gt;SponsorActivation&lt;/code&gt; repository. The endpoint and dashboard are just one presentation surface for that shared ABP query path.&lt;/p&gt;

&lt;p&gt;That is the ABP difference: low-code data stays reachable from code, and code-first entities stay reachable from low-code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why ABP Low-Code matters
&lt;/h2&gt;

&lt;p&gt;The value is not novelty. It is a faster way to build real business applications without separating speed from the application foundation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed without replatforming.&lt;/strong&gt; Runtime-managed screens reduce delivery time without moving the team onto a separate application stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance without friction.&lt;/strong&gt; Permissions, identity, auditability, and ABP platform foundations stay part of the story from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensibility without rewrite pressure.&lt;/strong&gt; When custom behavior shows up, the same application can be extended instead of replacing the low-code output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the core ABP Low-Code promise: faster delivery, still inside the application model you can extend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The public starting point for ABP Low-Code is &lt;strong&gt;ABP Studio&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl2nyd0n6qjbd7znpw3lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl2nyd0n6qjbd7znpw3lr.png" alt="ABP Studio new solution wizard with the Low-Code runtime and designer option enabled" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;ABP Studio&lt;/strong&gt; and create a new solution.&lt;/li&gt;
&lt;li&gt;In the solution wizard, enable &lt;strong&gt;Include Low-Code runtime and designer&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Complete the wizard, then run the generated backend and React UI from the solution.&lt;/li&gt;
&lt;li&gt;Sign in with the administrator account created for that solution.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Admin Console&lt;/strong&gt; to define runtime-managed entities, forms, pages, permissions, endpoints, and script actions.&lt;/li&gt;
&lt;li&gt;Switch to the application side to see those changes resolve live in the running app.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/designer" rel="noopener noreferrer"&gt;ABP Low-Code Designer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/fluent-api" rel="noopener noreferrer"&gt;ABP Low-Code Configuration &amp;amp; Fluent API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/scripting-api" rel="noopener noreferrer"&gt;ABP Low-Code Scripting API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/script-actions" rel="noopener noreferrer"&gt;ABP Low-Code Script Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/low-code/interceptors" rel="noopener noreferrer"&gt;ABP Low-Code Interceptors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/studio" rel="noopener noreferrer"&gt;ABP Studio Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/get-started/layered-web-application" rel="noopener noreferrer"&gt;Get Started with ABP: Creating a Layered Web Application&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>entities</category>
      <category>codefirst</category>
      <category>abp</category>
      <category>applicationdevelopme</category>
    </item>
    <item>
      <title>ABP Platform 10.6 RC Has Been Released</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Wed, 08 Jul 2026 05:41:45 +0000</pubDate>
      <link>https://dev.to/abp_io/abp-platform-106-rc-has-been-released-3b3m</link>
      <guid>https://dev.to/abp_io/abp-platform-106-rc-has-been-released-3b3m</guid>
      <description>&lt;h1&gt;
  
  
  ABP Platform 10.6 RC Has Been Released
&lt;/h1&gt;

&lt;p&gt;We are happy to release &lt;a href="https://abp.io" rel="noopener noreferrer"&gt;ABP&lt;/a&gt; version &lt;strong&gt;10.6 RC&lt;/strong&gt; (Release Candidate). This blog post introduces the new features and important changes in this new version.&lt;/p&gt;

&lt;p&gt;Try this version and provide feedback for a more stable version of ABP v10.6! Thanks to you in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started with the 10.6 RC
&lt;/h2&gt;

&lt;p&gt;You can check the &lt;a href="https://abp.io/get-started" rel="noopener noreferrer"&gt;Get Started page&lt;/a&gt; to see how to get started with ABP. You can either download &lt;a href="https://abp.io/get-started#abp-studio-tab" rel="noopener noreferrer"&gt;ABP Studio&lt;/a&gt; ( &lt;strong&gt;recommended&lt;/strong&gt; , if you prefer a user-friendly GUI application - desktop application) or use the &lt;a href="https://abp.io/docs/latest/cli" rel="noopener noreferrer"&gt;ABP CLI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdipj171vrvsga26ucomf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdipj171vrvsga26ucomf.png" alt="studio-switch-to-preview" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Guide
&lt;/h2&gt;

&lt;p&gt;You can check the migration guide if you are upgrading from v10.5 or earlier: &lt;a href="https://abp.io/docs/10.6/release-info/migration-guides/abp-10-6" rel="noopener noreferrer"&gt;ABP Version 10.6 Migration Guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New with ABP v10.6?
&lt;/h2&gt;

&lt;p&gt;In this section, I will introduce some major features released in this version. Here is a brief list of titles explained in the next sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention&lt;/li&gt;
&lt;li&gt;API Definition and Proxy Improvements for Content Types and Multipart Uploads&lt;/li&gt;
&lt;li&gt;Angular UI: Upgrade to Angular 22&lt;/li&gt;
&lt;li&gt;Antiforgery and OpenIddict Security Improvements&lt;/li&gt;
&lt;li&gt;OpenIddict: Generate Access Token from the UI&lt;/li&gt;
&lt;li&gt;Dependency Updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention
&lt;/h3&gt;

&lt;p&gt;ABP v10.6 adds three opt-in enhancements to the default background job worker. All of them are disabled by default, so existing applications keep the current behavior unless you enable them explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing successful jobs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default, a job is deleted as soon as it runs successfully. You can now set &lt;code&gt;StoreSuccessfulJobs = true&lt;/code&gt; to keep completed jobs in the store. A new &lt;code&gt;CompletionTime&lt;/code&gt; column marks completed jobs, and a cleanup worker prunes them after &lt;code&gt;SuccessfulJobRetentionTime&lt;/code&gt; (default: 7 days).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dedicated workers per job type&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AddDedicatedWorker(...)&lt;/code&gt; registers a worker that processes only the configured job argument types, each with its own distributed lock. The default worker continues handling all remaining job types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel job execution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;MaxParallelJobExecutionCount&lt;/code&gt; greater than 1 to execute multiple jobs in the same poll cycle. In this mode, each job is claimed with its own distributed lock so different application instances can process different jobs concurrently without running the same job twice.&lt;/p&gt;

&lt;p&gt;Example configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AbpBackgroundJobWorkerOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreSuccessfulJobs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SuccessfulJobRetentionTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDedicatedWorker&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmailJobArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SmsJobArgs&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"NotificationWorkerLock"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDedicatedWorker&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReportJobArgs&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReportWorkerLock"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxParallelJobExecutionCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These options are useful when you need better isolation between job types, higher throughput in clustered deployments, or an audit trail of successfully completed jobs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See the &lt;a href="https://abp.io/docs/10.6/framework/infrastructure/background-jobs" rel="noopener noreferrer"&gt;Background Jobs&lt;/a&gt; documentation and &lt;a href="https://github.com/abpframework/abp/pull/25742" rel="noopener noreferrer"&gt;#25742&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  API Definition and Proxy Improvements for Content Types and Multipart Uploads
&lt;/h3&gt;

&lt;p&gt;ABP v10.6 improves API definition generation and client proxies for file upload scenarios and non-JSON response types.&lt;/p&gt;

&lt;p&gt;The API definition now exposes response &lt;code&gt;ContentTypes&lt;/code&gt; and an &lt;code&gt;IsRemoteStream&lt;/code&gt; flag. C#, jQuery, and Angular proxies can use the declared media type instead of collapsing everything to &lt;code&gt;application/json&lt;/code&gt; and &lt;code&gt;text/plain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For upload DTOs containing &lt;code&gt;IRemoteStreamContent&lt;/code&gt;, generated Angular and jQuery proxies now forward &lt;code&gt;FormData&lt;/code&gt; as multipart requests instead of silently dropping the file payload or trying to serialize the stream as JSON.&lt;/p&gt;

&lt;p&gt;Server-side setup still follows the existing ABP pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AbpAspNetCoreMvcOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConventionalControllers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FormBodyBindingIgnoredTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UploadFileDto&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular client example after proxy regeneration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logo.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This closes long-standing gaps in generated proxies for stream-based uploads and improves support for text, blob, and custom response types.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See &lt;a href="https://github.com/abpframework/abp/pull/25639" rel="noopener noreferrer"&gt;#25639&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Angular UI: Upgrade to Angular 22
&lt;/h3&gt;

&lt;p&gt;ABP v10.6 upgrades the Angular UI stack to &lt;strong&gt;Angular 22.0.x&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This release also improves the locale loading mechanism with a fallback path, so culture resources load more reliably when optional locale files are missing or partially available.&lt;/p&gt;

&lt;p&gt;If you maintain a custom Angular UI on top of ABP, plan for the Angular 22 upgrade together with your ABP package update and regenerate proxies after upgrading.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See &lt;a href="https://github.com/abpframework/abp/pull/25690" rel="noopener noreferrer"&gt;#25690&lt;/a&gt; and &lt;a href="https://github.com/abpframework/abp/pull/25734" rel="noopener noreferrer"&gt;#25734&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Antiforgery and OpenIddict Security Improvements
&lt;/h3&gt;

&lt;p&gt;ABP v10.6 includes several security-focused fixes for mixed authentication scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Antiforgery claim issuer normalization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an application serves a token-authenticated SPA and cookie-authenticated MVC pages on the same origin, antiforgery validation could fail because the user id claim issuer differed between JWT and cookie authentication schemes. ABP now normalizes the user id claim issuer while generating and validating antiforgery tokens.&lt;/p&gt;

&lt;p&gt;This behavior is enabled by default through &lt;code&gt;AbpAntiForgeryOptions.NormalizeUserIdClaimIssuer&lt;/code&gt;. Razor Pages antiforgery validation was also aligned with the same normalization logic, which fixes failures in modules such as Setting Management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prevent OpenIddict &lt;code&gt;client_id&lt;/code&gt; from leaking into the interactive auth cookie&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ABP fixed a case where an OpenIddict authorization request could stamp the requested &lt;code&gt;client_id&lt;/code&gt; into the interactive authentication cookie during security-stamp refresh. That could corrupt audit logs and make later cookie-authenticated requests appear to belong to the OAuth client.&lt;/p&gt;

&lt;p&gt;The fix strips &lt;code&gt;client_id&lt;/code&gt; when the interactive cookie is refreshed. Tokens are unaffected, and cookies that were already corrupted self-heal on the next refresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward the current access token for authenticated client requests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HttpContextAbpAccessTokenProvider&lt;/code&gt; now forwards the incoming access token whenever the request is authenticated, including &lt;code&gt;client_credentials&lt;/code&gt; requests. This prevents unnecessary fallback to configured identity clients in machine-to-machine scenarios.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See &lt;a href="https://github.com/abpframework/abp/pull/25655" rel="noopener noreferrer"&gt;#25655&lt;/a&gt;, &lt;a href="https://github.com/abpframework/abp/pull/25669" rel="noopener noreferrer"&gt;#25669&lt;/a&gt;, &lt;a href="https://github.com/abpframework/abp/pull/25711" rel="noopener noreferrer"&gt;#25711&lt;/a&gt;, and &lt;a href="https://github.com/abpframework/abp/pull/25740" rel="noopener noreferrer"&gt;#25740&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  OpenIddict: Generate Access Token from the UI
&lt;/h3&gt;

&lt;p&gt;ABP Commercial v10.6 RC adds a &lt;strong&gt;Generate Access Token&lt;/strong&gt; action to OpenIddict application management pages across MVC, Blazor, MudBlazor, and Angular UIs.&lt;/p&gt;

&lt;p&gt;Administrators can request a token for an OpenIddict application directly from the UI. The backend forwards a &lt;code&gt;client_credentials&lt;/code&gt; request to &lt;code&gt;/connect/token&lt;/code&gt; and returns the generated access token to the caller.&lt;/p&gt;

&lt;p&gt;This is especially useful for testing integrations, validating scopes, and troubleshooting machine-to-machine authentication without leaving the admin UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Updates
&lt;/h3&gt;

&lt;p&gt;ABP v10.6 RC includes several dependency and package updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Angular packages upgraded to &lt;strong&gt;22.0.x&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.*&lt;/code&gt; and &lt;code&gt;System.*&lt;/code&gt; packages upgraded to &lt;strong&gt;10.0.9&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Data.SqlClient&lt;/code&gt; upgraded to &lt;strong&gt;7.0.2&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Swashbuckle.AspNetCore&lt;/code&gt; upgraded to &lt;strong&gt;10.2.3&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Check the &lt;a href="https://abp.io/docs/10.6/package-version-changes" rel="noopener noreferrer"&gt;Package Version Changes&lt;/a&gt; document for all updates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Other Improvements and Enhancements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permission management&lt;/strong&gt; : Skip dynamic permission initialization during migration runs to avoid noisy logs when the database is unavailable (&lt;a href="https://github.com/abpframework/abp/pull/25743" rel="noopener noreferrer"&gt;#25743&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security / principal access&lt;/strong&gt; : &lt;code&gt;ThreadCurrentPrincipalAccessor&lt;/code&gt; now returns an anonymous principal instead of &lt;code&gt;null&lt;/code&gt; in non-web contexts (&lt;a href="https://github.com/abpframework/abp/pull/25752" rel="noopener noreferrer"&gt;#25752&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Angular proxy generation&lt;/strong&gt; : Array parameters are now generated as &lt;code&gt;readonly&lt;/code&gt; in Angular proxies (&lt;a href="https://github.com/abpframework/abp/pull/25687" rel="noopener noreferrer"&gt;#25687&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date/time normalization&lt;/strong&gt; : Removed misleading warnings when normalizing &lt;code&gt;Unspecified&lt;/code&gt; &lt;code&gt;DateTime&lt;/code&gt; values near range boundaries (&lt;a href="https://github.com/abpframework/abp/pull/25703" rel="noopener noreferrer"&gt;#25703&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Management&lt;/strong&gt; : Indexing is more resilient under memory pressure in the commercial module.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Community News
&lt;/h2&gt;

&lt;h3&gt;
  
  
  New ABP Community Articles
&lt;/h3&gt;

&lt;p&gt;As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/abp-10.5.0-expands-blazor-ui-options-with-mudblazor-support-03rzmlpm" rel="noopener noreferrer"&gt;ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support&lt;/a&gt; by &lt;a href="https://abp.io/community/members/maliming" rel="noopener noreferrer"&gt;Liming Ma&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/angular-22-state-management-signals-signalstore-or-ngrx-yq8zg0nw" rel="noopener noreferrer"&gt;Angular 22 State Management: Signals, SignalStore, or NgRx?&lt;/a&gt; by &lt;a href="https://abp.io/community/members/sumeyye.kurtulus" rel="noopener noreferrer"&gt;Sumeyye Kurtulus&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/working-with-dapr-workflows-in-the-abp-framework-6476or18" rel="noopener noreferrer"&gt;Working with Dapr Workflows in the ABP Framework&lt;/a&gt; by &lt;a href="https://abp.io/community/members/EngincanV" rel="noopener noreferrer"&gt;Engincan Veske&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/my-speakers-view-of-convex-summit-2026-ai-net-conference-3uk6ln1l" rel="noopener noreferrer"&gt;My Speaker's View of CONVEX Summit 2026&lt;/a&gt; by &lt;a href="https://abp.io/community/members/alper" rel="noopener noreferrer"&gt;Alper Ebiçoğlu&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the ABP Community for all the content they have published. You can also &lt;a href="https://abp.io/community/posts/create" rel="noopener noreferrer"&gt;post your ABP related (text or video) content&lt;/a&gt; to the ABP Community.&lt;/p&gt;

&lt;h3&gt;
  
  
  ABP Summer Campaign: Get Up To 20% Off + $300 in AI Credits
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa30qxbx1m3pwqss5j0pq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa30qxbx1m3pwqss5j0pq.png" alt="summer-sale" width="752" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Summer is a great time to start building with ABP. From &lt;strong&gt;July 6 to July 20&lt;/strong&gt; , we're offering exclusive summer savings on &lt;strong&gt;ABP licenses and renewals&lt;/strong&gt; : &lt;strong&gt;20% off new licenses&lt;/strong&gt; , &lt;strong&gt;10% off renewals&lt;/strong&gt; , and &lt;strong&gt;up to $300 in AI credits&lt;/strong&gt; for the &lt;strong&gt;ABP AI Agent&lt;/strong&gt; in &lt;strong&gt;ABP Studio&lt;/strong&gt;. Whether you're starting a new project or upgrading your development workflow, this limited-time offer helps you save on your license while accelerating development with AI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can read the announcement here: &lt;a href="https://abp.io/community/announcements/abp-summer-campaign-get-up-to-20-off-300-in-ai-credits-r5lqtpg9" rel="noopener noreferrer"&gt;ABP Summer Campaign: Get Up To 20% Off + $300 in AI Credits&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This version comes with some new features and a lot of enhancements to the existing features. You can see the &lt;a href="https://abp.io/docs/10.6/release-info/road-map" rel="noopener noreferrer"&gt;Road Map&lt;/a&gt; documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.6 RC and provide feedback to help us release a more stable version.&lt;/p&gt;

&lt;p&gt;Thanks for being a part of this community!&lt;/p&gt;

</description>
      <category>update</category>
      <category>version</category>
      <category>newfeatures</category>
      <category>release</category>
    </item>
    <item>
      <title>ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Wed, 01 Jul 2026 08:25:31 +0000</pubDate>
      <link>https://dev.to/abp_io/abp-1050-expands-blazor-ui-options-with-mudblazor-support-4bd6</link>
      <guid>https://dev.to/abp_io/abp-1050-expands-blazor-ui-options-with-mudblazor-support-4bd6</guid>
      <description>&lt;h1&gt;
  
  
  ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support
&lt;/h1&gt;

&lt;p&gt;With ABP 10.5.0, new Blazor projects can now use &lt;strong&gt;MudBlazor&lt;/strong&gt; (Material Design) as an alternative to the long-standing default, &lt;strong&gt;Blazorise&lt;/strong&gt; (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.5.0 packages are live on nuget.org.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why add another Blazor UI library?
&lt;/h2&gt;

&lt;p&gt;Blazorise has been ABP's default Blazor UI library for years and &lt;strong&gt;remains the default and is fully supported&lt;/strong&gt; — existing Blazorise projects can keep moving at their own pace, and upgrading to 10.5.0 does not change anything for them.&lt;/p&gt;

&lt;p&gt;We added MudBlazor because one Blazor UI choice cannot fit every team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design language&lt;/strong&gt; — Bootstrap and Material Design serve different audiences, and forcing a single choice does not fit every team&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-source preference&lt;/strong&gt; — MudBlazor is MIT-licensed, which works well for teams that want an open-source frontend component stack without extra component-library licensing or compliance overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem fit&lt;/strong&gt; — Material Design third-party components (charts, rich text editors, data visualization, and so on) tend to integrate more naturally with a MudBlazor project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For new projects you can start with MudBlazor right away. Existing Blazorise projects do not need to be rewritten just to switch UI libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who should consider MudBlazor?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Teams that want the frontend component stack &lt;strong&gt;fully open source&lt;/strong&gt; with no licensing to manage (individual developers, open-source community projects, education / learning settings)&lt;/li&gt;
&lt;li&gt;Organizations with internal &lt;strong&gt;third-party dependency or supply-chain compliance&lt;/strong&gt; requirements that prefer MIT-licensed components&lt;/li&gt;
&lt;li&gt;New Blazor projects that want to start with &lt;strong&gt;Material Design&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Teams already comfortable with the &lt;strong&gt;MudBlazor ecosystem&lt;/strong&gt; (charts, rich text, rich UI components)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the MudBlazor option covers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Framework core
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Volo.Abp.MudBlazorUI&lt;/code&gt; provides the MudBlazor implementation of ABP's UI service abstractions, so code written against &lt;code&gt;IUiMessageService&lt;/code&gt; / &lt;code&gt;IUiNotificationService&lt;/code&gt; / &lt;code&gt;IUiPageProgressService&lt;/code&gt; runs unchanged in a MudBlazor project. Key building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MudBlazorUiMessageService&lt;/code&gt; — &lt;code&gt;Info&lt;/code&gt; / &lt;code&gt;Success&lt;/code&gt; / &lt;code&gt;Warn&lt;/code&gt; / &lt;code&gt;Error&lt;/code&gt; / &lt;code&gt;Confirm&lt;/code&gt; rendered through &lt;code&gt;MudDialog&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MudBlazorUiNotificationService&lt;/code&gt; — toast notifications via &lt;code&gt;MudSnackbar&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MudBlazorUiPageProgressService&lt;/code&gt; — top progress bar via &lt;code&gt;MudProgressLinear&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AbpMudCrudPageBase&amp;lt;...&amp;gt;&lt;/code&gt; — the MudBlazor counterpart to Blazorise's &lt;code&gt;AbpCrudPageBase&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AbpMudExtensibleDataGrid&amp;lt;TItem&amp;gt;&lt;/code&gt; — a &lt;code&gt;MudDataGrid&lt;/code&gt; wrapper integrated with Object Extension and time-zone conversion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UiMessageAlert&lt;/code&gt; / &lt;code&gt;UiNotificationAlert&lt;/code&gt; / &lt;code&gt;PageAlert&lt;/code&gt; — page-level alert and notification containers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Theming is split across three hosts — Blazor Server, WebAssembly, and MauiBlazor — each shipped with matching bundling contributors and modules that wire MudBlazor's JS and CSS into the ABP bundle system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three themes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LeptonX MudBlazor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LeptonX Lite MudBlazor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Basic Theme MudBlazor&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each theme's layout adopts MudBlazor components such as &lt;code&gt;MudAppBar&lt;/code&gt;, &lt;code&gt;MudDrawer&lt;/code&gt;, &lt;code&gt;MudNavLink&lt;/code&gt;, and &lt;code&gt;MudMenu&lt;/code&gt;, while keeping the theme's original color palette, dim / light / system modes, and RTL support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ceaxcj9glnzaw7xm2cz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ceaxcj9glnzaw7xm2cz.png" alt="LeptonX MudBlazor Dashboard" width="800" height="474"&gt;&lt;/a&gt;&lt;em&gt;LeptonX rendered with MudBlazor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbdo0wt93vd6kzug5bnv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbdo0wt93vd6kzug5bnv.png" alt="LeptonX Lite MudBlazor Dashboard" width="800" height="476"&gt;&lt;/a&gt;&lt;em&gt;LeptonX Lite rendered with MudBlazor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5bvmjtmhe4hjdmyf1rtd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5bvmjtmhe4hjdmyf1rtd.png" alt="Basic Theme MudBlazor Dashboard" width="800" height="476"&gt;&lt;/a&gt;&lt;em&gt;Basic Theme rendered with MudBlazor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The LeptonX themes reuse the same &lt;code&gt;lpx-*&lt;/code&gt; CSS classes across both UI libraries, so the overall information architecture, page layout, and theme experience stay consistent with the Blazorise version. Individual controls follow each UI library's own conventions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdocs%2Fen%2FCommunity-Articles%2F2026-07-01-mudblazor-in-abp-framework%2Fmud-vs-blazorise-leptonx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdocs%2Fen%2FCommunity-Articles%2F2026-07-01-mudblazor-in-abp-framework%2Fmud-vs-blazorise-leptonx.png" alt="Blazorise vs MudBlazor on the same LeptonX theme" width="799" height="386"&gt;&lt;/a&gt;&lt;em&gt;The same LeptonX theme — MudBlazor on the left, Blazorise on the right&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Module coverage
&lt;/h3&gt;

&lt;p&gt;Open-source modules in &lt;code&gt;abpframework/abp&lt;/code&gt; that ship with a MudBlazor implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Account&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt; — Users / Roles / OUs / ClaimTypes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission Management&lt;/strong&gt; — parent/child permissions with &lt;code&gt;MudTreeView&lt;/code&gt; and tri-state &lt;code&gt;MudCheckBox&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting Management&lt;/strong&gt; — grouped settings with &lt;code&gt;MudTabs&lt;/code&gt; (including theme switching)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tenant Management&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature Management&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additional MudBlazor implementations available on the Pro side, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity Pro&lt;/strong&gt; — extra management around Sessions, SecurityLogs, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenIddict Pro&lt;/strong&gt; — Application / Scope management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saas&lt;/strong&gt; — Tenant / Edition management with a connection-string dialog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Logging&lt;/strong&gt; — &lt;code&gt;MudDataGrid&lt;/code&gt; with a detail &lt;code&gt;MudDialog&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language Management&lt;/strong&gt; / &lt;strong&gt;Text Template Management&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Management&lt;/strong&gt; / &lt;strong&gt;Chat&lt;/strong&gt; / &lt;strong&gt;CMS Kit Pro&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Management&lt;/strong&gt; / &lt;strong&gt;GDPR&lt;/strong&gt; / &lt;strong&gt;Payment&lt;/strong&gt; , and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4fgr35u6czkvzlguyhdi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4fgr35u6czkvzlguyhdi.png" alt="Identity user management with MudDataGrid" width="799" height="505"&gt;&lt;/a&gt;&lt;em&gt;Identity user management built on &lt;code&gt;AbpMudExtensibleDataGrid&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfwwvglb9lyfdil7om4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfwwvglb9lyfdil7om4a.png" alt="Permission management modal" width="800" height="505"&gt;&lt;/a&gt;&lt;em&gt;Permission Management uses &lt;code&gt;MudTreeView&lt;/code&gt; and tri-state &lt;code&gt;MudCheckBox&lt;/code&gt; for parent/child permissions&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futc5p1nsb13mezrycvlg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futc5p1nsb13mezrycvlg.png" alt="Saas tenants list" width="800" height="506"&gt;&lt;/a&gt;&lt;em&gt;Saas module: tenant list with a "New tenant" dialog that includes connection-string editing&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Component mapping at a glance
&lt;/h3&gt;

&lt;p&gt;If you already know Blazorise, here are the most common mappings:&lt;/p&gt;

&lt;p&gt;| Blazorise | MudBlazor | |-----------|-----------| | &lt;code&gt;TextEdit @bind-Text&lt;/code&gt; | &lt;code&gt;MudTextField @bind-Value&lt;/code&gt; | | &lt;code&gt;Select / SelectItem&lt;/code&gt; | &lt;code&gt;MudSelect / MudSelectItem&lt;/code&gt; | | &lt;code&gt;DataGrid&lt;/code&gt; | &lt;code&gt;MudDataGrid&lt;/code&gt; (wrapped by ABP as &lt;code&gt;AbpMudExtensibleDataGrid&lt;/code&gt;) | | &lt;code&gt;Modal Show()/Hide()&lt;/code&gt; | &lt;code&gt;MudDialog ShowAsync()/CloseAsync()&lt;/code&gt; | | &lt;code&gt;Validations&lt;/code&gt; | &lt;code&gt;MudForm&lt;/code&gt; + built-in validation | | &lt;code&gt;Row / Column ColumnSize.Is6&lt;/code&gt; | &lt;code&gt;MudGrid / MudItem xs="12" sm="6"&lt;/code&gt; | | Bootstrap Icons &lt;code&gt;bi-*&lt;/code&gt; | &lt;code&gt;Icons.Material.Filled.*&lt;/code&gt; |&lt;/p&gt;

&lt;p&gt;A full mapping table with razor examples lives in the &lt;a href="https://abp.io/docs/latest/framework/ui/blazor" rel="noopener noreferrer"&gt;ABP Blazor UI documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supported Blazor project types
&lt;/h3&gt;

&lt;p&gt;ABP's MudBlazor support covers the Blazor project types you can create and run directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blazor Server&lt;/strong&gt; (&lt;code&gt;-u blazor-server&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blazor WebAssembly&lt;/strong&gt; (&lt;code&gt;-u blazor&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blazor WebApp&lt;/strong&gt; (&lt;code&gt;-u blazor-webapp&lt;/code&gt;, including InteractiveAuto)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ABP Suite
&lt;/h3&gt;

&lt;p&gt;ABP Suite detects the solution's UI library and generates the matching CRUD page automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Books&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AbpMudCrudPageBase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookAppService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BookDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetBookListInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreateUpdateBookDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;MudDialog&lt;/span&gt; &lt;span class="n"&gt;_createDialog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;MudForm&lt;/span&gt; &lt;span class="n"&gt;_createFormRef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The razor templates also split by UI library — Blazorise uses &lt;code&gt;&amp;lt;DataGrid&amp;gt;&lt;/code&gt; + &lt;code&gt;&amp;lt;Modal&amp;gt;&lt;/code&gt; + &lt;code&gt;&amp;lt;Validations&amp;gt;&lt;/code&gt;, MudBlazor uses &lt;code&gt;&amp;lt;MudDataGrid&amp;gt;&lt;/code&gt; + &lt;code&gt;&amp;lt;MudDialog&amp;gt;&lt;/code&gt; + &lt;code&gt;&amp;lt;MudForm&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing between Blazorise and MudBlazor
&lt;/h2&gt;

&lt;p&gt;Both UI libraries are production-ready and neither is strictly better. Common factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Familiarity&lt;/strong&gt; — teams comfortable with Bootstrap tend to stay on Blazorise; teams comfortable with Material Design pick MudBlazor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design system&lt;/strong&gt; — Bootstrap-style products lean toward Blazorise, Material Design products lean toward MudBlazor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem&lt;/strong&gt; — existing Bootstrap component libraries or design assets fit Blazorise; Material Design third-party components fit MudBlazor more naturally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing projects&lt;/strong&gt; — keep maintaining live Blazorise projects as they are; if you want to try MudBlazor, start a new project with it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensing&lt;/strong&gt; — the two UI libraries have different license terms, so check each library's official license page before making a choice (&lt;a href="https://blazorise.com/license" rel="noopener noreferrer"&gt;Blazorise&lt;/a&gt; / &lt;a href="https://github.com/MudBlazor/MudBlazor/blob/dev/LICENSE" rel="noopener noreferrer"&gt;MudBlazor&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not mix the two libraries within a single project — the choice is per solution, not per file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a MudBlazor project in ABP Studio
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ABP Studio (recommended)
&lt;/h3&gt;

&lt;p&gt;Open ABP Studio → &lt;strong&gt;New Solution&lt;/strong&gt; → pick a template → in the UI configuration step, select &lt;strong&gt;Blazor UI library = MudBlazor&lt;/strong&gt;. Everything else works the same as a Blazorise project. After Build &amp;amp; Run you land on a MudBlazor-styled application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ozxfdlfzrnwaod5fk77.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ozxfdlfzrnwaod5fk77.png" alt="ABP Studio New Solution wizard with MudBlazor selected" width="800" height="563"&gt;&lt;/a&gt;&lt;em&gt;New Solution wizard: pick MudBlazor for the Blazor UI library&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm5uqy8my4oqipvnyw0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm5uqy8my4oqipvnyw0k.png" alt="First run after creation" width="799" height="505"&gt;&lt;/a&gt;&lt;em&gt;Studio Build &amp;amp; Run brings up a MudBlazor + LeptonX dashboard in the embedded browser&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Blazorise (default; --blazor-ui-library can be omitted)&lt;/span&gt;
abp new MyApp &lt;span class="nt"&gt;-u&lt;/span&gt; blazor

&lt;span class="c"&gt;# MudBlazor&lt;/span&gt;
abp new MyApp &lt;span class="nt"&gt;-u&lt;/span&gt; blazor &lt;span class="nt"&gt;--blazor-ui-library&lt;/span&gt; mudblazor

&lt;span class="c"&gt;# Tiered + WebApp + LeptonX + MudBlazor&lt;/span&gt;
abp new MyApp &lt;span class="nt"&gt;-t&lt;/span&gt; app &lt;span class="nt"&gt;--tiered&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; blazor-webapp &lt;span class="nt"&gt;--blazor-ui-library&lt;/span&gt; mudblazor &lt;span class="nt"&gt;--theme&lt;/span&gt; leptonx

&lt;span class="c"&gt;# Microservice + MudBlazor + Blazor Server&lt;/span&gt;
abp new MyApp &lt;span class="nt"&gt;-t&lt;/span&gt; microservice &lt;span class="nt"&gt;-u&lt;/span&gt; blazor-server &lt;span class="nt"&gt;--blazor-ui-library&lt;/span&gt; mudblazor

&lt;span class="c"&gt;# Reusable Module + MudBlazor&lt;/span&gt;
abp new My.Module &lt;span class="nt"&gt;-t&lt;/span&gt; module &lt;span class="nt"&gt;-u&lt;/span&gt; blazor &lt;span class="nt"&gt;--blazor-ui-library&lt;/span&gt; mudblazor

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;abp new --help&lt;/code&gt; for the full option list.&lt;/p&gt;

&lt;p&gt;Suite-generated MudBlazor CRUD pages are covered in the &lt;strong&gt;ABP Suite&lt;/strong&gt; section above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;abp new MyMudApp &lt;span class="nt"&gt;-u&lt;/span&gt; blazor-server &lt;span class="nt"&gt;--blazor-ui-library&lt;/span&gt; mudblazor &lt;span class="nt"&gt;--theme&lt;/span&gt; leptonx

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/framework/ui/blazor/forms-validation?BlazorUI=MudBlazor" rel="noopener noreferrer"&gt;Forms &amp;amp; Validation (MudBlazor)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/ui-themes/lepton-x/blazor" rel="noopener noreferrer"&gt;LeptonX with MudBlazor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/framework/ui/blazor/basic-theme" rel="noopener noreferrer"&gt;Basic Theme MudBlazor variant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/framework/ui/blazor/page-header" rel="noopener noreferrer"&gt;Page Header (MudBlazor)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I'm already using Blazorise — will upgrading to 10.5.0 break my project?&lt;/strong&gt; No. Blazorise stays the default, and package paths, type names, and namespaces are fully compatible. Follow the standard ABP upgrade flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Blazorise and MudBlazor in the same project?&lt;/strong&gt; We don't recommend it. The UI library is a project-level choice — themes, bundling, and module dependencies all switch with it. Mixing both within a single solution leads to bundle conflicts, duplicated layouts, and similar issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about my custom razor pages?&lt;/strong&gt; Your custom Razor pages are tied to the UI library they were built with, so switching libraries means rewriting those pages using the component mapping above. Template-generated pages and module-provided pages don't need to be touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;MudBlazor is now a first-class Blazor UI library in ABP. With 10.5.0 released, every related package, theme, template, Studio integration, and Suite generator is in place — you can try it out with a single &lt;code&gt;abp new&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;If you hit a bug, have a suggestion, or want a particular module's MudBlazor UX prioritized, let us know via &lt;a href="https://github.com/abpframework/abp/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt; or &lt;a href="https://abp.io/support" rel="noopener noreferrer"&gt;abp.io support&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mudblazor.com" rel="noopener noreferrer"&gt;MudBlazor official site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/docs/latest/framework/ui/blazor" rel="noopener noreferrer"&gt;ABP Blazor UI documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/themes/leptonx" rel="noopener noreferrer"&gt;ABP LeptonX theme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/studio" rel="noopener noreferrer"&gt;ABP Studio download&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>blazor</category>
      <category>mudblazor</category>
      <category>blazorise</category>
      <category>abpsuite</category>
    </item>
    <item>
      <title>ABP.IO Platform 10.5 Final Has Been Released!</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Tue, 30 Jun 2026 11:45:46 +0000</pubDate>
      <link>https://dev.to/abp_io/abpio-platform-105-final-has-been-released-5gg5</link>
      <guid>https://dev.to/abp_io/abpio-platform-105-final-has-been-released-5gg5</guid>
      <description>&lt;h1&gt;
  
  
  ABP.IO Platform 10.5 Final Has Been Released!
&lt;/h1&gt;

&lt;p&gt;We are glad to announce that &lt;a href="https://abp.io/" rel="noopener noreferrer"&gt;ABP&lt;/a&gt; 10.5 stable version has been released.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New With Version 10.5?
&lt;/h2&gt;

&lt;p&gt;All the new features were explained in detail in the &lt;a href="https://abp.io/community/announcements/announcing-abp-10-5-release-candidate-k6oxdfle" rel="noopener noreferrer"&gt;10.5 RC Announcement Post&lt;/a&gt;, so there is no need to review them again. You can check it out for more details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with 10.5
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How to Upgrade an Existing Solution
&lt;/h3&gt;

&lt;p&gt;You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained:&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading via ABP Studio
&lt;/h3&gt;

&lt;p&gt;If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See &lt;a href="https://abp.io/docs/latest/studio/installation#upgrading" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; for more info.&lt;/p&gt;

&lt;p&gt;After upgrading the ABP Studio, then you can open your solution in the application, and simply click the &lt;strong&gt;Upgrade ABP Packages&lt;/strong&gt; action button to instantly upgrade your solution:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvicov4f3mrul7ees05g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvicov4f3mrul7ees05g.png" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading via ABP CLI
&lt;/h3&gt;

&lt;p&gt;Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version.&lt;/p&gt;

&lt;p&gt;If you haven't installed it yet, you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; Volo.Abp.Studio.Cli

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or to update the existing CLI, you can run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool update &lt;span class="nt"&gt;-g&lt;/span&gt; Volo.Abp.Studio.Cli

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing/updating the ABP CLI, you can use the &lt;a href="https://abp.io/docs/latest/CLI#update" rel="noopener noreferrer"&gt;&lt;code&gt;update&lt;/code&gt; command&lt;/a&gt; to update all the ABP related NuGet and NPM packages in your solution as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;abp update

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run this command in the root folder of your solution to update all ABP related packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Guides
&lt;/h2&gt;

&lt;p&gt;There are no explicitly marked breaking changes in this version. However, there are still some important migration notes for specific scenarios. Please read the migration guide carefully, if you are upgrading from v10.4 or earlier versions: &lt;a href="https://abp.io/docs/10.5/release-info/migration-guides/abp-10-5" rel="noopener noreferrer"&gt;ABP Version 10.5 Migration Guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Community News
&lt;/h2&gt;

&lt;h3&gt;
  
  
  New ABP Community Articles
&lt;/h3&gt;

&lt;p&gt;As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/members/sumeyye.kurtulus" rel="noopener noreferrer"&gt;Sumeyye Kurtulus&lt;/a&gt; has published 2 new articles:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://abp.io/community/articles/angular-22-state-management-signals-signalstore-or-ngrx-yq8zg0nw" rel="noopener noreferrer"&gt;Angular 22 State Management: Signals, SignalStore, or NgRx?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/community/articles/customizing-the-abp-framework-a-developers-guide-to-nklweri3" rel="noopener noreferrer"&gt;Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/working-with-dapr-workflows-in-the-abp-framework-6476or18" rel="noopener noreferrer"&gt;Working with Dapr Workflows in the ABP Framework&lt;/a&gt; by &lt;a href="https://abp.io/community/members/EngincanV" rel="noopener noreferrer"&gt;Engincan Veske&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/members/alper" rel="noopener noreferrer"&gt;Alper Ebicoglu&lt;/a&gt; has published 2 new articles:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://abp.io/community/articles/my-speakers-view-of-convex-summit-2026-ai-net-conference-3uk6ln1l" rel="noopener noreferrer"&gt;My Speaker's View of CONVEX Summit 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://abp.io/community/articles/ai-isnt-replacing-developers-its-changing-what-good-2016q6ng" rel="noopener noreferrer"&gt;AI Isn't Replacing Developers - It's Changing What Good Developers Spend Time On&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-the-complete-series-f7jute7n" rel="noopener noreferrer"&gt;Deep Dive on ABP AI Agent: The Complete Series&lt;/a&gt; by &lt;a href="https://abp.io/community/members/berkansasmaz" rel="noopener noreferrer"&gt;Berkan Sasmaz&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;We have created a deep-dive series for ABP Studio's AI Coding Agent. You can read this series to learn the main features of the AI Coding Agent and how it can help you while developing ABP-based solutions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the ABP Community for all the content they have published. You can also &lt;a href="https://abp.io/community/posts/create" rel="noopener noreferrer"&gt;post your ABP related (text or video) content&lt;/a&gt; to the ABP Community.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the Next Version
&lt;/h2&gt;

&lt;p&gt;The next feature version will be 10.6. You can follow the &lt;a href="https://github.com/abpframework/abp/milestones" rel="noopener noreferrer"&gt;release planning here&lt;/a&gt;. Please &lt;a href="https://github.com/abpframework/abp/issues/new" rel="noopener noreferrer"&gt;submit an issue&lt;/a&gt; if you have any problems with this version.&lt;/p&gt;

</description>
      <category>update</category>
      <category>abp</category>
      <category>newversion</category>
      <category>release</category>
    </item>
    <item>
      <title>Angular 22 State Management: Signals, SignalStore, or NgRx?</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Tue, 30 Jun 2026 07:38:01 +0000</pubDate>
      <link>https://dev.to/abp_io/angular-22-state-management-signals-signalstore-or-ngrx-l5b</link>
      <guid>https://dev.to/abp_io/angular-22-state-management-signals-signalstore-or-ngrx-l5b</guid>
      <description>&lt;h1&gt;
  
  
  Angular 22 State Management: Signals, SignalStore, or NgRx?
&lt;/h1&gt;

&lt;p&gt;Angular has been steadily moving toward a signal-first architecture since the introduction of Signals in Angular 16. With Angular 22, that transition reaches another milestone. Signals are now at the center of Angular's reactive programming model, while APIs such as Resource and Signal Forms have matured into production-ready solutions. Combined with the framework's continued investment in zoneless change detection, these improvements significantly influence how Angular applications should manage state.&lt;/p&gt;

&lt;p&gt;This shift also changes the role of NgRx. While the classic NgRx Store remains a powerful solution for large, event-driven applications, many scenarios that previously required reducers, selectors, and effects can now be implemented with much simpler, feature-scoped signal stores. Rather than replacing NgRx, Angular 22 encourages developers to choose the right state management strategy based on the scope and complexity of the problem.&lt;/p&gt;

&lt;p&gt;In this article, we'll explore how Angular 22 changes the state management landscape, compare the classic NgRx Store with NgRx SignalStore, and demonstrate best practices for building modern Angular applications. We'll also discuss how Angular's new reactive APIs fit into enterprise applications and what these changes mean for projects built with the ABP Framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Angular 22 Changes State Management
&lt;/h2&gt;

&lt;p&gt;Angular Signals introduced a fundamentally different approach by providing fine-grained reactivity built directly into the framework. Instead of propagating changes through Observable streams, Signals allow Angular to track exactly which pieces of state are consumed and update only the affected parts of the UI. This results in more predictable rendering, less boilerplate, and improved runtime performance.&lt;/p&gt;

&lt;p&gt;Angular 22 builds on this foundation by making Signals the preferred reactive primitive throughout the framework. New APIs such as &lt;strong&gt;Resource&lt;/strong&gt; for asynchronous data loading and &lt;strong&gt;Signal Forms&lt;/strong&gt; for reactive forms integrate naturally with Signals, reducing the need for custom RxJS pipelines in many common scenarios.&lt;/p&gt;

&lt;p&gt;For developers using NgRx, this doesn't mean abandoning existing applications or rewriting every store. Instead, it changes how state management should be approached. Component-local state can often be managed with plain Signals, feature-level state fits naturally into SignalStore, and the classic NgRx Store continues to excel for large-scale applications that benefit from centralized event streams, auditing, and global state synchronization.&lt;/p&gt;

&lt;p&gt;Understanding these changing responsibilities is the key to designing maintainable Angular applications in the Angular 22 era. A practical way to think about state management is to start with the simplest solution and introduce additional abstractions only when the application's complexity requires them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Signals for Local Component State
&lt;/h2&gt;

&lt;p&gt;Plain Angular Signals are ideal for state that belongs exclusively to a single component. Examples include dialog visibility, selected tabs, loading indicators, filter values, or temporary form data.&lt;/p&gt;

&lt;p&gt;Signals provide a straightforward API with minimal overhead and integrate seamlessly with Angular's change detection. For state that never needs to be shared outside a component or its immediate children, introducing a dedicated store often adds unnecessary complexity.&lt;/p&gt;

&lt;p&gt;A settings page often contains UI state that doesn't need to be shared with the rest of the application. Using a dedicated store for this would introduce unnecessary complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserListComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;showInactive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;filteredUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showInactive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This state is entirely local to the component and doesn't justify introducing a SignalStore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use NgRx SignalStore for Feature State
&lt;/h2&gt;

&lt;p&gt;As applications grow, state often needs to be shared across multiple components within the same feature. Examples include user profiles, shopping carts, administration screens, dashboards, or settings pages.&lt;/p&gt;

&lt;p&gt;NgRx SignalStore is designed specifically for these scenarios. It combines Angular Signals with a lightweight, feature-oriented architecture where state, computed values, and business logic are defined together. Instead of scattering logic across reducers, selectors, effects, and services, developers can keep everything related to a feature inside a single store.&lt;/p&gt;

&lt;p&gt;SignalStore also integrates naturally with Angular's signal-based APIs, making it an excellent choice for modern Angular applications built around Resources and Signal Forms.&lt;/p&gt;

&lt;p&gt;A User Management module is shared by multiple pages. The selected user, filters, and loaded entities should remain synchronized across those pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;withState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;

  &lt;span class="nf"&gt;withComputed&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedUserId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;selectedUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;

  &lt;span class="nf"&gt;withMethods&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;selectUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything related to the feature lives in one place: state, derived values, and business operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  NgRx Store vs. NgRx SignalStore
&lt;/h2&gt;

&lt;p&gt;Although both solutions belong to the NgRx ecosystem, they are designed to solve different architectural problems.&lt;/p&gt;

&lt;p&gt;The classic NgRx Store follows the Redux pattern, where every state change is represented by an action that flows through reducers before producing a new immutable state. This explicit, event-driven architecture provides excellent traceability and scales well for applications with extensive global interactions.&lt;/p&gt;

&lt;p&gt;SignalStore takes a different approach. Instead of centering the application around dispatched actions, it treats state as a reactive service built with Angular Signals. A SignalStore typically contains three core building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State&lt;/strong&gt; , which represents the application's reactive data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computed signals&lt;/strong&gt; , which derive values from existing state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Methods&lt;/strong&gt; , which encapsulate business logic and state updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This functional model significantly reduces boilerplate while remaining predictable and testable. Since it builds directly on Angular Signals, it also integrates naturally with Angular's fine-grained change detection without requiring selectors or &lt;code&gt;async&lt;/code&gt; pipes for many common scenarios.&lt;/p&gt;

&lt;p&gt;The following comparison summarizes the strengths of each approach.&lt;/p&gt;

&lt;p&gt;| Feature | Classic NgRx Store | NgRx SignalStore | | ---------------- | ------------------------------- | --------------------------------- | | Architecture | Redux-based global store | Feature-oriented reactive store | | Reactivity | RxJS Observables | Angular Signals | | Boilerplate | Higher | Lower | | State Scope | Global application state | Feature or route state | | Side Effects | Effects | Store methods or &lt;code&gt;rxMethod&lt;/code&gt; | | Change Detection | Observable subscriptions | Native signal reactivity | | Best For | Large event-driven applications | Modern feature-based applications |&lt;/p&gt;

&lt;p&gt;For most new Angular 22 applications, SignalStore is an excellent default choice for feature-level state management because it embraces the framework's signal-first architecture while keeping code concise and maintainable. The classic NgRx Store remains indispensable for applications that rely heavily on centralized event processing, global synchronization, or advanced debugging capabilities.&lt;/p&gt;

&lt;p&gt;Instead of asking &lt;em&gt;"Which one should I use?"&lt;/em&gt;, the better question is &lt;em&gt;"Which scope of state am I trying to manage?"&lt;/em&gt; The answer usually determines the appropriate solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Angular 22 Features That Improve State Management
&lt;/h2&gt;

&lt;p&gt;Angular 22 introduces several framework APIs that naturally complement modern state management patterns. Rather than replacing NgRx, these APIs reduce the amount of custom infrastructure developers previously had to build around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource API
&lt;/h3&gt;

&lt;p&gt;One of the most significant additions is the &lt;strong&gt;Resource API&lt;/strong&gt; , which provides a signal-based approach to asynchronous data loading.&lt;/p&gt;

&lt;p&gt;Historically, fetching remote data in Angular involved coordinating &lt;code&gt;HttpClient&lt;/code&gt;, RxJS operators, subscriptions, loading flags, and error handling. While these patterns remain valid, they often require considerable boilerplate even for straightforward scenarios.&lt;/p&gt;

&lt;p&gt;Resources encapsulate these concerns into a single reactive abstraction. A Resource automatically tracks the signals it depends on, performs requests when those dependencies change, cancels obsolete requests, and exposes its lifecycle through reactive state such as the current value, loading status, and errors.&lt;/p&gt;

&lt;p&gt;This makes Resources particularly well suited for read-oriented operations where data should stay synchronized with application state.&lt;/p&gt;

&lt;p&gt;For example, changing a selected user ID can automatically trigger a new request without manually wiring &lt;code&gt;switchMap&lt;/code&gt; or managing subscription lifecycles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;httpResource&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Signal Forms
&lt;/h3&gt;

&lt;p&gt;Another major improvement is the stabilization of &lt;strong&gt;Signal Forms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Traditional Reactive Forms expose their state through &lt;code&gt;FormControl&lt;/code&gt; and &lt;code&gt;FormGroup&lt;/code&gt; instances, requiring developers to query validation status, dirty state, touched state, and values through an imperative API.&lt;/p&gt;

&lt;p&gt;Signal Forms expose these properties as signals instead. Every field becomes reactive by default, making templates easier to read while eliminating much of the manual state synchronization commonly found in form-heavy applications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@if (profileForm.email.invalid() &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; profileForm.email.touched()) {
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Please enter a valid email.&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because field state is already reactive, components rarely need additional subscriptions or helper observables to keep the UI synchronized.&lt;/p&gt;

&lt;p&gt;It's important to note that Signal Forms are responsible for &lt;strong&gt;UI state&lt;/strong&gt; , while business operations such as saving data, loading entities, or handling server responses still belong in a dedicated service or SignalStore. Keeping these responsibilities separate results in components that remain focused on presentation while stores continue to own application logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices for Building Modern SignalStores
&lt;/h2&gt;

&lt;p&gt;SignalStore significantly reduces the ceremony traditionally associated with state management, but the same architectural principles still apply. A well-designed store should encapsulate business logic without becoming responsible for concerns that belong elsewhere.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep Stores Focused on a Single Feature A SignalStore should represent a cohesive business feature rather than becoming a global container for unrelated state. For example, an administration module might expose separate stores for users, roles, and permissions instead of combining all administrative functionality into a single, monolithic store. Smaller stores are easier to test, understand, and maintain over time.&lt;/li&gt;
&lt;li&gt;Store Business State, Not UI State Not every piece of state belongs in a store. Transient UI concerns such as dialog visibility, selected tabs, expanded panels, or temporary input values are usually better managed with plain Signals inside the component. Stores should own state that represents the application's business domain—entities, filters, permissions, settings, or data shared across multiple components.&lt;/li&gt;
&lt;li&gt;Derive State Instead of Duplicating It Whenever possible, compute values instead of storing them. SignalStore's &lt;code&gt;withComputed()&lt;/code&gt; feature makes it easy to derive reactive values from existing state, reducing the likelihood of inconsistent or stale data. Instead of storing both a list of users and an active user count, derive the count directly from the collection.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;withComputed&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;activeUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping a single source of truth simplifies updates and reduces maintenance. 4. Prefer Immutable State Updates Although SignalStore simplifies updates through &lt;code&gt;patchState()&lt;/code&gt;, state should still be treated as immutable. Updating only the affected portions of state makes changes predictable and allows Angular's signal system to efficiently notify dependent computations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;newUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integrating Resources with SignalStore
&lt;/h2&gt;

&lt;p&gt;Resources and SignalStore solve different problems, and understanding their responsibilities leads to a cleaner architecture.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Resource&lt;/strong&gt; is responsible for synchronizing data with a remote source. It knows how to load data, react to parameter changes, expose loading and error states, and keep requests up to date.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;SignalStore&lt;/strong&gt; , on the other hand, owns the application's business state. It coordinates operations, exposes domain-specific methods, derives computed values, and serves as the single source of truth for a feature.&lt;/p&gt;

&lt;p&gt;Rather than replacing one another, they work best together.&lt;/p&gt;

&lt;p&gt;A common pattern is to use a Resource for loading entities while allowing the store to expose business operations that modify those entities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;withState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;

  &lt;span class="nf"&gt;withComputed&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;selectedUserId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;userResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;

  &lt;span class="nf"&gt;withMethods&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;selectUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;patchState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, changing the selected user automatically causes the Resource to fetch new data. The store doesn't need to manage subscriptions or manually coordinate loading indicators because the Resource already exposes this information through signals.&lt;/p&gt;

&lt;p&gt;This separation keeps data synchronization declarative while allowing the store to remain focused on business behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Signal Forms with SignalStore
&lt;/h2&gt;

&lt;p&gt;Signal Forms and SignalStore naturally complement one another because both are built on Angular Signals. However, they should not be treated as interchangeable.&lt;/p&gt;

&lt;p&gt;Signal Forms are responsible for managing user input and validation, while SignalStore coordinates business operations such as loading, updating, and persisting data.&lt;/p&gt;

&lt;p&gt;A common workflow consists of four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the entity through the store.&lt;/li&gt;
&lt;li&gt;Populate the Signal Form.&lt;/li&gt;
&lt;li&gt;Allow the user to edit the data.&lt;/li&gt;
&lt;li&gt;Submit the updated values back to the store.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The component remains responsible only for orchestrating the interaction between the form and the store.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEditorComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserStore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach keeps presentation concerns inside the component while allowing business rules to remain centralized in the store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Asynchronous Operations
&lt;/h2&gt;

&lt;p&gt;One challenge when combining Signal Forms with SignalStore is coordinating asynchronous operations.&lt;/p&gt;

&lt;p&gt;A form submission typically expects an asynchronous operation to complete before updating its own state. Meanwhile, the store is responsible for managing loading indicators, server errors, and successful updates.&lt;/p&gt;

&lt;p&gt;Instead of placing HTTP requests directly inside components, expose descriptive methods such as &lt;code&gt;createUser()&lt;/code&gt;, &lt;code&gt;updateProfile()&lt;/code&gt;, or &lt;code&gt;changePassword()&lt;/code&gt; from the store. Components simply invoke these methods and react to the outcome.&lt;/p&gt;

&lt;p&gt;This keeps components lightweight while making business logic reusable across multiple views.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Clear Separation of Responsibilities
&lt;/h2&gt;

&lt;p&gt;A useful guideline is to divide responsibilities as follows:&lt;/p&gt;

&lt;p&gt;| Concern | Recommended Owner | | ------------------- | ------------------------------------------ | | User input | Signal Forms | | Validation | Signal Forms | | Loading remote data | Resource | | Business rules | SignalStore | | State mutations | SignalStore | | HTTP persistence | Service or repository invoked by the store |&lt;/p&gt;

&lt;p&gt;Following these boundaries results in components that focus on presentation, stores that encapsulate business logic, and Resources that handle server synchronization. Each part has a single responsibility, making the application easier to understand, test, and maintain as it grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from Classic NgRx to SignalStore
&lt;/h2&gt;

&lt;p&gt;Migrating to SignalStore doesn't require replacing an entire application's state management strategy overnight. In fact, most enterprise applications can adopt SignalStore incrementally while continuing to use the classic NgRx Store where it provides the greatest value.&lt;/p&gt;

&lt;p&gt;A practical migration strategy is to start with isolated features rather than the application's global state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the Classic Store for Global State
&lt;/h3&gt;

&lt;p&gt;Global concerns such as authentication, user sessions, application configuration, notifications, and cross-feature communication often continue to benefit from the centralized architecture of the classic NgRx Store.&lt;/p&gt;

&lt;p&gt;These areas typically rely on dispatched actions and event-driven workflows that remain well suited to Redux patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduce SignalStore for New Features
&lt;/h3&gt;

&lt;p&gt;New feature modules are excellent candidates for SignalStore.&lt;/p&gt;

&lt;p&gt;Instead of creating actions, reducers, selectors, and effects, developers can define state, computed values, and business methods in a single store. This reduces boilerplate while aligning the feature with Angular's signal-first architecture.&lt;/p&gt;

&lt;p&gt;Existing features can also be migrated gradually as they evolve, avoiding large-scale refactoring efforts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Move Component State First
&lt;/h3&gt;

&lt;p&gt;The easiest migration is often replacing component-local Observables and &lt;code&gt;BehaviorSubject&lt;/code&gt;s with Signals.&lt;/p&gt;

&lt;p&gt;Many components don't require a dedicated store at all. Converting temporary UI state to Signals simplifies the codebase immediately and familiarizes teams with Angular's reactive model before introducing SignalStore.&lt;/p&gt;

&lt;p&gt;Incremental adoption minimizes risk while allowing teams to modernize applications at a sustainable pace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for ABP Applications
&lt;/h2&gt;

&lt;p&gt;Angular 22's signal-first architecture aligns well with ABP's modular application model.&lt;/p&gt;

&lt;p&gt;Most ABP applications consist of independent feature modules such as Identity, Tenant Management, SaaS, or CMS. These modules naturally map to feature-scoped SignalStores, allowing state and business logic to remain encapsulated within each module. However, the full support will be introduced in the next version.&lt;/p&gt;

&lt;p&gt;As Angular continues investing in Signals, Resources, and Signal Forms, future ABP applications can increasingly rely on the framework's native reactive APIs instead of custom state management patterns.&lt;/p&gt;

&lt;p&gt;This doesn't diminish the importance of RxJS or the classic NgRx Store. RxJS remains an essential foundation of Angular's HTTP infrastructure and many third-party libraries, while the traditional Store continues to provide an excellent solution for complex global state management.&lt;/p&gt;

&lt;p&gt;Instead, Angular 22 encourages developers to use each reactive tool where it provides the greatest value.&lt;/p&gt;

&lt;p&gt;Whether you're upgrading an existing ABP application or starting a new project, adopting SignalStore for feature-level state can simplify development while remaining fully compatible with Angular's evolving ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Angular's evolution toward a signal-first architecture represents more than a new reactive API—it changes how applications should be designed.&lt;/p&gt;

&lt;p&gt;Rather than treating every piece of state as part of a centralized store, Angular now encourages developers to choose the appropriate abstraction for each responsibility. Plain Signals excel at local component state, SignalStore provides a lightweight solution for feature-level business logic, Resources simplify server synchronization, and Signal Forms modernize user input management.&lt;/p&gt;

&lt;p&gt;The classic NgRx Store continues to play an important role in large, event-driven applications, but it no longer needs to be the default choice for every state management scenario.&lt;/p&gt;

&lt;p&gt;By embracing these complementary tools, developers can build Angular applications that are simpler to maintain, require less boilerplate, and integrate naturally with the framework's latest capabilities.&lt;/p&gt;

&lt;p&gt;As Angular continues to evolve around Signals and fine-grained reactivity, adopting these patterns today will help applications remain aligned with the framework's direction while providing a solid foundation for future improvements.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>statemanagement</category>
      <category>bestpractices</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Working with Dapr Workflows in the ABP Framework</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Mon, 29 Jun 2026 17:39:42 +0000</pubDate>
      <link>https://dev.to/abp_io/working-with-dapr-workflows-in-the-abp-framework-29k8</link>
      <guid>https://dev.to/abp_io/working-with-dapr-workflows-in-the-abp-framework-29k8</guid>
      <description>&lt;h1&gt;
  
  
  Working with Dapr Workflows in the ABP Framework
&lt;/h1&gt;

&lt;p&gt;Most real business processes don't finish in a single request.&lt;/p&gt;

&lt;p&gt;An order gets placed, inventory gets checked, a payment gets charged, and the customer gets notified. Each step can fail, time out, or need a retry. And the whole thing has to survive a process restart without losing its place or charging someone twice.&lt;/p&gt;

&lt;p&gt;We usually solve this with a pile of queues, a state table, and a lot of defensive code to track where each process is. It works, but the business logic ends up scattered across handlers and database rows, and nobody can read the flow top to bottom anymore.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://abp.io/community/search?tag=elsa" rel="noopener noreferrer"&gt;I covered &lt;strong&gt;Elsa&lt;/strong&gt; in two earlier articles&lt;/a&gt; as one way to handle workflows in ABP. &lt;strong&gt;Dapr Workflow&lt;/strong&gt; takes a different path: instead of an in-app engine, the workflow engine runs in the &lt;a href="https://docs.dapr.io/concepts/dapr-services/sidecar/" rel="noopener noreferrer"&gt;&lt;strong&gt;Dapr sidecar&lt;/strong&gt;&lt;/a&gt;, and you write the process as ordinary C# code that Dapr makes durable. If the host crashes halfway through, the workflow picks up right where it left off.&lt;/p&gt;

&lt;p&gt;In this article, we'll build a small Dapr Workflow inside a fresh ABP project and run it end to end. By the time you reach the bottom, you should be able to copy the code, run it, and watch a workflow march through its steps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Versions matter here, because both ABP and Dapr move fast. This article is written in June 2026 against &lt;strong&gt;ABP 10.4&lt;/strong&gt; (.NET 10), &lt;strong&gt;Dapr 1.18&lt;/strong&gt; , and the &lt;strong&gt;&lt;code&gt;Dapr.Workflow&lt;/code&gt; 1.18.x&lt;/strong&gt; package. The &lt;code&gt;Dapr.Workflow&lt;/code&gt; package was rewritten in Dapr 1.17, so older tutorials you find online may use a different API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Dapr Workflow Actually Is?
&lt;/h2&gt;

&lt;p&gt;You define a &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/workflow/" rel="noopener noreferrer"&gt;&lt;strong&gt;workflow&lt;/strong&gt;&lt;/a&gt; that orchestrates a process, and &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/#workflows-and-activities" rel="noopener noreferrer"&gt;&lt;strong&gt;activities&lt;/strong&gt;&lt;/a&gt; that do the actual work (call a database, hit an API, send an email).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is orchestration rather than choreography:&lt;/strong&gt; one place drives the process, instead of services reacting to each other's events. The definitions live in your app, but the engine that executes them runs in the Dapr sidecar next to it.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph App["Your ABP App"]
        WF["OrderProcessingWorkflow"]
        ACT["Activities&amp;lt;br/&amp;gt;CheckInventory, ProcessPayment, NotifyCustomer"]
    end
    subgraph Sidecar["Dapr Sidecar"]
        ENGINE["Workflow Engine&amp;lt;br/&amp;gt;(durable execution)"]
    end
    STORE[("State Store&amp;lt;br/&amp;gt;Redis")]

    App --&amp;gt;|gRPC| Sidecar
    Sidecar --&amp;gt;|reads / writes history| STORE

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key idea is &lt;strong&gt;durable execution&lt;/strong&gt;. Dapr records every step to a state store, so the workflow can be replayed from history at any time. A crash, a deployment, or a scale-out event doesn't lose progress, and a workflow can run for seconds or for months.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ One rule follows from this: &lt;strong&gt;workflow code must be deterministic&lt;/strong&gt;. No &lt;code&gt;DateTime.Now&lt;/code&gt;, no random values, no direct I/O. Anything non-deterministic goes into an activity. Even logging is affected, so inside a workflow you use &lt;code&gt;context.CreateReplaySafeLogger&amp;lt;T&amp;gt;()&lt;/code&gt; instead of a normal logger, otherwise every replay repeats your log lines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Under the hood, this all runs on &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/" rel="noopener noreferrer"&gt;&lt;strong&gt;Dapr actors&lt;/strong&gt;&lt;/a&gt;, which is why the state store has to support actors. The good news is that the default local setup already handles this, as you'll see in a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Quick Note on ABP and Dapr
&lt;/h2&gt;

&lt;p&gt;ABP already ships a set of Dapr integration packages: &lt;code&gt;Volo.Abp.Dapr&lt;/code&gt; (the core package), &lt;code&gt;Volo.Abp.EventBus.Dapr&lt;/code&gt; and &lt;code&gt;Volo.Abp.AspNetCore.Mvc.Dapr.EventBus&lt;/code&gt; (distributed event bus over Dapr pub/sub), &lt;code&gt;Volo.Abp.Http.Client.Dapr&lt;/code&gt; (service invocation), and &lt;code&gt;Volo.Abp.DistributedLocking.Dapr&lt;/code&gt; (distributed locking). You can read all about them in the &lt;a href="https://abp.io/docs/latest/framework/dapr" rel="noopener noreferrer"&gt;ABP Dapr integration documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These cover pub/sub, service-to-service calls, and locking. &lt;strong&gt;Workflows are not part of ABP's Dapr integration&lt;/strong&gt; , and that's fine. Dapr Workflow has its own first-class .NET SDK (&lt;code&gt;Dapr.Workflow&lt;/code&gt;), and you plug it straight into your ABP app like any other .NET library. So in this article we use the Dapr SDK directly, inside an ABP startup template.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you'd like to see deeper Dapr integration in ABP, or you'd like us to build a dedicated piece around Dapr Workflow, feel free to open a new issue on the &lt;a href="https://github.com/abpframework/abp/issues" rel="noopener noreferrer"&gt;ABP GitHub repository&lt;/a&gt;. Telling us what you need is the best way to help us prioritize it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What We'll Build
&lt;/h2&gt;

&lt;p&gt;To keep this concrete, we'll build a small &lt;strong&gt;order processing&lt;/strong&gt; workflow, the classic example for this kind of thing.&lt;/p&gt;

&lt;p&gt;The workflow takes an order, checks inventory, charges the customer, then notifies them. If the item is out of stock, it stops early and returns a rejected result. Nothing fancy on the business side, but it's enough to show the parts that matter: how a workflow chains activities, how state survives across steps, and how you start and track an instance.&lt;/p&gt;

&lt;p&gt;Here's the flow we're aiming for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An order comes in with a product, a quantity, and a price&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check inventory&lt;/strong&gt; : if there isn't enough stock, reject the order and stop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process payment&lt;/strong&gt; : charge the customer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notify the customer&lt;/strong&gt; : let them know the order went through&lt;/li&gt;
&lt;li&gt;Return a final result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those steps will be an &lt;strong&gt;activity&lt;/strong&gt; , and the workflow is the code that orchestrates them. Let's set up the project and build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start, make sure you have these installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;.NET 10 SDK&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ABP CLI&lt;/strong&gt; (the current Studio CLI). Install it with &lt;code&gt;dotnet tool install -g Volo.Abp.Studio.Cli&lt;/code&gt; (or update with &lt;code&gt;dotnet tool update -g Volo.Abp.Studio.Cli&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; , running on your machine&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dapr.io/getting-started/" rel="noopener noreferrer"&gt;&lt;strong&gt;Dapr CLI&lt;/strong&gt; , initialized once with &lt;code&gt;dapr init&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last step matters. When you run &lt;code&gt;dapr init&lt;/code&gt; in self-hosted mode, Dapr pulls a few containers (including Redis) and writes a default &lt;code&gt;statestore.yaml&lt;/code&gt; component. That default state store already has &lt;code&gt;actorStateStore: "true"&lt;/code&gt; set, which is exactly what Dapr Workflow needs. So once &lt;code&gt;dapr init&lt;/code&gt; finishes, you can run workflows locally with zero extra configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdapr-init-run-result.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdapr-init-run-result.png" alt="dapr-init-run-result"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; If you ever swap the default Redis store for your own component, double-check that it sets &lt;code&gt;actorStateStore: "true"&lt;/code&gt;. Without it, workflows silently fail to start, and it's the line people forget most often.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Create the Project
&lt;/h2&gt;

&lt;p&gt;In this article I'll create a new layered solution with &lt;strong&gt;EF Core&lt;/strong&gt; as the database provider, using the ABP CLI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you already have an ABP project, you don't need a new one. You can apply the following steps to your existing solution and skip this section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Create a new solution named &lt;code&gt;DaprWorkflowDemo&lt;/code&gt; (or whatever you want):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;abp new DaprWorkflowDemo

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the download finishes, your project boilerplate is ready. Open the solution in your IDE and run the &lt;code&gt;DaprWorkflowDemo.Web&lt;/code&gt; project once to confirm the app starts and the UI works.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Since, we have created the solution via ABP Studio CLI, it automatically runs the initial-tasks, which init database, seed initial data and run &lt;code&gt;abp install-libs&lt;/code&gt; command, so, no need run the *&lt;em&gt;DbMigrator&lt;/em&gt; project.&lt;/p&gt;

&lt;p&gt;Default admin username is &lt;strong&gt;admin&lt;/strong&gt; and the password is &lt;strong&gt;1q2w3E&lt;/strong&gt; *. You can use these credentials to login...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We'll do all the workflow work inside the &lt;code&gt;DaprWorkflowDemo.Web&lt;/code&gt; project, since that's the running host where the workflow engine connects to the sidecar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the Dapr.Workflow Package
&lt;/h2&gt;

&lt;p&gt;Open a terminal in the &lt;code&gt;DaprWorkflowDemo.Web&lt;/code&gt; project folder and add the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Dapr.Workflow

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;-&amp;gt; &lt;strong&gt;This single package gives you everything:&lt;/strong&gt; the base &lt;code&gt;Workflow&amp;lt;TInput, TOutput&amp;gt;&lt;/code&gt; and &lt;code&gt;WorkflowActivity&amp;lt;TInput, TOutput&amp;gt;&lt;/code&gt; types, the &lt;code&gt;AddDaprWorkflow&lt;/code&gt; registration helper, and the &lt;code&gt;DaprWorkflowClient&lt;/code&gt; you use to start and query workflows from code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the Workflow and Its Activities
&lt;/h2&gt;

&lt;p&gt;Now let's write the order processing flow we sketched out earlier.&lt;/p&gt;

&lt;p&gt;First, create a &lt;code&gt;Workflows&lt;/code&gt; folder in the &lt;code&gt;DaprWorkflowDemo.Web&lt;/code&gt; project. We'll keep everything there for simplicity.&lt;/p&gt;

&lt;p&gt;Every input and output in a workflow gets serialized to the state store, so the types you pass around should be simple, JSON-friendly records ( &lt;strong&gt;&lt;em&gt;ensure they are serializable!&lt;/em&gt;&lt;/strong&gt; ). Let's define them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;DaprWorkflowDemo.Web.Workflows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;TotalPrice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;InventoryResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;InStock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the workflow itself. A workflow derives from &lt;code&gt;Workflow&amp;lt;TInput, TOutput&amp;gt;&lt;/code&gt; and reads top to bottom like a normal method, even though every step is durably persisted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapr.Workflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;DaprWorkflowDemo.Web.Workflows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderProcessingWorkflow&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Workflow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WorkflowContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderPayload&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateReplaySafeLogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderProcessingWorkflow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Starting order {OrderId}: {Quantity} x {ProductName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 1. Check inventory&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InventoryResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CheckInventoryActivity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InStock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order {OrderId} rejected: out of stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Rejected: out of stock"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Process the payment&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPaymentActivity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Notify the customer&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotifyCustomerActivity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order {OrderId} completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Completed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of things worth pointing out here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CallActivityAsync&lt;/code&gt; does not invoke the activity directly. It schedules the work with the workflow engine, which records the result once the activity completes. If the process dies right after the payment step, Dapr replays the workflow, feeds it the already-recorded results for the completed steps, and resumes at the notification step. The customer never gets charged twice. This is the &lt;strong&gt;task chaining&lt;/strong&gt; pattern.&lt;/li&gt;
&lt;li&gt;Notice the replay-safe logger too. Because the engine replays the workflow to rebuild its state, a normal logger would print the same lines over and over. &lt;code&gt;context.CreateReplaySafeLogger&amp;lt;T&amp;gt;()&lt;/code&gt; logs only on the first real pass.&lt;/li&gt;
&lt;li&gt;Now the activities. An activity is where the real work happens, and the only place you're allowed to be non-deterministic. It derives from &lt;code&gt;WorkflowActivity&amp;lt;TInput, TOutput&amp;gt;&lt;/code&gt; and supports constructor injection, so you can pull in your ABP services, repositories, or any registered dependency:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapr.Workflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;DaprWorkflowDemo.Web.Workflows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckInventoryActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WorkflowActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InventoryResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CheckInventoryActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CheckInventoryActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CheckInventoryActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InventoryResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WorkflowActivityContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderPayload&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Checking inventory for {ProductName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Pretend we queried a stock service or a repository here.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inStock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InventoryResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inStock&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessPaymentActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WorkflowActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessPaymentActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ProcessPaymentActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessPaymentActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WorkflowActivityContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderPayload&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Charging {TotalPrice:C} for order {OrderId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Call your real payment provider here.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotifyCustomerActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WorkflowActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NotifyCustomerActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;NotifyCustomerActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NotifyCustomerActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WorkflowActivityContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderPayload&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Notifying customer about order {OrderId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Send an email, push a notification, publish an event, etc.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each activity is isolated, so Dapr can retry a failed one without re-running the whole workflow. The two activities that don't return anything useful use &lt;code&gt;object?&lt;/code&gt; as their output type and return &lt;code&gt;null&lt;/code&gt;. That's why the workflow calls them with the non-generic &lt;code&gt;CallActivityAsync&lt;/code&gt;, which ignores the result.&lt;/p&gt;

&lt;p&gt;Here's the shape of the process we just wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Start workflow&amp;lt;br/&amp;gt;(OrderPayload)"] --&amp;gt; B["CheckInventoryActivity"]
    B --&amp;gt;|In stock| C["ProcessPaymentActivity"]
    B --&amp;gt;|Out of stock| R["Return: Rejected"]
    C --&amp;gt; D["NotifyCustomerActivity"]
    D --&amp;gt; E["Return: Completed"]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Register the Workflow
&lt;/h2&gt;

&lt;p&gt;Workflows and activities need to be registered so the engine knows about them. Open your &lt;code&gt;DaprWorkflowDemoWebModule&lt;/code&gt; class and register them in &lt;code&gt;ConfigureServices&lt;/code&gt;. Most of the existing code is abbreviated for simplicity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;DaprWorkflowDemo.Web.Workflows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapr.Workflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServiceConfigurationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hostingEnvironment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetHostingEnvironment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConfiguration&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// ... existing ABP configuration ...&lt;/span&gt;

    &lt;span class="c1"&gt;//Configure Dapr Workflows...&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDaprWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterWorkflow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderProcessingWorkflow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CheckInventoryActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessPaymentActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NotifyCustomerActivity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;AddDaprWorkflow&lt;/code&gt; does two things for us. It registers a background worker that connects to the sidecar's workflow engine and hosts your workflow definitions, and it registers a &lt;code&gt;DaprWorkflowClient&lt;/code&gt; in the dependency injection container so you can start and query workflows from your own code later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's all the wiring. There's no component YAML to write, because Dapr ships a built-in workflow component named &lt;code&gt;dapr&lt;/code&gt; that runs on top of the actor state store we already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run It With the Dapr Sidecar
&lt;/h2&gt;

&lt;p&gt;Here's the part that's different from a normal &lt;code&gt;dotnet run&lt;/code&gt;. The workflow engine lives in the Dapr sidecar, so the app has to run &lt;strong&gt;alongside&lt;/strong&gt; a sidecar. The Dapr CLI handles that for us.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this section, I assume that you already run &lt;code&gt;dapr init&lt;/code&gt; command before, as explained above. If you haven't run it yet, please first run it and then follow the instructions/commands below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, make sure your database is migrated (run &lt;code&gt;DaprWorkflowDemo.DbMigrator&lt;/code&gt; if you haven't). Then, from the &lt;code&gt;DaprWorkflowDemo.Web&lt;/code&gt; project folder, start the app with Dapr:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dapr run &lt;span class="nt"&gt;--app-id&lt;/span&gt; dapr-workflow-demo &lt;span class="nt"&gt;--dapr-http-port&lt;/span&gt; 3500 &lt;span class="nt"&gt;--&lt;/span&gt; dotnet run

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on this command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--app-id&lt;/code&gt; is the identity of your app within Dapr. We'll use it nowhere else in this example, but Dapr needs it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--dapr-http-port 3500&lt;/code&gt; pins the sidecar's HTTP port so we know where to send requests. You can leave it out and let Dapr pick one, but pinning it keeps the next step simple.&lt;/li&gt;
&lt;li&gt;Everything after &lt;code&gt;--&lt;/code&gt; is the command Dapr runs for your app. &lt;code&gt;dapr run&lt;/code&gt; injects the sidecar's connection details (like the gRPC port) as environment variables, and the &lt;code&gt;Dapr.Workflow&lt;/code&gt; worker reads them automatically to connect to the engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice we don't pass &lt;code&gt;--app-port&lt;/code&gt; here. That flag is only needed when Dapr has to call &lt;strong&gt;into&lt;/strong&gt; your app (for pub/sub or service invocation). For workflows, your app connects &lt;strong&gt;out&lt;/strong&gt; to the sidecar over gRPC, so we don't need it for this scenario.&lt;/p&gt;

&lt;p&gt;Once it's running, you'll see both the ABP app logs and the Dapr sidecar logs in the same terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does It Actually Work?
&lt;/h2&gt;

&lt;p&gt;The quickest way to test is to talk to the sidecar's &lt;strong&gt;Workflow management HTTP API&lt;/strong&gt; directly. This hits Dapr, not your app, which makes it a clean smoke test with no extra endpoint code.&lt;/p&gt;

&lt;p&gt;Start a workflow instance. The component name is &lt;code&gt;dapr&lt;/code&gt; (the built-in one), the workflow name is the class name, and we pass our own instance ID so it's easy to query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=order-001"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"OrderId":"order-001","ProductName":"Mechanical Keyboard","Quantity":2,"TotalPrice":59.90}'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request body is the workflow input, and Dapr passes it straight through to your &lt;code&gt;OrderPayload&lt;/code&gt;. You should get a &lt;code&gt;202 Accepted&lt;/code&gt; back with the instance ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"instanceID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-001"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now query the status of that instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:3500/v1.0/workflows/dapr/order-001"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the workflow finishes, you'll see a &lt;code&gt;COMPLETED&lt;/code&gt; status along with the serialized output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instanceID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workflowName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderProcessingWorkflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-29T15:30:15.038490Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastUpdatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-29T15:30:15.360885500Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"runtimeStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMPLETED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dapr.workflow.input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ProductName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Mechanical Keyboard&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Quantity&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:2,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;OrderId&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;order-001&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;TotalPrice&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:59.9}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dapr.workflow.output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;orderId&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;order-001&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Completed&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you check the terminal, you'll also see the log lines from the workflow and each activity in order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdapr-workflow-response.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fabpframework%2Fabp%2Fdev%2Fdapr-workflow-response.png" alt="dapr-workflow-response"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same management API also lets you &lt;code&gt;terminate&lt;/code&gt;, &lt;code&gt;pause&lt;/code&gt;, &lt;code&gt;resume&lt;/code&gt;, and &lt;code&gt;purge&lt;/code&gt; instances, and &lt;code&gt;raiseEvent&lt;/code&gt; to send external events into a waiting workflow. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Permanently delete a finished workflow's state&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:3500/v1.0/workflows/dapr/order-001/purge"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;DaprWorkflowClient&lt;/code&gt; exposes the same operations in code (terminating, suspending and resuming, purging, and raising external events on an instance), which is the way to go for anything beyond a quick manual test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggering Workflows From Your ABP Code
&lt;/h2&gt;

&lt;p&gt;Hitting the sidecar API by hand is great for a quick check, but in a real app you'll start workflows from your own code, and this is the recommended path. That's what the &lt;code&gt;DaprWorkflowClient&lt;/code&gt; is for, and &lt;code&gt;AddDaprWorkflow&lt;/code&gt; already registered it for you.&lt;/p&gt;

&lt;p&gt;You can inject it anywhere, for example into a controller or an application service. Here's a minimal controller in the &lt;code&gt;DaprWorkflowDemo.Web&lt;/code&gt; project that starts an order and reads its status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.Tasks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;DaprWorkflowDemo.Web.Workflows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapr.Workflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Mvc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;DaprWorkflowDemo.Web.Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;DaprWorkflowClient&lt;/span&gt; &lt;span class="n"&gt;_workflowClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DaprWorkflowClient&lt;/span&gt; &lt;span class="n"&gt;workflowClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_workflowClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workflowClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderPayload&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;instanceId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_workflowClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScheduleNewWorkflowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderProcessingWorkflow&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Accepted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/api/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;instanceId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{instanceId}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetStatusAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_workflowClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetWorkflowStateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;RuntimeStatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RuntimeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadOutputAs&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ScheduleNewWorkflowAsync&lt;/code&gt; returns immediately and the workflow runs in the background, so this fits the asynchronous request pattern nicely: return &lt;code&gt;202 Accepted&lt;/code&gt; and let the client poll the status endpoint.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One ABP-specific thing to keep in mind: ABP enforces antiforgery validation for unsafe HTTP methods on cookie-authenticated requests. Server-to-server or &lt;code&gt;curl&lt;/code&gt; calls without an auth cookie usually pass straight through, but if you call the &lt;code&gt;POST&lt;/code&gt; endpoint from a logged-in browser session and get a &lt;code&gt;400&lt;/code&gt; antiforgery error, you can relax the auto validation for this controller through &lt;code&gt;AbpAntiForgeryOptions&lt;/code&gt;, the same way the Elsa articles did for the Elsa endpoints.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;We built a simple linear flow, but &lt;strong&gt;Dapr Workflow&lt;/strong&gt; supports the patterns you'll actually need in production, all in plain C#:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out / fan-in&lt;/strong&gt; : schedule many activities in parallel and aggregate the results (it's just &lt;code&gt;Select&lt;/code&gt; plus &lt;code&gt;Task.WhenAll&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External events&lt;/strong&gt; : pause a workflow until a human approves something or another system calls back. This is great for approval flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timers&lt;/strong&gt; : durably wait for minutes, days, or months without holding a thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Child workflows&lt;/strong&gt; : break a big process into smaller workflows with their own history and status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry policies&lt;/strong&gt; : give an activity an exponential backoff policy so transient failures recover on their own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dapr Workflow&lt;/strong&gt; gives you durable execution for long-running processes without bolting a heavy orchestration engine into your code. The process is plain C# that reads top to bottom, Dapr makes it fault-tolerant by replaying from the state store, and the orchestration stays deterministic while the side effects live in activities.&lt;/p&gt;

&lt;p&gt;The nice part for us is that none of this fights with ABP. You create a normal ABP solution, add the &lt;code&gt;Dapr.Workflow&lt;/code&gt; package, register your workflows in a module, and run with &lt;code&gt;dapr run&lt;/code&gt;. ABP's own Dapr packages still cover pub/sub, service invocation, and locking, so you can mix all of these in the same solution when you need them.&lt;/p&gt;

&lt;p&gt;All the code in this article is self-contained, so you can copy it into a fresh ABP project and follow along from top to bottom.&lt;/p&gt;

&lt;p&gt;Thanks for reading, see you in the next one!&lt;/p&gt;

</description>
      <category>workflow</category>
      <category>abpframework</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:59:05 +0000</pubDate>
      <link>https://dev.to/abp_io/customizing-the-abp-framework-a-developers-guide-to-leptonx-theme-overrides-in-angular-and-the-59le</link>
      <guid>https://dev.to/abp_io/customizing-the-abp-framework-a-developers-guide-to-leptonx-theme-overrides-in-angular-and-the-59le</guid>
      <description>&lt;h1&gt;
  
  
  Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI
&lt;/h1&gt;

&lt;p&gt;Enterprise ASP.NET Boilerplate (ABP) projects rarely stay with default theme behavior for long. At some point, teams need stricter brand alignment, user experience (UX) consistency across modules, or product-specific shell behavior that goes beyond palette and typography tweaks.&lt;/p&gt;

&lt;p&gt;This article explains a practical way to customize the LeptonX theme in Angular projects through two primary layers :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Style Overriding:&lt;/strong&gt; Utilizing design tokens, global CSS custom properties (variables), and component-level styling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element Overriding:&lt;/strong&gt; Replacing or extending UI fragments and layout pieces using ABP's built-in services.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, we connect this customization mindset to ABP’s new React direction, where application development teams own more of the user interface (UI) implementation directly from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Overriding Matters in Real ABP Solutions
&lt;/h2&gt;

&lt;p&gt;In enterprise software engineering, frontend customization is not a cosmetic task. Instead, it directly supports core technical and architectural goals :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brand System Compliance:&lt;/strong&gt; Enforcing strict color palettes, layouts, and typography across tenant-facing portals and internal back-office administration pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility (a11y) Improvements:&lt;/strong&gt; Optimizing focus states, color contrast ratios, screen reader compatibility, and keyboard navigation to meet WCAG standards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product Differentiation:&lt;/strong&gt; Structuring distinct top-level layouts, sidebar behavior, and navigation elements to separate multiple products within the same suite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational Usability:&lt;/strong&gt; Reorganizing application spaces to match domain-specific workflows and simplify intensive data-entry tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To avoid building fragile CSS overrides that break during framework updates, development teams must follow a strict, highly structured hierarchy of customization :&lt;/p&gt;

&lt;p&gt;| Level | Customization Type | Technical Mechanism | Strategic Role | | :---: | :--- | :--- | :--- | | &lt;strong&gt;1&lt;/strong&gt; | &lt;strong&gt;Token-Level Variables&lt;/strong&gt; | CSS Custom Properties | 🛡️ &lt;em&gt;First Line of Defense&lt;/em&gt; | | &lt;strong&gt;2&lt;/strong&gt; | &lt;strong&gt;Component-Style Patch&lt;/strong&gt; | Class-Based Overrides | 🎨 &lt;em&gt;Moderate Visual Tweaks&lt;/em&gt; | | &lt;strong&gt;3&lt;/strong&gt; | &lt;strong&gt;Element Replacement&lt;/strong&gt; | ReplaceableComponents | 🏗️ &lt;em&gt;Deep Structural Overrides&lt;/em&gt; |&lt;/p&gt;

&lt;p&gt;Adhering to this hierarchy reduces "style debt" and ensures that theme upgrades remain manageable throughout the application lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Style Overriding in LeptonX (Angular)
&lt;/h3&gt;

&lt;p&gt;Style overriding is the safest and most maintainable way to alter your application's presentation layer. The LeptonX engine relies heavily on CSS custom properties (variables) defined at the &lt;code&gt;:root&lt;/code&gt; level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing Brand Colors and Typography Tokens
&lt;/h3&gt;

&lt;p&gt;To modify the default colors and branding assets, developers can define custom properties within the global &lt;code&gt;src/styles.scss&lt;/code&gt; file :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Set the primary brand color used on active elements, buttons, and focuses */&lt;/span&gt;
  &lt;span class="py"&gt;--lpx-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e3a8a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Set the physical paths for the application logos */&lt;/span&gt;
  &lt;span class="py"&gt;--lpx-logo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/assets/images/logo.png')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--lpx-logo-icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/assets/images/logo-icon.png')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Displayed when sidebar is collapsed */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For applications utilizing multi-theme layouts (such as LeptonX Pro's Light, Dark, or Dim modes), variables can be scoped under individual theme classes to dynamically swap brand colors or assets :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Scoping theme-specific logos to prevent visibility issues on dark backgrounds */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="nc"&gt;.lpx-theme-dark&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="nc"&gt;.lpx-theme-dim&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--lpx-logo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/assets/images/logo-light.png')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--lpx-logo-icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/assets/images/logo-icon-light.png')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Solving the "Visual Branding Blink" on Initial Page Load
&lt;/h4&gt;

&lt;p&gt;A common issue in production occurs when the default LeptonX logo is briefly displayed on screen before the client browser parses the custom stylesheet. This latency creates a noticeable "blink" or flicker.&lt;/p&gt;

&lt;p&gt;To eliminate this rendering gap, bypass the CSS variable load phase by replacing the physical logo assets inside the web host project's public directory. Write your custom branding files directly to &lt;code&gt;/images/logo/leptonx/logo-light.png&lt;/code&gt; inside the server's public folder. Because the fallback variable defaults directly to this location, the client browser displays the custom logo asset immediately without waiting to parse the custom CSS rules.&lt;/p&gt;

&lt;p&gt;Additionally, note that styles registered solely in the application's global &lt;code&gt;styles.scss&lt;/code&gt; may fail to apply to the &lt;strong&gt;Account Layout&lt;/strong&gt; (such as the standard login page) because it compiles within an isolated module lifecycle. To ensure your styling overrides apply globally, register the assets and styles in the Virtual File System (VFS) of the.NET backend host, making them universally accessible across all client routing contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Element Overriding in LeptonX (Angular)
&lt;/h3&gt;

&lt;p&gt;When CSS modifications cannot support your required user experience (such as adding search interfaces, custom profile controls, or custom action layouts), teams must override the underlying UI elements.&lt;/p&gt;

&lt;p&gt;ABP provides the &lt;code&gt;ReplaceableComponentsService&lt;/code&gt; to dynamically replace pre-built layout pieces with custom, project-owned Angular components without breaking core module logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting the Mobile User Profile Freeze
&lt;/h3&gt;

&lt;p&gt;In compiled editions of the LeptonX Lite layout library (specifically versions 3.1.x through 4.3.1), developers have identified a rendering bug affecting mobile layouts. When a user logs in via a mobile device and taps the profile dropdown menu, the page freezes. Instead of displaying the profile options, the sidebar area recursively renders a duplicate copy of the active route page. This layout loop completely breaks navigation until the page is refreshed.&lt;/p&gt;

&lt;p&gt;The root cause is a layout bug inside the compiled LeptonX library template (&lt;code&gt;mn-user-profile.component.html&lt;/code&gt;), where the template markup is wrapped inside an &lt;code&gt;&amp;lt;ng-component&amp;gt;&lt;/code&gt; tag instead of a structurally neutral &lt;code&gt;&amp;lt;ng-container&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;To resolve this issue, you can implement a custom component replacement :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Generate a custom mobile profile component using the Angular CLI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement the component template, ensuring the wrapper elements utilize &lt;code&gt;&amp;lt;ng-container&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;ng-component&amp;gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inject the &lt;code&gt;ReplaceableComponentsService&lt;/code&gt; into your root &lt;code&gt;app.component.ts&lt;/code&gt; to swap the underlying component keys during application bootstrap :&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Template Context: From LeptonX Demo Setup to Real ABP Application Templates
&lt;/h3&gt;

&lt;p&gt;When transitioning customized designs from local prototypes to production environments, development teams must choose between two operating modes :&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Operational Mode&lt;/strong&gt; | &lt;strong&gt;Core Architecture&lt;/strong&gt; | &lt;strong&gt;Rationale &amp;amp; Trade-offs&lt;/strong&gt; | | --- | --- | --- | | &lt;strong&gt;Standard Template Mode&lt;/strong&gt; | Consumes LeptonX packages as standard dependencies (&lt;code&gt;@abp/ng.theme.lepton-x&lt;/code&gt;) from npm registries. All overrides are applied at the application layer. | &lt;strong&gt;Highly Recommended.&lt;/strong&gt; Keeps local project codebases clean, simplifies dependency updates, and avoids style debt. | | &lt;strong&gt;Source-Inspection Mode&lt;/strong&gt; | Utilizes the ABP CLI &lt;code&gt;get-source&lt;/code&gt; command to download the raw theme code and configure temporary local path aliases. | &lt;strong&gt;Diagnostic Only.&lt;/strong&gt; Best used for deep debugging, prototyping layout behaviors, or tracing framework-level bugs. |&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolving Strict MIME Type CSS Loading Exceptions
&lt;/h3&gt;

&lt;p&gt;During local development or initial production deployments of LeptonX Lite Angular applications, browsers may refuse to apply the theme's styles. This issue manifests as a console exception:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Refused to apply style from 'http://localhost:4200/bootstrap-dim.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This error occurs when the browser requests static layout stylesheets from paths that do not exist, causing the back-end host to return a default 404 HTML fallback page. To resolve this, run the installation command in your client-side workspace :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;abp install-libs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command forces the ABP CLI to parse package dependencies, copy the compiled stylesheets directly into the physical output directories, and make them available to the web server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Implementation: Integrating Theme Source Code and the Upgrade Trade-Off
&lt;/h3&gt;

&lt;p&gt;For complex enterprise scenarios requiring structural changes that cannot be achieved via standard token configurations or component replacements, developers have the option to bypass compiled packages entirely and integrate the theme’s raw source code.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Retrieve the Source Code
&lt;/h3&gt;

&lt;p&gt;ABP Commercial customers have full access to the complete source code of the LeptonX Pro theme. This can be downloaded directly through the ABP Suite user interface or by executing the following command in the ABP CLI within your project directory :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;abp get-source Volo.Abp.LeptonXTheme

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command downloads the raw C# and Angular source files directly into your local solution structure. Once downloaded, you can modify the underlying HTML templates, restructure Angular modules, and alter core layout scripts to meet your product requirements.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Upgrade Warning: Maintenance Overhead and Style Debt
&lt;/h4&gt;

&lt;p&gt;While direct access to the source code provides complete design freedom, it comes with a major warning regarding long-term maintenance :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bypassing the Update Stream:&lt;/strong&gt; Once you replace official package references (such as &lt;code&gt;@volosoft/abp.ng.theme.lepton-x&lt;/code&gt; or NuGet packages) with local project references, your application is disconnected from the automatic update pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual Merge Burden:&lt;/strong&gt; When Volosoft releases framework updates, security patches, or compatibility fixes (such as aligning with newer Angular or.NET compiler baselines), these updates will not automatically apply to your customized code. Your team must manually compare, diff, and merge upstream changes, which can introduce regressions and increase technical debt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VFS and APIs as the First Line of Defense:&lt;/strong&gt; Before choosing a full source code integration, try using the Virtual File System (VFS) on the backend or standard component replacement APIs in the frontend to override only the specific elements you need to change. This allows you to customize the UI while keeping the rest of your theme packages fully upgradeable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Connecting the Mindset to ABP’s New React Era
&lt;/h3&gt;

&lt;p&gt;The introduction of the React UI option in ABP 10.4 represents a major architectural shift. While the Angular implementation relies on structured layout packages and runtime component overrides, the React architecture prioritizes &lt;strong&gt;direct developer ownership&lt;/strong&gt; of the presentation layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    %% Styling
    classDef react fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#0d47a1;
    classDef dotnet fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px,color:#4a148c;
    classDef proxy fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,color:#e65100;
    classDef tool fill:#f5f5f5,stroke:#757575,color:#333;

    %% React App Box
    subgraph ReactApp ["React App Repository"]
        C1["Custom Business Components&amp;lt;br&amp;gt;&amp;lt;small&amp;gt;(Local Source Code)&amp;lt;/small&amp;gt;"]:::react
        C2["TanStack Router &amp;amp; Query&amp;lt;br&amp;gt;&amp;lt;small&amp;gt;(Type-Safe Client Routes)&amp;lt;/small&amp;gt;"]:::react

        T1["Vite Dev Server &amp;amp; Bundling&amp;lt;br&amp;gt;&amp;lt;small&amp;gt;(Fast HMR, Vitest)&amp;lt;/small&amp;gt;"]:::tool
        T2["Tailwind CSS / shadcn/ui&amp;lt;br&amp;gt;&amp;lt;small&amp;gt;(Accessible UI Components)&amp;lt;/small&amp;gt;"]:::tool

        C1 --&amp;gt; C2
        T1 --&amp;gt; T2
    end

    %% Backend Box
    subgraph NetCore ["ASP.NET Core Web API Host"]
        P1["Dynamic API Client Proxies&amp;lt;br&amp;gt;&amp;lt;small&amp;gt;(Auto-Generated Endpoints)&amp;lt;/small&amp;gt;"]:::proxy
        A1["ABP Admin Console&amp;lt;br&amp;gt;&amp;lt;small&amp;gt;(Delivered via NuGet)&amp;lt;/small&amp;gt;"]:::dotnet

        P1 &amp;lt;==&amp;gt; A1
    end

    %% Inter-Repository Flow
    ReactApp -- "Generates Dynamic Proxies" --&amp;gt; P1

    %% Layout Tweaks
    style ReactApp fill:#fafafa,stroke:#1e88e5,stroke-width:1px,stroke-dasharray: 5 5;
    style NetCore fill:#fafafa,stroke:#8e24aa,stroke-width:1px,stroke-dasharray: 5 5;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Stays Consistent vs. What Changes
&lt;/h3&gt;

&lt;p&gt;Understanding how patterns transfer between frameworks is key for teams migrating to the React UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What Stays Consistent:&lt;/strong&gt; Core DDD infrastructure, backend integration, dynamic API proxy generation, multi-tenancy models, and permission-aware routing configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What Changes:&lt;/strong&gt; Direct ownership of page layouts, faster iteration of UI composition, and modern utility-first styling tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A New Frontend Philosophy
&lt;/h3&gt;

&lt;p&gt;In the Angular model, developers import pre-built layouts from compiled packages and selectively override elements using classes or replacing components. While structured, this approach can sometimes feel like "fighting" the framework.&lt;/p&gt;

&lt;p&gt;The React UI model, by contrast, gives developers direct control over the UI components from day one. Standard administrative pages (such as Identity, Tenants, and Settings) are managed separately by the &lt;strong&gt;ABP Admin Console&lt;/strong&gt; on the back-end host, while all application layouts and views remain locally in your React project.&lt;/p&gt;

&lt;p&gt;Built with modern tools like &lt;strong&gt;Vite&lt;/strong&gt; , &lt;strong&gt;Tailwind CSS&lt;/strong&gt; , and &lt;strong&gt;shadcn/ui&lt;/strong&gt; , developers can customize and extend components directly in their local source files without needing complex overriding wrappers.&lt;/p&gt;

&lt;p&gt;Additionally, because the layout and page templates reside in local source directories rather than compiled packages, this architecture is highly optimized for AI-driven development. Automated coding agents (such as the ABP Studio AI Agent) can easily inspect and modify local layouts, run API proxy generation, and deploy updates quickly.&lt;/p&gt;

&lt;p&gt;Whether your enterprise solution leverages the structured, component-driven architecture of ABP's Angular UI or is stepping into the modern, developer-owned era of the Vite-powered React UI , establishing an intentional, upgrade-safe customization strategy is crucial. By resolving design changes through token-level custom properties first, documenting structural element overrides, and preparing public-facing technical resources to be highly citable by conversational search agents , development teams can insulate their codebases from technical debt. Ultimately, the transition from rigid theme packages to direct frontend ownership not only streamlines day-to-day software delivery but also ensures that your application framework remains flexible, performant, and visible in an AI-driven ecosystem.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>customization</category>
      <category>theme</category>
      <category>abpframework</category>
    </item>
    <item>
      <title>AI Isn’t Replacing Developers — It’s Changing What Good Developers Spend Time On</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Thu, 25 Jun 2026 07:37:37 +0000</pubDate>
      <link>https://dev.to/abp_io/ai-isnt-replacing-developers-its-changing-what-good-developers-spend-time-on-44ab</link>
      <guid>https://dev.to/abp_io/ai-isnt-replacing-developers-its-changing-what-good-developers-spend-time-on-44ab</guid>
      <description>&lt;p&gt;A lot of the current AI discussion in software development swings between two extremes: either AI will write everything, or it is just autocomplete with better marketing.&lt;/p&gt;

&lt;p&gt;Neither view is especially useful.&lt;/p&gt;

&lt;p&gt;What the evidence shows is more practical: AI coding tools can improve developer throughput on certain tasks, especially repetitive work, scaffolding, and first drafts. But they do not remove the need for developers. In many teams, they actually create a new category of work around review, verification, security, and long-term maintainability.&lt;/p&gt;

&lt;p&gt;That is the real story. AI is not replacing developers. It is changing what developers do, what teams optimize for, and where engineering judgment matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The productivity gains are real, but they are not magic
&lt;/h2&gt;

&lt;p&gt;There is enough data now to move beyond hot takes.&lt;/p&gt;

&lt;p&gt;Across multiple studies and industry reports, AI coding assistants show measurable productivity gains, but those gains are usually modest rather than transformational:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A BlueOptima analysis across 30,000 developers in 18 enterprises reported an average productivity uplift of 5.4%, with the most active users seeing gains closer to 20%.&lt;/li&gt;
&lt;li&gt;An open source study found roughly a 6.5% project-level productivity increase after Copilot adoption.&lt;/li&gt;
&lt;li&gt;GitHub survey data from more than 2,000 developers showed strong perceived benefits: improved flow, less mental drain on repetitive tasks, and greater job satisfaction.&lt;/li&gt;
&lt;li&gt;A longitudinal study from a large public-sector engineering organization found that developers using Copilot were already highly active, and while they reported productivity improvements, commit-based metrics did not show a statistically significant post-adoption jump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters.&lt;/p&gt;

&lt;p&gt;Perceived productivity and actual output are not always the same thing. Developers may feel faster because they spend less time on boilerplate, search, or syntax recall. That feeling is valuable. Less friction often means better focus. But it does not automatically translate into dramatically more shipped business value.&lt;/p&gt;

&lt;p&gt;In other words, AI helps, but it does not suspend the usual constraints of software delivery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unclear requirements still slow teams down&lt;/li&gt;
&lt;li&gt;poor architecture still creates drag&lt;/li&gt;
&lt;li&gt;bad testing practices still leak defects&lt;/li&gt;
&lt;li&gt;messy codebases are still messy codebases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your delivery bottleneck is typing, AI looks revolutionary. If your bottleneck is product ambiguity, compliance, integration complexity, or production risk, AI helps less than the marketing suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI coding tools are actually good at
&lt;/h2&gt;

&lt;p&gt;The strongest use case for AI in development is not autonomous software engineering. It is acceleration of narrow, well-bounded tasks.&lt;/p&gt;

&lt;p&gt;AI coding assistants are usually good at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generating boilerplate&lt;/li&gt;
&lt;li&gt;filling in repetitive CRUD patterns&lt;/li&gt;
&lt;li&gt;writing simple tests and test skeletons&lt;/li&gt;
&lt;li&gt;suggesting refactors&lt;/li&gt;
&lt;li&gt;producing documentation drafts&lt;/li&gt;
&lt;li&gt;translating between languages or frameworks&lt;/li&gt;
&lt;li&gt;helping developers recall APIs and syntax&lt;/li&gt;
&lt;li&gt;creating a first pass for routine utility code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why many developers genuinely like these tools. They reduce low-value friction.&lt;/p&gt;

&lt;p&gt;A practical example:&lt;/p&gt;

&lt;p&gt;A developer building an ABP-based application might use AI to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scaffold DTO mappings&lt;/li&gt;
&lt;li&gt;draft validation rules&lt;/li&gt;
&lt;li&gt;generate basic unit test cases&lt;/li&gt;
&lt;li&gt;create repository query examples&lt;/li&gt;
&lt;li&gt;summarize a service class before refactoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are useful accelerators. But the same tool is much less reliable when asked to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether a module boundary is correct&lt;/li&gt;
&lt;li&gt;how to model a permission system&lt;/li&gt;
&lt;li&gt;what tradeoff to make between consistency and performance&lt;/li&gt;
&lt;li&gt;how multi-tenancy affects data access rules&lt;/li&gt;
&lt;li&gt;which abstraction will still be maintainable a year later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the dividing line. AI handles local code generation better than system-level reasoning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xbimcnh3bp5kpb782te.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xbimcnh3bp5kpb782te.png" alt="Generated illustration" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where developers are still irreplaceable
&lt;/h2&gt;

&lt;p&gt;The most valuable parts of software development were never just typing code.&lt;/p&gt;

&lt;p&gt;Developers are still responsible for the parts AI consistently struggles with:&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the problem behind the ticket
&lt;/h3&gt;

&lt;p&gt;Business requirements are often incomplete, contradictory, or politically constrained. A human developer can ask the uncomfortable question, spot hidden assumptions, and translate vague intent into a workable implementation.&lt;/p&gt;

&lt;p&gt;AI can generate an answer. It cannot reliably challenge the question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making architecture tradeoffs
&lt;/h3&gt;

&lt;p&gt;Real systems involve tradeoffs, not ideal answers.&lt;/p&gt;

&lt;p&gt;Should this feature live in an existing module or a new service? Is eventual consistency acceptable here? Are we optimizing for onboarding speed, runtime performance, auditability, or cost control?&lt;/p&gt;

&lt;p&gt;These decisions depend on context that usually lives outside the prompt window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working safely in large, imperfect codebases
&lt;/h3&gt;

&lt;p&gt;Most production systems are not greenfield demos. They include legacy code, weird integrations, undocumented conventions, and historical constraints.&lt;/p&gt;

&lt;p&gt;This is where experienced developers earn their keep. They know that the technically correct change is not always the operationally safe change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Taking responsibility for outcomes
&lt;/h3&gt;

&lt;p&gt;An AI assistant does not get paged at 2 a.m. It does not own the incident review. It does not explain a data leak to legal, security, or customers.&lt;/p&gt;

&lt;p&gt;Software engineering is not just generation. It is accountability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden cost: verification debt
&lt;/h2&gt;

&lt;p&gt;One of the most important ideas in the current AI coding debate is verification debt.&lt;/p&gt;

&lt;p&gt;AI can generate code quickly, but that speed often shifts effort downstream. Instead of spending time writing code, teams spend time validating whether the generated code is correct, secure, idiomatic, and maintainable.&lt;/p&gt;

&lt;p&gt;That creates a new form of debt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code is produced faster than it is reviewed properly&lt;/li&gt;
&lt;li&gt;weak suggestions slip into the codebase because they look plausible&lt;/li&gt;
&lt;li&gt;reviewers must inspect more generated code with lower trust&lt;/li&gt;
&lt;li&gt;maintenance costs rise later because low-context code ages badly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recent survey data points in the same direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;72% of developers reported using AI tools daily&lt;/li&gt;
&lt;li&gt;AI contributes a substantial share of committed code in some teams&lt;/li&gt;
&lt;li&gt;96% of developers do not fully trust AI-generated code&lt;/li&gt;
&lt;li&gt;less than half consistently review AI-generated code before committing&lt;/li&gt;
&lt;li&gt;38% say reviewing AI code can take longer than reviewing human-written code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination should worry engineering leaders.&lt;/p&gt;

&lt;p&gt;If teams accept more machine-generated code while also trusting it less, the result is not full automation. The result is a fragile review pipeline.&lt;/p&gt;

&lt;p&gt;This is why senior engineers are not becoming obsolete. Their work is shifting toward validation, standards, and system integrity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gzu8sjmcpl4o2pdn2g8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gzu8sjmcpl4o2pdn2g8.png" alt="Generated illustration" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Security is the clearest reason AI won’t replace developers
&lt;/h2&gt;

&lt;p&gt;If you want one hard reality check, it is security.&lt;/p&gt;

&lt;p&gt;AI-generated code often looks polished. That makes insecure output more dangerous, not less dangerous.&lt;/p&gt;

&lt;p&gt;Research and industry testing have found recurring problems such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;flawed input validation&lt;/li&gt;
&lt;li&gt;weak authentication or authorization logic&lt;/li&gt;
&lt;li&gt;unsafe serialization patterns&lt;/li&gt;
&lt;li&gt;insecure defaults&lt;/li&gt;
&lt;li&gt;cross-site scripting exposure&lt;/li&gt;
&lt;li&gt;log injection issues&lt;/li&gt;
&lt;li&gt;dependency and configuration mistakes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Veracode study covering 100 LLMs across 80 coding tasks found that about 45% of AI-generated code samples contained security flaws. Reported failure rates were especially high in some languages and security-sensitive tasks.&lt;/p&gt;

&lt;p&gt;That aligns with what many teams see in practice: AI can produce code that appears complete while quietly missing the exact defensive details that matter in production.&lt;/p&gt;

&lt;p&gt;There is a second security problem too: the tools themselves.&lt;/p&gt;

&lt;p&gt;Recent research into AI-enabled IDE workflows has highlighted risks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt injection through project content&lt;/li&gt;
&lt;li&gt;data exfiltration from workspace context&lt;/li&gt;
&lt;li&gt;misuse of tool permissions&lt;/li&gt;
&lt;li&gt;remote code execution paths via compromised assistant workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the risk surface is now two-layered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the generated code may be unsafe&lt;/li&gt;
&lt;li&gt;the coding assistant environment may itself introduce supply-chain and data exposure risks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not a path to replacing developers. It is a path to needing more disciplined developers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj7gxijfqlmfc4ibdyagf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj7gxijfqlmfc4ibdyagf.png" alt="Generated illustration" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why junior and senior developers benefit differently
&lt;/h2&gt;

&lt;p&gt;AI does not help every developer in the same way.&lt;/p&gt;

&lt;p&gt;Less experienced developers often benefit the most from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;faster onboarding&lt;/li&gt;
&lt;li&gt;easier exploration of unfamiliar APIs&lt;/li&gt;
&lt;li&gt;reduced time spent on repetitive syntax work&lt;/li&gt;
&lt;li&gt;quick examples to unblock momentum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a good thing. Used well, AI can shorten the distance between "I know the concept" and "I can build the first version."&lt;/p&gt;

&lt;p&gt;But there is a catch.&lt;/p&gt;

&lt;p&gt;If juniors over-rely on generated solutions they do not understand, they can ship code without building judgment. That creates a team with higher output but thinner engineering depth.&lt;/p&gt;

&lt;p&gt;Senior developers usually get less value from raw generation and more value from targeted acceleration. Their role shifts toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;architectural direction&lt;/li&gt;
&lt;li&gt;code review and design review&lt;/li&gt;
&lt;li&gt;defining guardrails&lt;/li&gt;
&lt;li&gt;mentoring developers on when not to trust the tool&lt;/li&gt;
&lt;li&gt;shaping prompts and workflows around quality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not replacement. It is role redistribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The best teams treat AI like a power tool, not a developer
&lt;/h2&gt;

&lt;p&gt;The most productive framing is simple: AI is a power tool.&lt;/p&gt;

&lt;p&gt;A power tool can make a skilled worker much faster. It can also let an unskilled worker make bigger mistakes faster.&lt;/p&gt;

&lt;p&gt;Teams getting real value from AI coding assistants usually do a few things consistently.&lt;/p&gt;

&lt;h3&gt;
  
  
  They define where AI is allowed to help
&lt;/h3&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;okay for scaffolding and test drafts&lt;/li&gt;
&lt;li&gt;okay for documentation summaries&lt;/li&gt;
&lt;li&gt;okay for refactoring suggestions in low-risk modules&lt;/li&gt;
&lt;li&gt;not okay for auth flows without explicit review&lt;/li&gt;
&lt;li&gt;not okay for security-sensitive changes without human design approval&lt;/li&gt;
&lt;li&gt;not okay for direct commits to critical paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  They keep human review non-negotiable
&lt;/h3&gt;

&lt;p&gt;AI-generated code should be reviewed like code from a new team member who is fast, confident, and occasionally wrong in subtle ways.&lt;/p&gt;

&lt;p&gt;That means checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;correctness&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;li&gt;consistency with project conventions&lt;/li&gt;
&lt;li&gt;operational impact&lt;/li&gt;
&lt;li&gt;maintainability six months from now&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  They invest in guardrails
&lt;/h3&gt;

&lt;p&gt;Useful guardrails include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;secure coding standards&lt;/li&gt;
&lt;li&gt;mandatory tests for generated code&lt;/li&gt;
&lt;li&gt;SAST and dependency scanning&lt;/li&gt;
&lt;li&gt;branch protection and review policies&lt;/li&gt;
&lt;li&gt;secret scanning&lt;/li&gt;
&lt;li&gt;documented AI usage rules&lt;/li&gt;
&lt;li&gt;prompt hygiene, especially around sensitive data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  They optimize for maintainability, not just speed
&lt;/h3&gt;

&lt;p&gt;The wrong metric is lines generated.&lt;/p&gt;

&lt;p&gt;Better metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cycle time without increased incident rate&lt;/li&gt;
&lt;li&gt;review burden&lt;/li&gt;
&lt;li&gt;escaped defects&lt;/li&gt;
&lt;li&gt;time to understand generated code later&lt;/li&gt;
&lt;li&gt;security findings per change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmmfzdg7d7wbhkqpa9gn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmmfzdg7d7wbhkqpa9gn.png" alt="Generated illustration" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When AI helps most — and when it helps least
&lt;/h2&gt;

&lt;p&gt;A balanced view is more useful than either fear or hype.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use AI-assisted coding
&lt;/h3&gt;

&lt;p&gt;AI is a strong fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the task is repetitive or pattern-based&lt;/li&gt;
&lt;li&gt;the scope is narrow and easy to verify&lt;/li&gt;
&lt;li&gt;the code is low-risk and well-tested&lt;/li&gt;
&lt;li&gt;you need a first draft, not a final answer&lt;/li&gt;
&lt;li&gt;developers understand the output well enough to challenge it&lt;/li&gt;
&lt;li&gt;the team has solid review and security practices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generating DTOs, mappings, and validation stubs&lt;/li&gt;
&lt;li&gt;producing test cases for straightforward services&lt;/li&gt;
&lt;li&gt;drafting migration scripts that will be reviewed carefully&lt;/li&gt;
&lt;li&gt;summarizing unfamiliar code before manual refactoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When not to rely on AI-assisted coding
&lt;/h3&gt;

&lt;p&gt;AI is a poor fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;business rules are complex or ambiguous&lt;/li&gt;
&lt;li&gt;security is central to the change&lt;/li&gt;
&lt;li&gt;architecture decisions are still in flux&lt;/li&gt;
&lt;li&gt;the code touches compliance-heavy or highly regulated paths&lt;/li&gt;
&lt;li&gt;the surrounding codebase has lots of undocumented behavior&lt;/li&gt;
&lt;li&gt;the team is unlikely to review the output carefully&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;permission and tenancy boundaries&lt;/li&gt;
&lt;li&gt;payment or identity workflows&lt;/li&gt;
&lt;li&gt;critical infrastructure automation&lt;/li&gt;
&lt;li&gt;cross-service consistency logic&lt;/li&gt;
&lt;li&gt;sensitive data handling and audit trails&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this means for the future of software teams
&lt;/h2&gt;

&lt;p&gt;AI is changing software development, but not in the simplistic way people often describe.&lt;/p&gt;

&lt;p&gt;The likely outcome is not fewer developers because code writes itself. The more plausible outcome is a different distribution of engineering work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more generated code&lt;/li&gt;
&lt;li&gt;more review and verification work&lt;/li&gt;
&lt;li&gt;more emphasis on architecture and systems thinking&lt;/li&gt;
&lt;li&gt;more value placed on security awareness&lt;/li&gt;
&lt;li&gt;more leverage for developers who can guide tools effectively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also organizational effects.&lt;/p&gt;

&lt;p&gt;If AI tools can remove some low-level friction, teams may ship faster. Some studies and industry analyses even project large macroeconomic gains from AI-augmented software work. But inside engineering organizations, those gains depend on whether speed is paired with discipline.&lt;/p&gt;

&lt;p&gt;Without discipline, AI increases noise.&lt;/p&gt;

&lt;p&gt;With discipline, AI increases leverage.&lt;/p&gt;

&lt;p&gt;That is the distinction leaders should care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The developer job is not disappearing — it is getting more judgment-heavy
&lt;/h2&gt;

&lt;p&gt;The strongest developers in the AI era will not be the ones who generate the most code. They will be the ones who can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;define the problem clearly&lt;/li&gt;
&lt;li&gt;evaluate tradeoffs&lt;/li&gt;
&lt;li&gt;spot incorrect assumptions&lt;/li&gt;
&lt;li&gt;review machine output efficiently&lt;/li&gt;
&lt;li&gt;protect quality under delivery pressure&lt;/li&gt;
&lt;li&gt;turn generated fragments into coherent systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a more senior version of software engineering, not a smaller one.&lt;/p&gt;

&lt;p&gt;Typing code was never the whole profession. It was just the most visible part. AI is making that easier, which means the less visible parts now matter even more.&lt;/p&gt;

&lt;p&gt;And those parts are deeply human: judgment, context, responsibility, and taste.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI coding tools improve productivity on repetitive, bounded tasks, but the gains are usually incremental, not total automation.&lt;/li&gt;
&lt;li&gt;Developers are still needed for architecture, business logic, tradeoffs, security, and accountability.&lt;/li&gt;
&lt;li&gt;AI-generated code often adds verification debt, increasing review and maintenance work later.&lt;/li&gt;
&lt;li&gt;Security remains a major limitation, both in generated code and in AI-assisted development workflows.&lt;/li&gt;
&lt;li&gt;The winning teams use AI as a power tool with strong guardrails, not as a replacement for engineering judgment.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>security</category>
      <category>ai</category>
      <category>coding</category>
    </item>
    <item>
      <title>Deep Dive on ABP AI Agent: The Complete Series</title>
      <dc:creator>ABP.IO</dc:creator>
      <pubDate>Tue, 23 Jun 2026 08:23:28 +0000</pubDate>
      <link>https://dev.to/abp_io/deep-dive-on-abp-ai-agent-the-complete-series-1in2</link>
      <guid>https://dev.to/abp_io/deep-dive-on-abp-ai-agent-the-complete-series-1in2</guid>
      <description>&lt;h1&gt;
  
  
  Deep Dive on ABP AI Agent: The Complete Series
&lt;/h1&gt;

&lt;p&gt;ABP Studio is a development platform built around ABP Framework. With the introduction of &lt;strong&gt;ABP AI Coding Agent&lt;/strong&gt; , it became something more: a platform where an AI agent works inside the same environment you already use to build, run, monitor, and ship ABP solutions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fol6h6m1ruy5t6bd2rumo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fol6h6m1ruy5t6bd2rumo.png" alt="ABP Studio with ABP AI Agent" width="799" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;General-purpose AI coding tools are excellent for horizontal, file-shaped work. They read source files, edit them, and run shell commands. But ABP solutions are &lt;strong&gt;system-shaped&lt;/strong&gt; , not just file-shaped. A typical ABP solution is split across multiple modules and layers with strict dependency rules, composed of many runnable units (HTTP services, gateways, identity servers, background workers, Docker containers), and built on a strong set of conventions: aggregate roots, repositories, application services, DTOs, permissions, localization, event bus, distributed cache, and background jobs.&lt;/p&gt;

&lt;p&gt;A generic agent has none of that vocabulary. It does not know what a module is, which project is the Domain layer, or that an &lt;code&gt;ApplicationService&lt;/code&gt; should not depend on a &lt;code&gt;DbContext&lt;/code&gt; directly. It cannot start your microservices, gateway, and auth server together. It cannot tell you that the latest edit caused a runtime exception in the Identity service, because it has no concept of a running application.&lt;/p&gt;

&lt;p&gt;ABP AI Coding Agent was built to close exactly that gap. The agent is born inside a platform that already understands modules, run profiles, builds, migrations, proxies, Docker containers, monitoring, and Git workflows, and it uses every one of them.&lt;/p&gt;

&lt;p&gt;We wrote a &lt;strong&gt;nine-part deep dive series&lt;/strong&gt; to explain how each part of this system works, not as a product tour, but as a practical look at the decisions, controls, and architecture behind the experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-1-agent-plan-and-ask-modes-62wteg9t" rel="noopener noreferrer"&gt;Agent, Plan and Ask Modes&lt;/a&gt;&lt;/strong&gt; — The three interaction modes that control how much action the agent is allowed to take: &lt;strong&gt;Ask&lt;/strong&gt; for understanding (read-only), &lt;strong&gt;Plan&lt;/strong&gt; for designing the approach before editing, and &lt;strong&gt;Agent&lt;/strong&gt; for full implementation with builds, tools, and iteration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-2-supported-ai-models-in-abp-3krbc7yc" rel="noopener noreferrer"&gt;Supported AI Models + Usage Recommendations&lt;/a&gt;&lt;/strong&gt; — How ABP Studio separates models by role (main, research, browser, text processor, Git review) and why treating model selection as a practical decision based on the task leads to a better balance of capability, speed, and cost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-3-rules-skills-and-lessons-ai6kxubt" rel="noopener noreferrer"&gt;Rules, Skills and Lessons&lt;/a&gt;&lt;/strong&gt; — The three mechanisms that give the agent solution-specific memory: &lt;strong&gt;Rules&lt;/strong&gt; (always-on conventions), &lt;strong&gt;Skills&lt;/strong&gt; (on-demand procedures), and &lt;strong&gt;Lessons&lt;/strong&gt; (corrections the agent records and carries forward).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-4-integrated-abp-studio-tools-be2xa2om" rel="noopener noreferrer"&gt;Integrated ABP Studio Tools&lt;/a&gt;&lt;/strong&gt; — The tools that connect the agent to ABP Studio's runtime environment: monitoring (exceptions, logs, requests), applications, containers, tasks, and build actions, with a practical walkthrough showing the difference between debugging with and without tool access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-5-mcp-model-context-protocol-trb9o4ev" rel="noopener noreferrer"&gt;MCP (Model Context Protocol)&lt;/a&gt;&lt;/strong&gt; — How MCP extends the agent beyond the solution boundary to reach external systems like Prometheus, SEO analyzers, or documentation services, with per-tool enable/disable controls and stdio/HTTP server support.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-6-abp-studio-git-integration-09tr41ec" rel="noopener noreferrer"&gt;ABP Studio Git Integration&lt;/a&gt;&lt;/strong&gt; — The full Git loop inside ABP Studio: branching, diffing, AI-generated commit messages, AI code review on staged changes, GitHub issue context for starting tasks, and pull request feedback for addressing reviewer comments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-7-scopes-tfqtkdzu" rel="noopener noreferrer"&gt;Scopes&lt;/a&gt;&lt;/strong&gt; — How AI Scopes restrict the agent's working area to specific modules, packages, or solution areas, reducing unrelated exploration, preventing accidental edits, and making diffs easier to review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-8-parallel-agent-execution-1o0cik6g" rel="noopener noreferrer"&gt;Parallel Agent Execution&lt;/a&gt;&lt;/strong&gt; — Running multiple agent sessions at the same time, each with its own mode, model, scope, and workflow, plus read-only research subagents that fan out inside a single session.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-9-workflows-7jo1adb1" rel="noopener noreferrer"&gt;Workflows&lt;/a&gt;&lt;/strong&gt; — Repeatable before/after steps that wrap agent runs: start containers, build packages, add migrations, generate proxies, restart applications, and run validation tasks, so the agent focuses on the code change while the platform handles the deterministic parts.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Each article focuses on one feature, but the real value comes from how they work together.&lt;/p&gt;

&lt;p&gt;Modes decide how much action the agent takes. Models decide which brain handles the work. Rules, Skills, and Lessons shape what the agent knows. Tools and MCP extend what it can reach. Scopes define where it can work. Workflows define what happens around the work. Git Integration makes the result reviewable and recoverable. Parallel Execution lets multiple tasks move forward at the same time.&lt;/p&gt;

&lt;p&gt;That is the ABP AI Coding Agent experience: &lt;strong&gt;not a single AI button, but a set of controls built into a platform that already understands how ABP solutions are developed, run, and maintained.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>abpframework</category>
      <category>abpstudio</category>
      <category>ai</category>
      <category>abpplatform</category>
    </item>
  </channel>
</rss>
