<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Mon, 28 Sep 2026 06:20:11 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=mansur.besleney" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/.net-11-runtime-async-cleaner-stack-traces-and-faster-async-paths-v3xq0q2p</guid>
      <link>https://abp.io/community/posts/.net-11-runtime-async-cleaner-stack-traces-and-faster-async-paths-v3xq0q2p</link>
      <a10:author>
        <a10:name>mansur.besleney</a10:name>
        <a10:uri>https://abp.io/community/members/mansur.besleney</a10:uri>
      </a10:author>
      <category>dotnet</category>
      <category>new-features</category>
      <category>performance</category>
      <category>.net</category>
      <title>.NET 11 Runtime Async: Cleaner Stack Traces and Faster Async Paths</title>
      <description>This article explores the new Runtime Async improvements in .NET 11, focusing on cleaner live async stack traces and faster async execution paths. It explains how moving more async handling into the runtime and JIT can reduce overhead, improve debugging, and create new performance optimization opportunities while keeping the familiar async/await programming model unchanged.</description>
      <pubDate>Mon, 21 Sep 2026 11:44:31 Z</pubDate>
      <a10:updated>2026-09-28T01:32:07Z</a10:updated>
      <content:encoded><![CDATA[<h1>.NET 11 Runtime Async: Cleaner Stack Traces and Faster Async Paths</h1>
<h2>Introduction</h2>
<p>Asynchronous code is everywhere in a modern .NET application.</p>
<p>An HTTP endpoint awaits an application service. The application service awaits a repository. The repository awaits a database call. Logging, authorization, retries, serialization, and network operations add more asynchronous layers around the same request.</p>
<p>The C# code can still look simple:</p>
<pre><code class="language-csharp">public async Task&lt;OrderDto&gt; GetAsync(Guid id)
{
    Order order = await _orderRepository.GetAsync(id);
    return ObjectMapper.Map&lt;Order, OrderDto&gt;(order);
}
</code></pre>
<p>However, the implementation below <code>async</code> and <code>await</code> has not been simple. In .NET 10 and earlier versions, the C# compiler normally turns each async method into a generated state machine. This design works well, but it can add infrastructure frames to live call stacks, create intermediate task-related objects, and make production profiling more difficult.</p>
<p>.NET 11 starts changing that foundation with <strong>Runtime Async V2</strong>.</p>
<p>The source code and the async programming model do not change. The important change happens below the source code: more of the async transformation moves from the C# compiler into the runtime and the JIT compiler. This gives .NET more information at the moment it optimizes the method.</p>
<p>For developers, the main results are:</p>
<ul>
<li>Cleaner live async stack traces.</li>
<li>Better stepping and debugging through async code.</li>
<li>Opportunities to remove intermediate <code>Task</code> objects and allocations.</li>
<li>Smaller generated IL for eligible async methods.</li>
<li>Better optimization of common async paths.</li>
<li>A new lower-overhead foundation for async profiling.</li>
</ul>
<p>This article explains what changed, where you can observe the difference, how to measure it, and what you should consider before enabling the preview feature in a real application.</p>
<h2>The main change: async lowering moves closer to the runtime</h2>
<p>Consider this small method:</p>
<pre><code class="language-csharp">static async Task&lt;int&gt; ReadLengthAsync(
    Stream stream,
    CancellationToken cancellationToken)
{
    var buffer = new byte[4096];
    int bytesRead = await stream.ReadAsync(buffer, cancellationToken);
    return bytesRead;
}
</code></pre>
<p>With the classic model, the C# compiler creates a state-machine type. It usually contains:</p>
<ul>
<li>A state number.</li>
<li>An async method builder.</li>
<li>Fields for parameters and local values that must survive an incomplete <code>await</code>.</li>
<li>Awaiter fields.</li>
<li>A generated <code>MoveNext()</code> method.</li>
</ul>
<p><code>MoveNext()</code> runs the method until an awaited operation is incomplete. It saves the required state and registers itself as the continuation. Later, it runs again and continues from the saved state.</p>
<p>Runtime and JIT optimizations have made this model much cheaper over many .NET releases. Still, by the time the JIT receives the method, the compiler has already expanded a small source method into a more complex state machine.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-DotNET-11-Runtime-Async-Cleaner-Stack-Traces-and-Faster-Async-Paths/assets/runtime-async-architecture.png" alt="Classic compiler lowering compared with .NET 11 Runtime Async" /></p>
<p>With Runtime Async, the compiler emits smaller, suspension-aware IL and marks the method as async in metadata. The runtime and JIT then manage more of the transformation. They can use runtime information to decide:</p>
<ul>
<li>Which values are really alive at a suspension point.</li>
<li>How continuation state should be stored.</li>
<li>Whether an intermediate <code>Task</code> must be materialized.</li>
<li>Whether a direct async call can use a more efficient calling path.</li>
<li>How the method should be optimized after it becomes hot.</li>
</ul>
<p>The programming model remains the same. <code>await</code>, cancellation, exceptions, synchronous completion, and <code>ConfigureAwait</code> keep their existing meaning. Microsoft states that behavioral compatibility is an explicit goal; an observable semantic difference should be treated as a bug.</p>
<h3>Enabling Runtime Async in a .NET 11 project</h3>
<p>Application code must currently opt in:</p>
<pre><code class="language-xml">&lt;PropertyGroup&gt;
  &lt;TargetFramework&gt;net11.0&lt;/TargetFramework&gt;
  &lt;Features&gt;$(Features);runtime-async=on&lt;/Features&gt;
&lt;/PropertyGroup&gt;
</code></pre>
<p>No new C# syntax is required. You do not need <code>LangVersion=preview</code> or <code>EnablePreviewFeatures</code> for this switch in a <code>net11.0</code> project.</p>
<p>The .NET 11 runtime libraries are already compiled with Runtime Async enabled. Your own application and library methods use the new lowering only when you enable the feature during compilation.</p>
<h2>Cleaner live async stack traces</h2>
<p>The clearest developer-experience improvement is visible in a <strong>live stack trace</strong>.</p>
<p>This means the stack shown by:</p>
<ul>
<li>The debugger Call Stack window.</li>
<li><code>new StackTrace()</code> while the code is running.</li>
<li>A profiler that inspects live execution stacks.</li>
<li>Diagnostic code that captures the current stack.</li>
</ul>
<h3>Runnable example</h3>
<p>The complete project is included in <code>samples/AsyncStackTraceDemo</code>. Its important part is:</p>
<pre><code class="language-csharp">using System.Diagnostics;

await OuterAsync();

static async Task OuterAsync()
{
    await Task.CompletedTask;
    await MiddleAsync();
}

static async Task MiddleAsync()
{
    await Task.CompletedTask;
    await InnerAsync();
}

static async Task InnerAsync()
{
    await Task.CompletedTask;
    Console.WriteLine(new StackTrace(fNeedFileInfo: true));
}
</code></pre>
<p>The included script builds three variants:</p>
<ol>
<li>.NET 10 with compiler-generated async lowering.</li>
<li>.NET 11 with compiler-generated async lowering.</li>
<li>.NET 11 with Runtime Async enabled.</li>
</ol>
<p>On PowerShell:</p>
<pre><code class="language-powershell">./scripts/run-samples.ps1
</code></pre>
<p>On Bash:</p>
<pre><code class="language-bash">chmod +x ./scripts/run-samples.sh
./scripts/run-samples.sh
</code></pre>
<h3>Observed output</h3>
<p>The .NET 10 and .NET 11 classic builds both produced 13 live frames. The output included repeated infrastructure frames similar to:</p>
<pre><code class="language-text">InnerAsync()
AsyncMethodBuilderCore.Start(...)
InnerAsync()
MiddleAsync()
AsyncMethodBuilderCore.Start(...)
MiddleAsync()
OuterAsync()
AsyncMethodBuilderCore.Start(...)
OuterAsync()
Main()
AsyncMethodBuilderCore.Start(...)
Main()
Main(string[] args)
</code></pre>
<p>The Runtime Async build produced 5 frames:</p>
<pre><code class="language-text">InnerAsync()
MiddleAsync()
OuterAsync()
Main()
Main(string[] args)
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-DotNET-11-Runtime-Async-Cleaner-Stack-Traces-and-Faster-Async-Paths/assets/live-stack-comparison.png" alt="Live stack trace comparison: 13 classic frames and 5 Runtime Async frames" /></p>
<p>The exact generated method names can differ between source layout, SDK builds, and Debug or Release configuration. The useful result is the shape of the stack: Runtime Async shows the application call chain directly and removes the repeated async builder infrastructure from this example.</p>
<h3>Important: this is a live-stack improvement</h3>
<p>Do not describe this feature as a general fix for <code>exception.StackTrace</code>.</p>
<p>Exception stack traces from code such as the following were already cleaned up by the existing async infrastructure:</p>
<pre><code class="language-csharp">try
{
    await ProcessAsync();
}
catch (Exception exception)
{
    Console.WriteLine(exception.StackTrace);
}
</code></pre>
<p>The .NET 11 improvement is most visible while inspecting <strong>live execution</strong>, not after catching an exception.</p>
<h2>Where the performance opportunities come from</h2>
<p>Runtime Async does not make every async operation faster and it does not remove every allocation. It gives the runtime new opportunities that were difficult to see after the compiler had already created a state machine.</p>
<h3>1. Fewer intermediate Task objects</h3>
<p>Consider a common layered call chain:</p>
<pre><code class="language-csharp">static async Task&lt;int&gt; A() =&gt; await B();
static async Task&lt;int&gt; B() =&gt; await C();

static async Task&lt;int&gt; C()
{
    await Task.Yield();
    return 42;
}
</code></pre>
<p>In the classic model, every method has its own transformed state and task-like result. When the pattern is a direct call followed by a direct <code>await</code>, Runtime Async can sometimes use a special async calling path and link continuation state instead.</p>
<p>If the chain completes synchronously, the result can flow through internal calls as a value. If it suspends, the runtime can link continuation records without requiring a separate intermediate <code>Task&lt;int&gt;</code> at every eligible edge.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-DotNET-11-Runtime-Async-Cleaner-Stack-Traces-and-Faster-Async-Paths/assets/task-materialization.png" alt="How Runtime Async can avoid some intermediate Task objects" /></p>
<p>The optimization is not possible when the <code>Task</code> is observable as an object. For example:</p>
<pre><code class="language-csharp">Task&lt;int&gt; task = GetValueAsync();
pendingTasks.Add(task);
task.ContinueWith(LogCompletion);
return task;
</code></pre>
<p>Here, the program stores and manually observes the task. The runtime must preserve that behavior and materialize the object.</p>
<h3>2. Smaller generated IL and binaries</h3>
<p>The classic transformation creates an entry method, a generated type, state fields, a builder, and <code>MoveNext()</code> for each async method.</p>
<p>Runtime Async leaves a smaller method body for the runtime to transform. In a deliberately async-heavy size test published by Microsoft, ten small async methods produced these assembly sizes:</p>
<p>| Lowering | Assembly size |
|---|---:|
| Compiler-generated | 10,752 bytes |
| Runtime Async | 5,632 bytes |</p>
<p>This is not a promise that a complete application will become 48% smaller. The test was intentionally dominated by small async methods. The useful conclusion is that Runtime Async can reduce the per-method IL and metadata cost.</p>
<h3>3. Better exception flow through deep async chains</h3>
<p>Classic async state machines normally contain generated exception handling so that an unhandled exception can be stored in the returned task.</p>
<p>In a deep chain, an exception can be:</p>
<ol>
<li>Caught by generated code in the inner method.</li>
<li>Stored in its task.</li>
<li>Read and thrown again by the caller's awaiter.</li>
<li>Caught by the caller's generated code.</li>
<li>Stored in another task.</li>
</ol>
<p>Runtime Async can move through continuation records that have no real user exception handler and fault the observable root task more directly. This can reduce repeated throw, catch, and task-fault work in deep pass-through chains.</p>
<p>This optimization does not make exceptions a normal or recommended control-flow mechanism. It only makes the runtime-generated path less expensive where possible.</p>
<h3>4. Skipping unnecessary ExecutionContext work</h3>
<p><code>ExecutionContext</code> carries ambient state across async boundaries. <code>AsyncLocal&lt;T&gt;</code> values are a common example. Tracing and request-correlation systems can also use ambient state.</p>
<p>.NET 11 can detect when a continuation has no context state to restore and skip an unnecessary capture-and-restore cycle. <code>Task</code>, <code>Task&lt;T&gt;</code>, <code>ValueTask</code>, and <code>ValueTask&lt;T&gt;</code> paths can benefit.</p>
<p>The effect is workload-dependent. A high-throughput library path with little ambient state may benefit more than an ASP.NET Core request path that uses <code>Activity</code>, tracing, and several <code>AsyncLocal&lt;T&gt;</code> values.</p>
<h3>5. JIT improvements for async paths</h3>
<p>.NET 11 also improves several details around Runtime Async:</p>
<ul>
<li>Runtime Async methods participate in tiered compilation, so hot methods can receive Tier 1 optimizations.</li>
<li>The JIT recognizes common factories such as <code>Task.CompletedTask</code>, <code>Task.FromResult</code>, and <code>ValueTask.FromResult</code>.</li>
<li>Suspension points can be tail-merged to reduce generated code size.</li>
<li>Some continuation objects can be cached and reused.</li>
<li>Direct tail-await paths can avoid unnecessary layers.</li>
<li>Runtime Async supports ReadyToRun and NativeAOT scenarios.</li>
<li>ReadyToRun compilation can inline eligible await-less Runtime Async calls.</li>
</ul>
<p>These are implementation improvements. Application code should continue to choose <code>Task</code> or <code>ValueTask</code> based on API semantics and measured needs, not because Runtime Async exists.</p>
<h2>A reproducible benchmark</h2>
<p>The package includes <code>benchmarks/AsyncPathBenchmarks</code>. It compares classic and Runtime Async lowering inside the same .NET 11 process. This isolates the lowering strategy better than comparing a .NET 10 process with a .NET 11 process, where many other runtime changes would also affect the result.</p>
<p>The benchmark uses a compiler-recognized <code>RuntimeAsyncMethodGenerationAttribute</code> to force classic lowering for selected methods. This attribute is experimental and is <strong>not a public .NET API</strong>. Use it only for controlled experiments like this one.</p>
<p>Run it with:</p>
<pre><code class="language-bash">dotnet run \
  --project benchmarks/AsyncPathBenchmarks/AsyncPathBenchmarks.csproj \
  -c Release \
  --filter &quot;*&quot;
</code></pre>
<p>The included job performs 3 warm-up iterations and 10 measurement iterations. For serious performance work, increase the iteration count and run on a quiet, dedicated machine.</p>
<h3>Results from the included run</h3>
<p>Test environment:</p>
<ul>
<li>Ubuntu 24.04.3 LTS.</li>
<li>Intel Xeon Platinum 8573C, with 9 logical cores available to the container.</li>
<li>.NET SDK <code>11.0.100-rc.1.26425.128</code>.</li>
<li>.NET runtime <code>11.0.0-rc.1.26425.128</code>.</li>
<li>BenchmarkDotNet <code>0.16.0-preview.1</code>.</li>
<li>Release configuration and Workstation GC.</li>
</ul>
<p>| Method | Mean | Ratio | Allocated |
|---|---:|---:|---:|
| ClassicCompleted | 84.99 ns | 1.00 | 144 B |
| RuntimeCompleted | 15.83 ns | 0.20 | 0 B |
| ClassicYielding | 678.15 ns | 1.00 | 248 B |
| RuntimeYielding | 314.50 ns | 0.46 | 168 B |</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-DotNET-11-Runtime-Async-Cleaner-Stack-Traces-and-Faster-Async-Paths/assets/benchmark-results.png" alt="Benchmark results from the included .NET 11 RC1 run" /></p>
<p>These values are <strong>not a general performance claim</strong>. The benchmark ran in a shared container, could not raise process priority, and the <code>ClassicCompleted</code> measurements had high variance. The full BenchmarkDotNet report is included in <code>results/benchmark-results.md</code>.</p>
<p>The results demonstrate how to measure the feature and show clear allocation differences in this small pattern. They do not tell us how much faster an ABP application, API endpoint, or database operation will become.</p>
<p>Microsoft's own benchmark of the same two-layer pattern also showed lower time and allocation for Runtime Async, but with different absolute numbers. That difference is exactly why you should run the benchmark on your hardware and then measure a realistic application workload.</p>
<h3>Benchmark hygiene</h3>
<p>Runtime Async is a compile-time feature. When switching the feature on and off in the same project, force a rebuild. Otherwise, an incremental build can leave an assembly from the previous configuration in the output directory.</p>
<p>The included scripts use <code>--no-incremental</code> for this reason.</p>
<p>For a fair application benchmark:</p>
<ul>
<li>Use Release builds.</li>
<li>Pin the SDK and runtime versions.</li>
<li>Record OS, CPU, GC mode, and application configuration.</li>
<li>Warm up tiered compilation before recording steady-state throughput.</li>
<li>Measure latency percentiles, CPU, and allocations—not only requests per second.</li>
<li>Run several independent processes or test rounds.</li>
<li>Compare the same commit with only the Runtime Async switch changed.</li>
<li>Test with your real third-party libraries and normal observability configuration.</li>
</ul>
<h2>Where should the difference be visible?</h2>
<p>Runtime Async has the best opportunity in code with many small async layers:</p>
<pre><code class="language-text">HTTP endpoint
  → application service
    → authorization helper
      → retry or policy layer
        → repository
          → database or network operation
</code></pre>
<p>Possible observable effects include:</p>
<ul>
<li>Fewer managed allocations per request.</li>
<li>Lower GC pressure at high request rates.</li>
<li>Better throughput on async-heavy CPU paths.</li>
<li>Smaller application or library assemblies.</li>
<li>Cleaner debugger and profiler stacks.</li>
</ul>
<p>The difference may be difficult to see when:</p>
<ul>
<li>Most request time is spent waiting for a slow database or remote API.</li>
<li>The async call chain is shallow.</li>
<li>Tasks are stored or otherwise observed as objects.</li>
<li>Much of the hot path is in older third-party assemblies compiled with classic lowering.</li>
<li>Tracing, logging, or <code>AsyncLocal&lt;T&gt;</code> usage requires ambient context flow.</li>
<li>The application is limited by database capacity, locks, serialization, or network bandwidth.</li>
</ul>
<p>A 100-millisecond database query can easily hide a small runtime improvement in end-to-end request latency. The same improvement may still appear in allocation rate, CPU use, or maximum throughput under load.</p>
<h2>Investigating async behavior in production</h2>
<p>Production async problems usually appear as one of these symptoms:</p>
<ul>
<li>High latency but low CPU usage.</li>
<li>Thread-pool queue growth.</li>
<li>Unexpected allocation or GC pressure.</li>
<li>Time spent in continuations or framework infrastructure.</li>
<li>A request that starts on one thread and continues on another.</li>
</ul>
<p>The last case is especially important. When an async method suspends, its physical thread stack unwinds. The continuation can later resume on a different thread.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-DotNET-11-Runtime-Async-Cleaner-Stack-Traces-and-Faster-Async-Paths/assets/physical-logical-async.png" alt="Physical thread stacks compared with a logical async call chain" /></p>
<p>A normal sampling profiler can see the code currently running on Thread 27, but the earlier physical stack from Thread 12 is gone. An async-aware profiler needs runtime events to rebuild the logical chain.</p>
<h3>Start with counters</h3>
<p>Use counters to decide whether you need a more detailed trace:</p>
<pre><code class="language-bash">dotnet tool install --global dotnet-counters
dotnet-counters monitor --process-id &lt;PID&gt;
</code></pre>
<p>Watch the signals that match the symptom, such as:</p>
<ul>
<li>CPU usage.</li>
<li>Allocation rate and GC activity.</li>
<li>Thread-pool thread count.</li>
<li>Thread-pool queue length.</li>
<li>Exception rate.</li>
</ul>
<p>Counters are good for detecting a problem, but they normally do not show the full logical async call chain.</p>
<h3>Collect a short trace</h3>
<p>Install <code>dotnet-trace</code>:</p>
<pre><code class="language-bash">dotnet tool install --global dotnet-trace
</code></pre>
<p>Collect a short runtime and sampled-thread trace:</p>
<pre><code class="language-bash">dotnet-trace collect \
  --process-id &lt;PID&gt; \
  --duration 00:00:00:30 \
  --profile dotnet-common,dotnet-sampled-thread-time \
  --output async-investigation.nettrace
</code></pre>
<p>The exact providers should follow the question you are investigating. More events create more data and more overhead. Start narrow, record for a limited time, and reproduce one known problem window.</p>
<p>Open the trace in a compatible tool such as Visual Studio's performance tools or PerfView. Tool support matters: a runtime can emit new events before every analysis tool presents them in a useful async view.</p>
<h3>What is new for profiling in .NET 11?</h3>
<p>Traditional Task Parallel Library events can be very verbose in async-heavy applications. .NET 11 adds a new buffered async-profiler event pipeline. Its design uses:</p>
<ul>
<li>Per-thread buffers.</li>
<li>Compact timestamp and instruction-pointer encoding.</li>
<li>Batched event flushing.</li>
<li>A small wrapper frame that helps connect CPU samples to a logical async continuation.</li>
</ul>
<p>In the runtime pull request's synthetic measurements, the new stream produced much less trace data and low overhead at realistic event rates. Treat those values as design measurements, not as a guarantee for your service. The event format is internal and can change, and analysis tools must understand it before you receive the full benefit.</p>
<p>Follow these production rules:</p>
<ul>
<li>Measure trace overhead on a staging environment first.</li>
<li>Keep collection windows short.</li>
<li>Avoid collecting unnecessary providers.</li>
<li>Check for dropped events and increase buffers only when needed.</li>
<li>Run the tool with the required process permissions.</li>
<li>Protect traces because they can contain application names, paths, exception messages, and business information.</li>
</ul>
<h3>Keep distributed tracing in the picture</h3>
<p>Runtime stacks and CPU traces answer questions about execution cost. <code>Activity</code> and OpenTelemetry traces answer a different question: where did the request spend wall-clock time across services, databases, and external calls?</p>
<p>For a production investigation, use both views when possible:</p>
<ul>
<li>Distributed trace: request latency and service boundaries.</li>
<li>Runtime trace: CPU, GC, contention, threads, and managed call stacks.</li>
<li>Application logs: business context and failure details.</li>
</ul>
<p>Together, they are more useful than any one source alone.</p>
<h2>Preview limitations and production readiness</h2>
<p>Runtime Async currently supports async methods returning:</p>
<p>| Return type or feature | .NET 11 Runtime Async status |
|---|---|
| <code>Task</code> | Supported |
| <code>Task&lt;T&gt;</code> | Supported |
| <code>ValueTask</code> | Supported |
| <code>ValueTask&lt;T&gt;</code> | Supported |
| <code>async void</code> | Uses classic compiler transformation |
| Async iterators / <code>IAsyncEnumerable&lt;T&gt;</code> | Uses classic compiler transformation |
| Custom task-like types and custom builders | Use classic compiler transformation |</p>
<p>Runtime Async is compatible with existing assemblies. New code can await libraries compiled with the classic model, and classic code can call Runtime Async methods. However, the largest optimization opportunities appear when more of a direct async call chain uses Runtime Async.</p>
<p>The important readiness facts are:</p>
<ul>
<li>.NET 11 is RC1 at the time of writing, not the final release.</li>
<li>Runtime Async remains preview and opt-in for application code.</li>
<li>Microsoft describes the .NET 11 performance goal as broad parity with .NET 10, not a universal speedup.</li>
<li>Important paths are already equal or faster, but known slower cases still exist.</li>
<li>Tooling support for new profiling data can arrive at different times.</li>
<li>Performance results can change between RC1, the final release, and servicing updates.</li>
</ul>
<p>For production, do not enable the feature only because a microbenchmark is faster. Test compatibility, performance, diagnostics, startup, publishing, and rollback in the same deployment model you use for the real application.</p>
<h2>Conclusion</h2>
<p>Runtime Async is one of the most important runtime changes in .NET 11, even though it does not add new C# syntax.</p>
<p>It moves more async implementation work into the runtime and JIT, where .NET has better information for optimization. The most immediate improvement is easier debugging through cleaner live stacks. The longer-term opportunity is reducing the hidden cost of composing many small async methods.</p>
<p>The feature is also a good reminder about performance work: architecture creates an opportunity, but measurement tells us whether that opportunity matters in our application.</p>
<p>Keep writing clear async code. Use <code>Task</code> and <code>ValueTask</code> according to their API trade-offs. Then benchmark the application, inspect its traces, and let evidence decide when Runtime Async is ready for your production workload.</p>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/overview">What's new in .NET 11</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime">What's new in the .NET 11 runtime</a></li>
<li><a href="https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/">Performance Improvements in .NET 11 — Stephen Toub</a></li>
<li><a href="https://github.com/dotnet/runtime/pull/127238">High-performance EventSource runtime async profiler — dotnet/runtime PR #127238</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace">dotnet-trace documentation</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters">dotnet-counters documentation</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-monitor">dotnet-monitor documentation</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a23d5f5-0897-f2c5-944c-6cd7fccd15be" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a23d5f5-0897-f2c5-944c-6cd7fccd15be" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/automatically-validate-your-documentation-how-we-built-a-tutorial-validator-m3ozgkhv</guid>
      <link>https://abp.io/community/posts/automatically-validate-your-documentation-how-we-built-a-tutorial-validator-m3ozgkhv</link>
      <a10:author>
        <a10:name>mansur.besleney</a10:name>
        <a10:uri>https://abp.io/community/members/mansur.besleney</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>tool</category>
      <category>tutorial</category>
      <category>validation</category>
      <category>open source</category>
      <title>Automatically Validate Your Documentation: How We Built a Tutorial Validator</title>
      <description>The tutorial validator behaves like a real developer following your guide step by step. It reads instructions, runs commands, writes files, executes the application, and verifies expected results. We initially created it to automatically validate ABP Framework tutorials, then released it as an open-source tool so anyone can use it to test their own documentation.

</description>
      <pubDate>Wed, 11 Mar 2026 11:01:58 Z</pubDate>
      <a10:updated>2026-09-28T03:58:43Z</a10:updated>
      <content:encoded><![CDATA[<h1>Automatically Validate Your Documentation: How We Built an AI Tutorial Validator</h1>
<blockquote>
<p>If you're in a hurry and want to quickly check the repository, you can find the source code of the AI Tutorial Validator here 👉 <a href="https://github.com/abpframework/ai-tutorial-validator">github.com/abpframework/ai-tutorial-validator</a></p>
</blockquote>
<p>Writing a tutorial is difficult. Keeping technical documentation accurate over time is even harder.
If you maintain developer documentation, you probably know the problem: a tutorial that worked a few months ago can silently break after a framework update, dependency change, or a small missing line in a code snippet.
New developers follow the guide, encounter an error, and quickly lose trust in the documentation.
To solve this problem, we built the tutorial validator — an open-source AI-powered tutorial validator that automatically verifies whether a software tutorial actually works from start to finish.
Instead of manually reviewing documentation, the tutorial validator behaves like a real developer following your guide step by step.
It reads instructions, runs commands, writes files, executes the application, and verifies expected results.
We initially created it to automatically validate ABP Framework tutorials, then released it as an open-source tool so anyone can use it to test their own documentation.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image.png" alt="the tutorial validator Orchestrator" /></p>
<h2>The Problem: Broken Tutorials in Technical Documentation</h2>
<p>Many documentation issues are difficult to catch during normal reviews.
Common problems include:</p>
<ul>
<li><p>A command assumes a file already exists</p>
</li>
<li><p>A code snippet misses a namespace or import</p>
</li>
<li><p>A tutorial step relies on hidden context</p>
</li>
<li><p>An endpoint is expected to respond but fails</p>
</li>
<li><p>A dependency version changed and breaks the project</p>
</li>
</ul>
<p>Traditional proofreading tools only check grammar or wording.
<strong>The tutorial validator focuses on execution correctness.</strong>
It treats tutorials like testable workflows, ensuring that every step works exactly as written.</p>
<h2>How the Tutorial Validator Works?</h2>
<p>The tutorial validator validates tutorials using a three-stage pipeline:</p>
<ol>
<li><strong>Analyst</strong>: Scrapes tutorial pages and converts instructions into a structured test plan</li>
<li><strong>Executor</strong>: Follows the plan step by step in a clean environment</li>
<li><strong>Reporter</strong>: Produces a clear result summary and optional notifications</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-1.png" alt="the tutorial validator Analyst" /></p>
<p>It identifies commands, code edits, HTTP requests, and expected outcomes.
The key idea is simple: if a developer needs to do it, the validator does it too.
That includes running terminal commands, editing files, checking HTTP responses, and validating build outcomes.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-2.png" alt="the tutorial validator Executor" /></p>
<h2>Why Automated Tutorial Validation Matters?</h2>
<p>The tutorial validator is designed for practical documentation quality, not just technical experimentation.</p>
<ul>
<li><strong>Catches real-world breakages early</strong> before readers report them</li>
<li><strong>Creates repeatable validation</strong> instead of one-off manual checks</li>
<li><strong>Works well in teams</strong> through report outputs, logs, and CI-friendly behavior</li>
<li><strong>Supports different strictness levels</strong> with developer personas (<code>junior</code>, <code>mid</code>, <code>senior</code>)</li>
</ul>
<p>For example, <code>junior</code> and <code>mid</code> personas are great for spotting unclear documentation, while <code>senior</code> helps identify issues an experienced developer could work around.</p>
<h2>Built for ABP, Open for Everyone</h2>
<p>Although TutorialValidator was originally built to validate <strong>ABP Framework tutorials</strong>, it works with <strong>any publicly accessible software tutorial</strong>.</p>
<p>It supports validating any publicly accessible software tutorial and can run in:</p>
<ul>
<li><strong>Docker mode</strong> for clean, isolated execution (recommended)</li>
<li><strong>Local mode</strong> for faster feedback when your environment is already prepared</li>
</ul>
<p>It also supports multiple AI providers, including OpenAI, Azure OpenAI, and OpenAI-compatible endpoints.</p>
<h2>Open Source and Easily Extensible</h2>
<p>The tutorial validator is designed with a modular architecture.
The project consists of multiple focused components:</p>
<ul>
<li><strong>Core</strong> – shared models and contracts</li>
<li><strong>Analyst</strong> – tutorial scraping and step extraction</li>
<li><strong>Executor</strong> – step-by-step execution engine</li>
<li><strong>Orchestrator</strong> – workflow coordination</li>
<li><strong>Reporter</strong> – notifications and result summaries</li>
</ul>
<p>This architecture makes it easy to extend the validator with:</p>
<ul>
<li>new step types</li>
<li>additional AI providers</li>
<li>custom reporting integrations</li>
</ul>
<p>This architecture keeps the project easy to understand and extend. Teams can add new step types, plugins, or reporting channels based on their own workflow.</p>
<h2>Final Thoughts</h2>
<p>Documentation is a critical part of the product experience.
When tutorials break, developer trust breaks too.
TutorialValidator helps teams move from:</p>
<blockquote>
<p>We believe this tutorial works 🙄</p>
</blockquote>
<p>to</p>
<blockquote>
<p>We verified this tutorial works ✅</p>
</blockquote>
<p>If your team maintains <strong>technical tutorials, developer guides, or framework documentation</strong>, automated tutorial validation can provide a powerful safety net.</p>
<p>Documentation is part of the product experience. When tutorials fail, trust fails.
If your team maintains technical tutorials, this project can give you a practical safety net and a repeatable quality process.</p>
<hr />
<p>You can find the source code of the tutorial validator at this repo 👉 <a href="https://github.com/abpframework/ai-tutorial-validator">github.com/abpframework/ai-tutorial-validator</a></p>
<p>We would love to hear your feedback, ideas and waiting PRs to improve this application.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1feebc-588f-a5c3-ef46-6648a41236a2" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1feebc-588f-a5c3-ef46-6648a41236a2" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/top-10-exception-handling-mistakes-in-.net-and-how-to-actually-fix-them-jhm8wzvg</guid>
      <link>https://abp.io/community/posts/top-10-exception-handling-mistakes-in-.net-and-how-to-actually-fix-them-jhm8wzvg</link>
      <a10:author>
        <a10:name>mansur.besleney</a10:name>
        <a10:uri>https://abp.io/community/members/mansur.besleney</a10:uri>
      </a10:author>
      <category>dotnet</category>
      <category>exception-handling</category>
      <category>best-practices</category>
      <category>asp.net</category>
      <title>Top 10 Exception Handling Mistakes in .NET (and How to Actually Fix Them)</title>
      <description>In this article we will see top mistakes that all of us can do for exception handling in .Net. These small details can save you hours of time while you track exceptions.</description>
      <pubDate>Fri, 17 Oct 2025 08:51:17 Z</pubDate>
      <a10:updated>2026-09-27T22:30:55Z</a10:updated>
      <content:encoded><![CDATA[<h1>💥 Top 10 Exception Handling Mistakes in .NET (and How to Actually Fix Them)</h1>
<p>Every .NET developer has been there it's 3 AM, production just went down, and the logs are flooding in.<br />
You open the error trace, only to find… nothing useful. The stack trace starts halfway through a catch block, or worse it's empty. Somewhere, an innocent-looking <code>throw ex;</code> or a swallowed background exception has just cost hours of sleep.</p>
<p>Exception handling is one of those things that seems simple on the surface but can quietly undermine an entire system if done wrong. Tiny mistakes like catching <code>Exception</code>, forgetting an <code>await</code>, or rethrowing incorrectly don't just break code; they break observability. They hide root causes, produce misleading logs, and make even well-architected applications feel unpredictable.</p>
<p>In this article, we'll go through the most common exception handling mistakes developers make in .NET and more importantly, how to fix them. Along the way, you'll see how small choices in your code can mean the difference between a five-minute fix and a full-blown production nightmare.</p>
<hr />
<h2>🧨 1. Catching <code>Exception</code> (and Everything Else)</h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">try
{
    // Some operation
}
catch (Exception ex)
{
    // Just to be safe
}

</code></pre>
<p><strong>Why it's a problem:</strong><br />
Catching the base <code>Exception</code> type hides all context including <code>OutOfMemoryException</code>, <code>StackOverflowException</code>, and other runtime-level issues that you should never handle manually. It also makes debugging painful since you lose the ability to treat specific failures differently.</p>
<p><strong>The right way:</strong><br />
Catch only what you can handle:</p>
<pre><code class="language-csharp">catch (SqlException ex)
{
    // Handle DB issues
}
catch (IOException ex)
{
    // Handle file issues
}

</code></pre>
<p>If you really must catch all exceptions (e.g., at a system boundary), <strong>log and rethrow</strong>:</p>
<pre><code class="language-csharp">catch (Exception ex)
{
    _logger.LogError(ex, &quot;Unexpected error occurred&quot;);
    throw;
}

</code></pre>
<blockquote>
<p>💡 <strong>ABP Tip:</strong> In ABP-based applications, you rarely need to catch every exception at the controller or service level.<br />
The framework's built-in <code>AbpExceptionFilter</code> already handles unexpected exceptions, logs them, and returns standardized JSON responses automatically keeping your controllers clean and consistent.</p>
</blockquote>
<hr />
<h2>🕳️ 2. Swallowing Exceptions Silently</h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">try
{
    DoSomething();
}
catch
{
    // ignore
}

</code></pre>
<p><strong>Why it's a problem:</strong><br />
Silent failures make debugging nearly impossible. You lose stack traces, error context, and sometimes even awareness that something failed at all.</p>
<p><strong>The right way:</strong><br />
Always log or rethrow, unless you have a very specific reason not to:</p>
<pre><code class="language-csharp">try
{
    _cache.Remove(key);
}
catch (Exception ex)
{
    _logger.LogWarning(ex, &quot;Failed to clear cache key {Key}&quot;, key);
}

</code></pre>
<blockquote>
<p>💡 <strong>ABP Tip:</strong> Since ABP automatically logs all unhandled exceptions, it's often better to let the framework handle them. Only catch exceptions when you want to enrich logs or add custom business logic before rethrowing.</p>
</blockquote>
<hr />
<h2>🌀 3. Using <code>throw ex;</code> Instead of <code>throw;</code></h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">catch (Exception ex)
{
    Log(ex);
    throw ex;
}

</code></pre>
<p><strong>Why it's a problem:</strong><br />
Using <code>throw ex;</code> resets the stack trace you lose where the exception actually occurred. This is one of the biggest causes of misleading production logs.</p>
<p><strong>The right way:</strong></p>
<pre><code class="language-csharp">catch (Exception ex)
{
    Log(ex);
    throw; // preserves stack trace
}

</code></pre>
<hr />
<h2>⚙️ 4. Wrapping Everything in Try/Catch</h2>
<p><strong>The mistake:</strong><br />
Developers sometimes wrap <em>every function</em> in try/catch “just to be safe.”</p>
<p><strong>Why it's a problem:</strong><br />
This clutters your code and hides the real source of problems. Exception handling should happen at <strong>system boundaries</strong>, not in every method.</p>
<p><strong>The right way:</strong><br />
Handle exceptions at higher levels (e.g., middleware, controllers, background jobs). Let lower layers throw naturally.</p>
<blockquote>
<p>💡 <strong>ABP Tip:</strong> The ABP Framework provides a top-level exception pipeline via filters and middleware. You can focus purely on your business logic ABP automatically translates unhandled exceptions into standardized API responses.</p>
</blockquote>
<hr />
<h2>📉 5. Using Exceptions for Control Flow</h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">try
{
    var user = GetUserById(id);
}
catch (UserNotFoundException)
{
    user = CreateNewUser();
}

</code></pre>
<p><strong>Why it's a problem:</strong><br />
Exceptions are expensive and should represent <em>unexpected</em> states, not normal control flow.</p>
<p><strong>The right way:</strong></p>
<pre><code class="language-csharp">var user = GetUserByIdOrDefault(id) ?? CreateNewUser();

</code></pre>
<hr />
<h2>🪓 6. Forgetting to Await Async Calls</h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">try
{
    DoSomethingAsync(); // missing await!
}
catch (Exception ex)
{
    ...
}

</code></pre>
<p><strong>Why it's a problem:</strong><br />
Without <code>await</code>, the exception happens on another thread, outside your <code>try/catch</code>. It never gets caught.</p>
<p><strong>The right way:</strong></p>
<pre><code class="language-csharp">try
{
    await DoSomethingAsync();
}
catch (Exception ex)
{
    _logger.LogError(ex, &quot;Error during async operation&quot;);
}

</code></pre>
<hr />
<h2>🧵 7. Ignoring Background Task Exceptions</h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">Task.Run(() =&gt; SomeWork());

</code></pre>
<p><strong>Why it's a problem:</strong><br />
Unobserved task exceptions can crash your process or vanish silently, depending on configuration.</p>
<p><strong>The right way:</strong></p>
<pre><code class="language-csharp">_ = Task.Run(async () =&gt;
{
    try
    {
        await SomeWork();
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, &quot;Background task failed&quot;);
    }
});

</code></pre>
<hr />
<h2>📦 8. Throwing Generic Exceptions</h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">throw new Exception(&quot;Something went wrong&quot;);

</code></pre>
<p><strong>Why it's a problem:</strong><br />
Generic exceptions carry no semantic meaning. You can't catch or interpret them specifically later.</p>
<p><strong>The right way:</strong><br />
Use more descriptive types:</p>
<pre><code class="language-csharp">throw new InvalidOperationException(&quot;Order is already processed&quot;);

</code></pre>
<blockquote>
<p>💡 <strong>ABP Tip:</strong> In ABP applications, you can throw a <code>BusinessException</code> or <code>UserFriendlyException</code> instead.<br />
These support structured data, error codes, localization, and automatic HTTP status mapping:</p>
<pre><code class="language-csharp">throw new BusinessException(&quot;App:010046&quot;)
    .WithData(&quot;UserName&quot;, &quot;john&quot;);

</code></pre>
<p>This integrates with ABP's localization system, letting your error messages be translated automatically based on the error code.</p>
</blockquote>
<hr />
<h2>🪞 9. Losing Inner Exceptions</h2>
<p><strong>The mistake:</strong></p>
<pre><code class="language-csharp">catch (Exception ex)
{
    throw new CustomException(&quot;Failed to process order&quot;);
}

</code></pre>
<p><strong>Why it's a problem:</strong><br />
You lose the inner exception and its stack trace the real reason behind the failure.</p>
<p><strong>The right way:</strong></p>
<pre><code class="language-csharp">catch (Exception ex)
{
    throw new CustomException(&quot;Failed to process order&quot;, ex);
}

</code></pre>
<blockquote>
<p>💡 <strong>ABP Tip:</strong> ABP automatically preserves and logs inner exceptions (for example, inside <code>BusinessException</code> chains). You don't need to add boilerplate to capture nested errors just throw them properly.</p>
</blockquote>
<hr />
<h2>🧭 10. Missing Global Exception Handling</h2>
<p><strong>The mistake:</strong><br />
Catching exceptions manually in every controller.</p>
<p><strong>Why it's a problem:</strong><br />
It creates duplicated logic, inconsistent responses, and gaps in logging.</p>
<p><strong>The right way:</strong><br />
Use middleware or a global exception filter:</p>
<pre><code class="language-csharp">app.UseExceptionHandler(&quot;/error&quot;);

</code></pre>
<blockquote>
<p>💡 <strong>ABP Tip:</strong> ABP already includes a complete global exception system that:</p>
<ul>
<li><p>Logs exceptions automatically</p>
</li>
<li><p>Returns a standard <code>RemoteServiceErrorResponse</code> JSON object</p>
</li>
<li><p>Maps exceptions to correct HTTP status codes (e.g., 403 for business rules, 404 for entity not found, 400 for validation)</p>
</li>
<li><p>Allows customization through <code>AbpExceptionHttpStatusCodeOptions</code><br />
You can even implement an <code>ExceptionSubscriber</code> to react to certain exceptions (e.g., send notifications or trigger audits).</p>
</li>
</ul>
</blockquote>
<hr />
<h2>🧩 Bonus: Validation Is Not an Exception</h2>
<p><strong>The mistake:</strong><br />
Throwing exceptions for predictable user input errors.</p>
<p><strong>The right way:</strong><br />
Use proper validation instead:</p>
<pre><code class="language-csharp">[Required]
public string UserName { get; set; }

</code></pre>
<blockquote>
<p>💡 <strong>ABP Tip:</strong> ABP automatically throws an <code>AbpValidationException</code> when DTO validation fails.<br />
You don't need to handle this manually ABP formats it into a structured JSON response with <code>validationErrors</code>.</p>
</blockquote>
<hr />
<h2>🧠 Final Thoughts</h2>
<p>Exception handling isn't just about preventing crashes it's about making your failures <strong>observable, meaningful, and recoverable</strong>.<br />
When done right, your logs tell a story: <em>what happened, where, and why</em>.<br />
When done wrong, you're left staring at a 3 AM mystery.</p>
<p>By avoiding these common pitfalls and taking advantage of frameworks like ABP that handle the heavy lifting you'll spend less time chasing ghosts and more time building stable, predictable systems.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1d038a-9a2d-4688-725e-134df6ad5299" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1d038a-9a2d-4688-725e-134df6ad5299" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/demystified-aggregates-in-ddd-.net-from-theory-to-practice-2becl93q</guid>
      <link>https://abp.io/community/posts/demystified-aggregates-in-ddd-.net-from-theory-to-practice-2becl93q</link>
      <a10:author>
        <a10:name>mansur.besleney</a10:name>
        <a10:uri>https://abp.io/community/members/mansur.besleney</a10:uri>
      </a10:author>
      <category>entity-framework-core</category>
      <category>ddd</category>
      <category>dotnet</category>
      <title>Demystified Aggregates in DDD &amp; .NET: From Theory to Practice</title>
      <description>The goal of this article is to take a fresh, practical look at Aggregates and show how they can be applied in a way that works in real life.
</description>
      <pubDate>Fri, 12 Sep 2025 14:40:43 Z</pubDate>
      <a10:updated>2026-09-28T00:14:59Z</a10:updated>
      <content:encoded><![CDATA[<h1>Demystified Aggregates in DDD &amp; .NET: From Theory to Practice</h1>
<h2>Introduction</h2>
<p>Domain-Driven Design (DDD) is one of the key foundations of modern software architecture and has taken a strong place in the .NET world. At the center of DDD are Aggregates, which protect the consistency of business rules. While they are one of DDD’s biggest strengths, they’re also one of the most commonly misunderstood ideas. Trying to follow “pure” DDD rules to the letter often clashes with the complexity and performance needs of real-world projects, leaving developers in tough situations. The goal of this article is to take a fresh, practical look at Aggregates and show how they can be applied in a way that works in real life.</p>
<hr />
<h3><strong>Chapter 1: Laying the Groundwork: What Is a Classic Aggregate?</strong></h3>
<p>Before jumping into pragmatic shortcuts, let’s make sure we’re all on the same page. To do that, we’ll start with the classic “by the book” definition of an Aggregate and the rules that make it tick.</p>
<h4><strong>What Exactly Is an Aggregate?</strong></h4>
<p>At its simplest, an <strong>Aggregate</strong> is a group of related objects (Entities and Value Objects) that are treated as <strong>one unit of change</strong>. And this group has a leader: the <strong>Aggregate Root</strong>.</p>
<ul>
<li><p><strong>Aggregate Root</strong> → Think of it as the gatekeeper. All outside commands (like “add a product to the order”) must go through the root. You can’t just poke around and change stuff inside.</p>
</li>
<li><p><strong>Entity</strong> → Objects within the Aggregate that have their own identity (ID). Example: an <code>OrderLine</code> inside an <code>Order</code>.</p>
</li>
<li><p><strong>Value Object</strong> → Objects without an identity. They’re defined entirely by their values, like an <code>Address</code> or <code>Money</code>.</p>
</li>
</ul>
<p>The Aggregate’s main purpose isn’t just grouping things together—it’s about <strong>protecting business rules (invariants).</strong> For example: <em>“an order’s total amount can never be negative.”</em> The Aggregate Root makes sure rules like this are never broken.</p>
<h4><strong>The Role of Aggregates: Transaction Boundaries</strong></h4>
<p>The most important job of an Aggregate is defining the <strong>transactional consistency boundary</strong>. In other words:</p>
<p>👉 Any change you make inside an Aggregate either <strong>fully succeeds</strong> or <strong>fully fails</strong>. There’s no half-done state.</p>
<p>From a database perspective, when you call <code>SaveChanges()</code> or <code>Commit()</code>, everything within one Aggregate gets saved in a single transaction. If you add a product and update the total price, those two actions are atomic—they succeed together. Thanks to Aggregates, you’ll never end up in weird states like <em>“product was added but total wasn’t updated.”</em></p>
<h4><strong>The Golden Rules of Aggregates</strong></h4>
<p>Classic DDD lays out three golden rules for working with Aggregates:</p>
<ol>
<li><p><strong>Talk Only to the Root</strong><br />
You can’t directly update something like an <code>OrderLine</code>. You must go through the root: <code>Order.AddOrderLine(...)</code> or <code>Order.RemoveOrderLine(...)</code>. That way, the root always enforces the rules.</p>
</li>
<li><p><strong>Reference Other Aggregates by ID Only</strong><br />
An <code>Order</code> shouldn’t hold a <code>Customer</code> object directly. Instead, it should just store <code>CustomerId</code>. This keeps Aggregates independent and avoids loading massive object graphs.</p>
</li>
<li><p><strong>Change Only One Aggregate per Transaction</strong><br />
Need to create an order <em>and</em> update loyalty points? Classic DDD says: do it in two steps. First, save the <code>Order</code>. Then publish a <strong>domain event</strong> to update the <code>Customer</code>. This enables scalability but introduces <strong>eventual consistency</strong>.</p>
</li>
</ol>
<h4><strong>A Classic Example: The Order Aggregate in .NET</strong></h4>
<p>Here’s a simple example showing an <code>Order</code> Aggregate that enforces a business rule:</p>
<pre><code class="language-csharp">// Aggregate Root: The entry point and rule enforcer
public class Order
{
    public Guid Id { get; private set; }
    public Guid CustomerId { get; private set; }

    private readonly List&lt;OrderLine&gt; _orderLines = new();
    public IReadOnlyCollection&lt;OrderLine&gt; OrderLines =&gt; _orderLines.AsReadOnly();

    public decimal TotalPrice { get; private set; }

    public Order(Guid id, Guid customerId)
    {
        Id = id;
        CustomerId = customerId;
    }

    public void AddOrderLine(Guid productId, int quantity, decimal price)
    {
        // Rule 1: Max 10 order lines
        if (_orderLines.Count &gt;= 10)
            throw new InvalidOperationException(&quot;An order can contain at most 10 products.&quot;);

        // Rule 2: No duplicate products
        var existingLine = _orderLines.FirstOrDefault(ol =&gt; ol.ProductId == productId);
        if (existingLine != null)
            throw new InvalidOperationException(&quot;This product is already in the order.&quot;);

        var orderLine = new OrderLine(productId, quantity, price);
        _orderLines.Add(orderLine);

        RecalculateTotalPrice();
    }

    private void RecalculateTotalPrice()
    {
        TotalPrice = _orderLines.Sum(ol =&gt; ol.TotalPrice);
    }
}

public class OrderLine
{
    public Guid Id { get; private set; }
    public Guid ProductId { get; private set; }
    public int Quantity { get; private set; }
    public decimal UnitPrice { get; private set; }
    public decimal TotalPrice =&gt; Quantity * UnitPrice;

    public OrderLine(Guid productId, int quantity, decimal unitPrice)
    {
        Id = Guid.NewGuid();
        ProductId = productId;
        Quantity = quantity;
        UnitPrice = unitPrice;
    }
}

</code></pre>
<p>Here, the <code>Order</code> enforces the rule <em>“an order can have at most 10 items”</em> inside its <code>AddOrderLine</code> method. Nobody outside the class can bypass this, because <code>_orderLines</code> is private.</p>
<p>👉 That’s the real strength of a classic Aggregate: <strong>business rules are always protected at the boundary.</strong></p>
<hr />
<h3><strong>Chapter 2: Theory in Books vs. Reality in Code — Why Classic Aggregates Struggle</strong></h3>
<p>In Chapter 1, we painted the “ideal” world of DDD. Aggregates were like fortresses guarding our business rules…<br />
But what happens when we try to build that fortress in a real project with tools like Entity Framework Core? That’s when the gap between theory and practice starts to show up.</p>
<h4><strong>1. That <code>.Include()</code> Chain — Do We Really Need It? The Performance Trap</strong></h4>
<p>DDD books tell us: <em>“To validate a business rule, you must load the entire aggregate into memory.”</em><br />
Sounds reasonable if consistency is the goal.</p>
<p>But let’s picture a scenario: we have an <code>Order</code> aggregate with <strong>500 order lines</strong> inside it. And all we want to do is change its status to <code>Confirmed</code>.</p>
<pre><code class="language-csharp">// Just to update a single field...
var order = await _context.Orders
                          .Include(o =&gt; o.OrderLines) // &lt;-- 500 rows pulled in!
                          .SingleOrDefaultAsync(o =&gt; o.Id == orderId);

order.Confirm(); // Just sets order.Status = &quot;Confirmed&quot;;

await _context.SaveChangesAsync();

</code></pre>
<p>This query pulls <strong>all 500 order lines into memory</strong> just so we can flip a single <code>Status</code> field. Even in small projects, this is a silent performance killer. As the system grows, it will drag your app down.</p>
<h4><strong>2. The Abandoned Fortress — Sliding into Anemic Domain Models</strong></h4>
<p>Now, what’s a developer’s natural reaction to this? Something like:</p>
<p><em>“Pulling this much data is expensive. Maybe I should strip down the aggregate into a plain POCO with properties only, and move the logic into an <code>OrderService</code> class.”</em></p>
<p>This is how we slip straight into the <strong>Anemic Domain Model</strong> trap. Our classes lose their behavior, becoming nothing more than data bags.<br />
The whole DDD principle of <em>“keep behavior close to data”</em> evaporates. Business logic leaks out of the aggregate and spreads across services. We think we’re doing DDD, but in reality, we’ve fallen back into classic transaction-script style coding.</p>
<h4><strong>3. One Model Doesn’t Fit All — The Clash of Command and Query</strong></h4>
<p>Aggregates are designed for <strong>commands</strong> — write operations where business rules must be enforced.</p>
<p>But what about <strong>queries</strong>? Imagine a dashboard where we just want to list the last 10 orders. All we need is <code>OrderId</code>, <code>CustomerName</code>, and <code>TotalAmount</code>.</p>
<p>Loading 10 fully-hydrated <code>Order</code> aggregates (with all their order lines) just for that list? That’s like using a cannon to hunt a sparrow. Wasteful, slow, and clumsy.<br />
Aggregates simply aren’t built for reporting or read-heavy scenarios.</p>
<p>And there you have it — the three usual suspects that make developers doubt DDD in real life:</p>
<ul>
<li><p>Performance headaches</p>
</li>
<li><p>The risk of falling into an Anemic Model</p>
</li>
<li><p>Aggregates being too heavy for read operations</p>
</li>
</ul>
<p>So, should we give up on DDD? Absolutely not!<br />
The key is to stop following the rules blindly and instead focus on their <strong>real intent</strong>. In the next chapter, we’ll explore the pragmatic approach — <strong>Demystified Aggregates</strong> — and how they can actually help us solve these problems.</p>
<hr />
<h3><strong>Chapter 3: Enter the Solution — What Exactly Is a &quot;Demystified Aggregate&quot;?</strong></h3>
<p>The issues we listed in the last chapter don’t mean DDD is bad. They just show that blindly applying textbook rules without considering the realities of your project creates friction.</p>
<p>A <strong>Demystified Aggregate</strong> isn’t a library or a framework. It’s a <strong>way of thinking</strong>. Its philosophy is simple: focus on the Aggregate’s real job, and make sure it does that job <strong>as efficiently as possible.</strong></p>
<h4><strong>1. Philosophy: Focus on Purpose, Not Rules</strong></h4>
<p>What’s the Aggregate’s most sacred duty?<br />
<strong>To protect business rules (invariants) during a data change (command).</strong></p>
<p>Here’s the key: an Aggregate’s job isn’t to always hold all data in memory. Its job is to <strong>ensure consistency while performing an operation</strong>.</p>
<p>Think of it like a security guard at a bank vault. Their job is to make sure transfers are done correctly. They don’t need to memorize the serial number of every single banknote. They just need the critical info for the current operation: the balance and the transfer amount.</p>
<p>The Demystified Aggregate says the same thing: when running a method, you <strong>only load the data that method actually needs</strong>, not the entire Aggregate.</p>
<h4><strong>2. The Core Idea: What “State” Does a Behavior Actually Need?</strong></h4>
<p>To apply this idea in code, ask yourself:<br />
<em>“What data does the <code>Confirm()</code> method on my <code>Order</code> Aggregate actually need?”</em></p>
<ul>
<li><p>Maybe just the order’s current <code>Status</code>. (<code>&quot;Pending&quot;</code> can become <code>&quot;Confirmed&quot;</code>, <code>&quot;Cancelled&quot;</code> throws an error.)</p>
</li>
<li><p>What about <code>AddItem(product, quantity)</code>?</p>
<ul>
<li><p>It needs the <code>Status</code> (can’t add items to a cancelled order).</p>
</li>
<li><p>And maybe the existing <code>OrderLines</code> (to increase quantity if the item already exists).</p>
</li>
</ul>
</li>
</ul>
<p>See the pattern? Each behavior needs different data. So why load everything every single time?</p>
<h4><strong>3. How Do We Do This in .NET &amp; EF Core? Practical Solutions</strong></h4>
<p>Putting this philosophy into code is easier than you might think.</p>
<p><strong>The Approach: Purpose-Built Repository Methods</strong></p>
<p>Instead of a generic <code>GetByIdAsync()</code>, create methods tailored to the operation at hand. Let’s revisit our classic <strong>Order Confirmation</strong> scenario in a “Before &amp; After” style.</p>
<p><strong>BEFORE (Classic &amp; Inefficient Approach)</strong></p>
<pre><code class="language-csharp">// Repository Layer
public async Task&lt;Order&gt; GetByIdAsync(Guid id)
{
    // LOAD EVERYTHING!
    return await _context.Orders
                         .Include(o =&gt; o.OrderLines)
                         .SingleOrDefaultAsync(o =&gt; o.Id == id);
}

// Application Service Layer
public async Task ConfirmOrderAsync(Guid orderId)
{
    var order = await _orderRepository.GetByIdAsync(orderId);
    order.Confirm(); // This method might not even care about OrderLines!
    await _unitOfWork.SaveChangesAsync();
}

</code></pre>
<p><strong>AFTER (Demystified &amp; Focused Approach)</strong></p>
<pre><code class="language-csharp">// Repository Layer
public async Task&lt;Order&gt; GetForConfirmationAsync(Guid id)
{
    // LOAD ONLY WHAT WE NEED! (No OrderLines needed)
    return await _context.Orders
                         .SingleOrDefaultAsync(o =&gt; o.Id == id);
}

// Application Service Layer
public async Task ConfirmOrderAsync(Guid orderId)
{
    // Intent is crystal clear in the code!
    var order = await _orderRepository.GetForConfirmationAsync(orderId);
    
    // Aggregate still protects the business rule.
    // Confirm() checks status, etc.
    order.Confirm(); 
    
    await _unitOfWork.SaveChangesAsync();
}

</code></pre>
<p><strong>What Do We Gain?</strong></p>
<ol>
<li><p><strong>Awesome Performance:</strong> We avoid unnecessary JOINs and data transfer.</p>
</li>
<li><p><strong>Clear Intent:</strong> Anyone reading <code>GetForConfirmationAsync</code> immediately knows this operation only cares about the order itself, not its items. Code documents itself.</p>
</li>
<li><p><strong>No Compromise:</strong> Our Aggregate still enforces the business rules via <code>Confirm()</code>. DDD’s spirit remains intact.</p>
</li>
</ol>
<p>For <strong>read/query operations</strong>, the answer is even simpler: skip Aggregates altogether! Use optimized queries that return DTOs via <code>Select</code> projections, or even raw SQL with Dapper.</p>
<p>That’s the essence of a Demystified Aggregate: <strong>using the right tool for the right job.</strong></p>
<p>In the next chapter, we’ll wrap everything up and tie all the concepts together.</p>
<hr />
<h3><strong>Conclusion: Pragmatism Beats Dogmatism in DDD</strong></h3>
<p>We’ve reached the finish line. We started with the “pure” textbook definition of Aggregates in the ideal world of Domain-Driven Design. Then we hit the real-world walls of performance and complexity. Finally, we learned how to break through those walls.</p>
<p>The biggest lesson from the <strong>Demystified Aggregates</strong> approach is simple:</p>
<p><strong>DDD isn’t a rigid rulebook — it’s a way of thinking.</strong></p>
<p>Our goal isn’t to implement the “most pure DDD ever written in a book.” It’s to make our domain logic clean, solid, understandable, and performant. In this journey, patterns and rules should serve us, not the other way around.</p>
<h3><strong>Key Takeaways</strong></h3>
<ol>
<li><p><strong>Focus on the Core Purpose:</strong><br />
The primary reason an Aggregate exists is to enforce business rules (invariants) and ensure consistency while handling a command. Every design decision should revolve around this purpose.</p>
</li>
<li><p><strong>Load Only What You Need:</strong><br />
You don’t have to load the entire Aggregate to execute a behavior. Use purpose-built repository methods (<code>GetForX()</code>) to fetch just the data needed for the operation. This can drastically improve both performance and readability.</p>
</li>
<li><p><strong>Separate Writing from Reading:</strong><br />
Use rich, protected Aggregates for commands (write operations). For queries (read operations), don’t burden your Aggregates. Instead, rely on projections, DTOs, or optimized queries. This is one of the simplest, most practical ways to embrace CQRS principles.</p>
</li>
</ol>
<p>Don’t be afraid to shape your Aggregates based on your project and the realities of your tools (like Entity Framework Core). The power of DDD lies in its <strong>flexibility and pragmatism</strong>.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1c508b-ef2d-94f1-46e1-ce3162eee56f" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1c508b-ef2d-94f1-46e1-ce3162eee56f" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-build-persistent-background-jobs-with-abp-framework-and-quartz-n9aloh93</guid>
      <link>https://abp.io/community/posts/how-to-build-persistent-background-jobs-with-abp-framework-and-quartz-n9aloh93</link>
      <a10:author>
        <a10:name>mansur.besleney</a10:name>
        <a10:uri>https://abp.io/community/members/mansur.besleney</a10:uri>
      </a10:author>
      <title>How to Build Persistent Background Jobs with ABP Framework and Quartz</title>
      <description>Build reliable background jobs with ABP Framework and Quartz.NET. Learn persistent PostgreSQL storage, QuartzBackgroundWorkerBase implementation, ICachedServiceProvider usage, trigger types, misfire handling, and create a complete subscription reminder system with email notifications.</description>
      <pubDate>Thu, 31 Jul 2025 08:30:26 Z</pubDate>
      <a10:updated>2026-09-28T05:38:40Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Build Persistent Background Jobs with ABP Framework and Quartz</h1>
<h2>Introduction</h2>
<p>In modern SaaS applications, automated background processing is essential for delivering reliable user experiences. Whether you're sending subscription reminders, processing payments, or generating reports, background jobs ensure critical tasks happen on schedule without blocking your main application flow.</p>
<h3>What is <code>Quartz.NET</code>?</h3>
<p><code>Quartz.NET</code> is a powerful, open-source job scheduling library for .NET applications that provides cron-based scheduling for complex time patterns, job persistence across application restarts, clustering support for high-availability scenarios, flexible trigger types, and the ability to pass parameters to jobs through job data maps. It's the de facto standard for enterprise-grade job scheduling in the .NET ecosystem.</p>
<h3>Quartz Storage Options: In-Memory vs Persistent</h3>
<p>When configuring <strong>Quartz</strong>, you have two primary storage options, each with significant implications for how your application behaves:</p>
<h3>🧠 In-Memory Storage (<code>RAMJobStore</code>)</h3>
<ul>
<li>Keeps all job information in application memory.</li>
<li><strong>Very fast</strong> – no database overhead.</li>
<li><strong>Volatile</strong> – all jobs, triggers, and schedules are lost when the application stops or restarts.</li>
<li>Best suited for:
<ul>
<li>Development environments.</li>
<li>Scenarios where job loss is acceptable.</li>
</ul>
</li>
</ul>
<h3>🗃️ Persistent Storage (<code>JobStoreTX</code> or similar)</h3>
<ul>
<li>Stores all job information in a database.</li>
<li><strong>Reliable</strong> – schedules persist across:
<ul>
<li>Application restarts</li>
<li>Server crashes</li>
<li>Deployments</li>
</ul>
</li>
<li><strong>Supports horizontal scaling</strong> – multiple application instances can share the same job queue.</li>
<li><strong>Slight performance overhead</strong> due to database I/O.</li>
<li>Best choice for:
<ul>
<li>Production systems.</li>
<li>Any scenario where <strong>business continuity and reliability</strong> are critical.</li>
</ul>
</li>
</ul>
<h3>How ABP Simplifies Quartz Integration</h3>
<p>ABP handles Quartz configuration, dependency injection, and lifecycle management automatically. Developers define jobs using <code>QuartzBackgroundWorkerBase</code> and access services via <code>ICachedServiceProvider</code>, following ABP's standard conventions and leveraging optimal service caching for background job scenarios.</p>
<h3>Benefits of the Integration</h3>
<ul>
<li>Full support for ABP’s cross-cutting concerns (e.g., multi-tenancy, localization)</li>
<li>Robust scheduling powered by Quartz</li>
<li>Built-in logging, error handling, and performance monitoring</li>
<li>Scales easily without modifying business logic</li>
</ul>
<h3>Real-World Use Case: Subscription Reminders</h3>
<p>In this tutorial, we'll build a subscription reminder system that monitors client subscriptions, identifies those nearing expiration, sends professional email reminders seven days before expiration, tracks reminder history to prevent duplicates, and runs automatically every day at 9:00 AM using Quartz scheduling with PostgreSQL persistence. This system demonstrates how ABP and Quartz work together to solve real business problems with clean, maintainable code that follows enterprise-grade patterns.</p>
<h2>Installing and Configuring Quartz</h2>
<p>Getting Quartz up and running in an ABP application is straightforward thanks to ABP's dedicated integration package. We'll replace the default background job system with Quartz for persistent job storage and robust scheduling capabilities.</p>
<h3>Adding the Quartz Package</h3>
<p>The easiest way to add Quartz support to your ABP application is using the ABP CLI. Open a terminal in your project directory and run:</p>
<pre><code class="language-bash">abp add-package Volo.Abp.BackgroundWorkers.Quartz
</code></pre>
<p>This command automatically adds the necessary NuGet package reference and updates your module dependencies. The ABP CLI handles all the heavy lifting, ensuring you get the correct version that matches your ABP Framework version.</p>
<h3>Configuring Quartz for Persistent Storage</h3>
<p>Once the package is installed, you need to configure Quartz to use your database (in my case it is PostgreSQL) for job persistence. This configuration goes in your main module's <code>PreConfigureServices</code> method:</p>
<pre><code class="language-csharp">[DependsOn(
    // ... other dependencies
    typeof(AbpBackgroundJobsQuartzModule),
    typeof(AbpBackgroundWorkersQuartzModule)
)]
public class MySaaSApplicationModule : AbpModule
{
    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        var hostingEnvironment = context.Services.GetHostingEnvironment();
        var configuration = context.Services.GetConfiguration();

        ConfigureAuthentication(context, configuration);
        ConfigureUrls(configuration);
        ConfigureImpersonation(context, configuration);
        ConfigureQuartz(); // Add this line
    }

    private void ConfigureQuartz()
    {
        PreConfigure&lt;AbpQuartzOptions&gt;(options =&gt;
        {
            options.Properties = new NameValueCollection
            {
                [&quot;quartz.scheduler.instanceName&quot;] = &quot;QuartzScheduler&quot;,
                [&quot;quartz.jobStore.type&quot;] = &quot;Quartz.Impl.AdoJobStore.JobStoreTX, Quartz&quot;,
                [&quot;quartz.jobStore.tablePrefix&quot;] = &quot;qrtz_&quot;,
                [&quot;quartz.jobStore.dataSource&quot;] = &quot;myDS&quot;,
                [&quot;quartz.dataSource.myDS.connectionString&quot;] = _configuration.GetConnectionString(&quot;Default&quot;),
                [&quot;quartz.dataSource.myDS.provider&quot;] = &quot;Npgsql&quot;,
                [&quot;quartz.serializer.type&quot;] = &quot;json&quot;
            };
        });
    }
}
</code></pre>
<p>This configuration tells Quartz to store all job information in your PostgreSQL database using tables prefixed with &quot;qrtz_&quot;. The key points are:</p>
<ul>
<li><strong>Job Store Type</strong>: Uses ADO.NET with transaction support for reliable job persistence</li>
<li><strong>Connection String</strong>: Shares your application's existing database connection</li>
<li><strong>Table Prefix</strong>: Keeps Quartz tables separate with the &quot;qrtz_&quot; prefix</li>
<li><strong>JSON Serialization</strong>: Makes job data readable and debuggable</li>
<li><strong>PostgreSQL Provider</strong>: Uses Npgsql for optimal PostgreSQL integration</li>
</ul>
<p>When your application starts, ABP automatically initializes the Quartz scheduler with these settings. Any background workers you create will be registered and scheduled automatically, with their state persisted to the database for reliability across application restarts.</p>
<p>For detailed installation options and advanced configuration scenarios, check the official <a href="https://abp.io/docs/latest/framework/infrastructure/background-workers/quartz">ABP documentation.</a></p>
<h2>Database Setup for Quartz</h2>
<p>With Quartz configured for persistent storage, we need to create the necessary database tables where Quartz will store job definitions, triggers, and execution history. Rather than running SQL scripts directly against the database, we'll use Entity Framework migrations to maintain consistency with ABP's database management approach.</p>
<h3>Creating an Empty Migration for Quartz Tables</h3>
<p>Instead of executing raw SQL scripts against the database, we created an empty Entity Framework migration and populated it with the required Quartz table definitions. This approach keeps all database changes within the migration system, ensuring they're version-controlled, repeatable, and consistent across different environments.</p>
<p>To create the empty migration, we used the standard Entity Framework CLI command:</p>
<pre><code class="language-bash">dotnet ef migrations add AddQuartzTables
</code></pre>
<p>This generates a new migration file with empty <code>Up</code> and <code>Down</code> methods that we can populate with the Quartz table creation scripts.</p>
<h3>Adding Quartz SQL Schema to Migration</h3>
<p>Once the empty migration was created, we populated it with the PostgreSQL-specific SQL needed to create all Quartz tables. The SQL scripts were obtained from the official Quartz repository, which provides database schema scripts for various database providers:</p>
<pre><code class="language-csharp">public partial class AddQuartzTables : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@&quot;
            CREATE TABLE qrtz_job_details (
                sched_name VARCHAR(120) NOT NULL,
                job_name VARCHAR(200) NOT NULL,
                job_group VARCHAR(200) NOT NULL,
                description VARCHAR(250) NULL,
                job_class_name VARCHAR(250) NOT NULL,
                is_durable BOOLEAN NOT NULL,
                is_nonconcurrent BOOLEAN NOT NULL,
                is_update_data BOOLEAN NOT NULL,
                requests_recovery BOOLEAN NOT NULL,
                job_data BYTEA NULL,
                PRIMARY KEY (sched_name, job_name, job_group)
            );

            CREATE TABLE qrtz_triggers (
                sched_name VARCHAR(120) NOT NULL,
                trigger_name VARCHAR(200) NOT NULL,
                trigger_group VARCHAR(200) NOT NULL,
                job_name VARCHAR(200) NOT NULL,
                job_group VARCHAR(200) NOT NULL,
                -- ... additional columns and constraints
                PRIMARY KEY (sched_name, trigger_name, trigger_group),
                FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details(sched_name, job_name, job_group)
            );

            -- Additional tables: qrtz_simple_triggers, qrtz_cron_triggers, 
            -- qrtz_simprop_triggers, qrtz_blob_triggers, qrtz_calendars,
            -- qrtz_paused_trigger_grps, qrtz_fired_triggers, qrtz_scheduler_state, qrtz_locks
        &quot;);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@&quot;
            DROP TABLE IF EXISTS qrtz_locks;
            DROP TABLE IF EXISTS qrtz_scheduler_state;
            -- ... drop all other Quartz tables in reverse order
            DROP TABLE IF EXISTS qrtz_triggers;
            DROP TABLE IF EXISTS qrtz_job_details;
        &quot;);
    }
}
</code></pre>
<p>The complete SQL scripts for all supported database providers, including PostgreSQL, MySQL, SQL Server, and others, can be found in the official <code>Quartz.NET</code> repository. You should use the script that matches your specific database provider and version requirements.</p>
<h3>Why Use Migrations Instead of Direct SQL Scripts?</h3>
<p>This migration-based approach offers several important advantages over running SQL scripts directly:</p>
<p><strong>Version Control Integration</strong>: The migration becomes part of your codebase, tracked in source control alongside your application changes. This means every developer and deployment environment gets the exact same database schema.</p>
<p><strong>Rollback Capability</strong>: The <code>Down</code> method provides a clean way to remove Quartz tables if needed, something that's much harder to manage with standalone SQL scripts.</p>
<p><strong>Environment Consistency</strong>: Whether you're setting up a development machine, staging server, or production deployment, running DBMigrator or <code>dotnet ef database update</code> command ensures the same schema is created everywhere.</p>
<p><strong>Integration with ABP's Database Management</strong>: This approach aligns perfectly with how ABP manages all other database changes, keeping your database evolution strategy consistent.</p>
<p>The Quartz tables created by this migration handle all aspects of job persistence - from storing job definitions and triggers to tracking execution history and managing scheduler state. With these tables in place, your Quartz scheduler can reliably persist jobs across application restarts and coordinate work across multiple application instances if needed.</p>
<p>After creating this migration, running DBMigrator <code>dotnet ef database update</code> will create all the necessary Quartz infrastructure in your PostgreSQL database, ready to store and manage your background jobs.</p>
<p>For complete SQL scripts for your specific database provider, visit the official <a href="https://www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html#creating-and-initializing-database">Quartz documentation.</a></p>
<h2>Building the Business Logic</h2>
<p>Before implementing our Quartz background job, we needed to create the essential business entities and services that our subscription reminder system would work with. Since this article focuses on Quartz integration rather than general ABP development patterns, we'll keep this section brief and move quickly to the background job implementation.</p>
<h3>Core Entities and Services</h3>
<p>For our subscription reminder system, we created the following core components:</p>
<p><strong>Entities:</strong></p>
<ul>
<li><strong><code>Client</code></strong>: Represents customers with subscription information (Name, Email, SubscriptionEnd, IsActive)</li>
<li><strong><code>ReminderLog</code></strong>: Tracks when reminder emails have been sent to prevent duplicates</li>
</ul>
<p><strong>Application Services:</strong></p>
<ul>
<li><strong><code>ClientAppService</code></strong>: Handles CRUD operations and provides methods to find clients with expiring subscriptions</li>
<li><strong><code>ReminderLogAppService</code></strong>: Manages reminder history and prevents duplicate notifications</li>
<li><strong><code>EmailService</code></strong>: Sends professional HTML reminder emails via SMTP</li>
</ul>
<p><strong>Data Transfer Objects (DTOs):</strong></p>
<ul>
<li>Complete set of DTOs for both entities following ABP conventions</li>
<li>Input/output DTOs for all service operations</li>
</ul>
<h3>Business Logic Overview</h3>
<p>The system follows standard ABP patterns with entities inheriting from <code>FullAuditedAggregateRoot</code>, services implementing <code>ICrudAppService</code> interfaces, and proper AutoMapper configurations for entity-DTO mapping. We also included a data seeder to create sample clients for testing purposes.</p>
<p>The key business methods our background job will use are:</p>
<ul>
<li><code>GetExpiringClientsAsync()</code> - Finds clients whose subscriptions expire in the next 7 days</li>
<li><code>CreateAsync()</code> - Logs when a reminder has been sent</li>
<li><code>SendSubscriptionExpiryReminderAsync()</code> - Sends professional email reminders</li>
</ul>
<h3>Focus on Background Operations</h3>
<p>Rather than diving deep into ABP entity creation, repository patterns, or service layer implementation details, we'll move directly to the heart of this article: implementing robust background jobs with Quartz. The entities and services we created simply provide the business context for our background job to operate within.</p>
<p>The real value of this tutorial lies in showing how ABP's <code>QuartzBackgroundWorkerBase</code> integrates seamlessly with your business logic to create reliable, persistent background operations that survive application restarts and scale across multiple instances.</p>
<p>Let's now implement the background job that ties everything together and demonstrates the power of ABP + Quartz integration.</p>
<h2>Implementing the Background Job (The ABP Way)</h2>
<p>This is where the magic happens. ABP's integration with Quartz provides a clean, powerful way to create background jobs that follow framework conventions while leveraging Quartz's robust scheduling capabilities. Let's dive into how we implemented our subscription reminder job and explore the advanced features ABP provides.</p>
<h3>Creating a QuartzBackgroundWorkerBase Job</h3>
<p>Instead of implementing Quartz's raw <code>IJob</code> interface, ABP provides <code>QuartzBackgroundWorkerBase</code>, which integrates seamlessly with ABP's dependency injection, logging, and lifecycle management systems:</p>
<pre><code class="language-csharp">public class SubscriptionExpiryNotifierJob : QuartzBackgroundWorkerBase
{
    public SubscriptionExpiryNotifierJob()
    {
        // Configure the job to run daily at 9:00 AM
        JobDetail = JobBuilder.Create&lt;SubscriptionExpiryNotifierJob&gt;()
            .WithIdentity(nameof(SubscriptionExpiryNotifierJob))
            .Build();

        Trigger = TriggerBuilder.Create()
            .WithIdentity(nameof(SubscriptionExpiryNotifierJob))
            .WithCronSchedule(&quot;0 0 9 * * ?&quot;) // Every day at 9:00 AM
            .Build();

        ScheduleJob = async scheduler =&gt;
        {
            if (!await scheduler.CheckExists(JobDetail.Key))
            {
                await scheduler.ScheduleJob(JobDetail, Trigger);
            }
        };
    }

    public override async Task Execute(IJobExecutionContext context)
    {
        // Use ICachedServiceProvider for better performance and proper scoping
        var serviceProvider = ServiceProvider.GetRequiredService&lt;ICachedServiceProvider&gt;();
        
        // These services will be cached and reused throughout the job execution
        var clientAppService = serviceProvider.GetRequiredService&lt;IClientAppService&gt;();
        var reminderLogAppService = serviceProvider.GetRequiredService&lt;IReminderLogAppService&gt;();
        var emailService = serviceProvider.GetRequiredService&lt;IEmailService&gt;();

        Logger.LogInformation(&quot;🔄 Starting subscription expiry notification job...&quot;);

        // 1. Get clients expiring in 7 days
        var expiringClients = await clientAppService.GetExpiringClientsAsync(7);
        
        Logger.LogInformation(&quot;📋 Found {Count} clients with expiring subscriptions&quot;, expiringClients.Count);

        // 2. Process each client
        foreach (var client in expiringClients)
        {
            await ProcessClientAsync(client, emailService, reminderLogAppService);
        }

        Logger.LogInformation(&quot;✅ Job completed successfully&quot;);
    }
}
</code></pre>
<h3>Key Implementation Features</h3>
<p><strong>Constructor-Based Configuration</strong>: Unlike traditional Quartz jobs that require external scheduling code, ABP's approach lets you define both the job and its schedule directly in the constructor. This keeps related configuration together and makes the job self-contained.</p>
<p><strong>ABP Service Integration</strong>: The <code>ICachedServiceProvider</code> gives you access to any service in ABP's dependency injection container, enabling you to use application services, repositories, domain services, or any other ABP component with optimized caching and proper scoping.</p>
<p><strong>Built-in Logging</strong>: The <code>Logger</code> property provides access to ABP's logging infrastructure, automatically including context like correlation IDs and tenant information in multi-tenant applications.</p>
<p><strong>Custom Scheduling Logic</strong>: The <code>ScheduleJob</code> property allows you to customize how the job gets registered with Quartz. In our example, we check if the job already exists before scheduling it, preventing duplicate registrations during application restarts.</p>
<h3>Understanding Quartz Trigger Types</h3>
<p>Quartz provides several trigger types to handle different scheduling requirements. Choosing the right trigger type is crucial for your job's behavior and performance.</p>
<h4>CronTrigger - Complex Time-Based Scheduling</h4>
<p>CronTrigger uses cron expressions for sophisticated scheduling patterns. This is what we used for our daily subscription reminders:</p>
<pre><code class="language-csharp">// Daily at 9:00 AM
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;DailyReminder&quot;)
    .WithCronSchedule(&quot;0 0 9 * * ?&quot;)
    .Build();

// Every weekday at 2:30 PM
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;WeekdayReport&quot;)
    .WithCronSchedule(&quot;0 30 14 ? * MON-FRI&quot;)
    .Build();

// First day of every month at midnight
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;MonthlyCleanup&quot;)
    .WithCronSchedule(&quot;0 0 0 1 * ?&quot;)
    .Build();
</code></pre>
<p><strong>Cron Expression Format</strong>: <code>Seconds Minutes Hours Day-of-Month Month Day-of-Week Year(optional)</code></p>
<ul>
<li><code>0 0 9 * * ?</code> = 9:00 AM every day</li>
<li><code>0 */15 * * * ?</code> = Every 15 minutes</li>
<li><code>0 0 12 ? * SUN</code> = Every Sunday at noon</li>
</ul>
<h4>SimpleTrigger - Interval-Based Scheduling</h4>
<p>SimpleTrigger is perfect for jobs that need to run at regular intervals or a specific number of times:</p>
<pre><code class="language-csharp">// Run every 30 seconds indefinitely
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;HealthCheck&quot;)
    .StartNow()
    .WithSimpleSchedule(x =&gt; x
        .WithIntervalInSeconds(30)
        .RepeatForever())
    .Build();

// Run every 5 minutes, but only 10 times
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;LimitedRetry&quot;)
    .StartNow()
    .WithSimpleSchedule(x =&gt; x
        .WithIntervalInMinutes(5)
        .WithRepeatCount(9)) // 0-based, so 9 = 10 executions
    .Build();

// One-time execution after 1 hour delay
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;DelayedCleanup&quot;)
    .StartAt(DateTimeOffset.UtcNow.AddHours(1))
    .Build();
</code></pre>
<h4>CalendarIntervalTrigger - Calendar-Aware Intervals</h4>
<p>CalendarIntervalTrigger handles intervals that need to respect calendar boundaries:</p>
<pre><code class="language-csharp">// Every month on the same day (handles varying month lengths)
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;MonthlyBilling&quot;)
    .WithCalendarIntervalSchedule(x =&gt; x
        .WithIntervalInMonths(1))
    .Build();

// Every week, starting Monday
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;WeeklyReport&quot;)
    .WithCalendarIntervalSchedule(x =&gt; x
        .WithIntervalInWeeks(1))
    .Build();
</code></pre>
<h4>DailyTimeIntervalTrigger - Time Windows</h4>
<p>DailyTimeIntervalTrigger runs jobs within specific time windows on certain days:</p>
<pre><code class="language-csharp">// Every 2 hours between 8 AM and 6 PM, Monday through Friday
Trigger = TriggerBuilder.Create()
    .WithIdentity(&quot;BusinessHoursSync&quot;)
    .WithDailyTimeIntervalSchedule(x =&gt; x
        .OnMondayThroughFriday()
        .StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(8, 0))
        .EndingDailyAt(TimeOfDay.HourAndMinuteOfDay(18, 0))
        .WithIntervalInHours(2))
    .Build();
</code></pre>
<h3>Choosing the Right Trigger Type</h3>
<p>For different scenarios, you'd choose different trigger types:</p>
<ul>
<li><strong>Daily/Weekly/Monthly Operations</strong>: Use <strong>CronTrigger</strong> for maximum flexibility</li>
<li><strong>High-Frequency Tasks</strong>: Use <strong>SimpleTrigger</strong> for performance (every few seconds/minutes)</li>
<li><strong>Business Calendar Operations</strong>: Use <strong>CalendarIntervalTrigger</strong> for month-end reports, quarterly tasks</li>
<li><strong>Business Hours Operations</strong>: Use <strong>DailyTimeIntervalTrigger</strong> for operations that should only run during specific hours</li>
</ul>
<h3>Automatic Job Registration</h3>
<p>One of ABP's most powerful features is automatic job discovery and registration. When your application starts, ABP automatically:</p>
<ol>
<li><strong>Scans for Background Workers</strong>: ABP discovers all classes inheriting from <code>QuartzBackgroundWorkerBase</code></li>
<li><strong>Registers with DI Container</strong>: Each job is registered as a service in the dependency injection container</li>
<li><strong>Schedules with Quartz</strong>: ABP calls the <code>ScheduleJob</code> delegate to register the job with the Quartz scheduler</li>
<li><strong>Handles Lifecycle</strong>: ABP manages starting and stopping jobs with the application lifecycle</li>
</ol>
<p>This means you simply create your job class, and ABP handles everything else. No manual registration, no startup code, no configuration files - it just works.</p>
<h3>Understanding Misfire Handling</h3>
<p>Misfires occur when a scheduled job cannot execute at its intended time, typically due to system downtime, resource constraints, or the scheduler being paused. Quartz provides several misfire instructions to handle these scenarios:</p>
<h4>CronTrigger Misfire Instructions</h4>
<p>For cron-based schedules like our daily reminder job, Quartz offers these misfire behaviors:</p>
<p><strong><code>MisfireInstruction.DoNothing</code></strong> (Default):</p>
<pre><code class="language-csharp">Trigger = TriggerBuilder.Create()
    .WithIdentity(nameof(SubscriptionExpiryNotifierJob))
    .WithCronSchedule(&quot;0 0 9 * * ?&quot;, x =&gt; x.WithMisfireHandlingInstructionDoNothing())
    .Build();
</code></pre>
<ul>
<li>Skips all missed executions</li>
<li>Waits for the next naturally scheduled time</li>
<li>Best for jobs where missing executions is acceptable</li>
</ul>
<p><strong><code>MisfireInstruction.FireOnceNow</code></strong>:</p>
<pre><code class="language-csharp">.WithCronSchedule(&quot;0 0 9 * * ?&quot;, x =&gt; x.WithMisfireHandlingInstructionFireAndProceed())
</code></pre>
<ul>
<li>Immediately executes one missed job upon recovery</li>
<li>Then continues with the normal schedule</li>
<li>Useful when you need to catch up on missed work</li>
</ul>
<p><strong><code>MisfireInstruction.IgnoreMisfires</code></strong>:</p>
<pre><code class="language-csharp">.WithCronSchedule(&quot;0 0 9 * * ?&quot;, x =&gt; x.WithMisfireHandlingInstructionIgnoreMisfires())
</code></pre>
<ul>
<li>Executes all missed jobs immediately upon recovery</li>
<li>Can cause a burst of executions after extended downtime</li>
<li>Use carefully to avoid overwhelming the system</li>
</ul>
<h4>SimpleTrigger Misfire Instructions</h4>
<p>Simple triggers have their own set of misfire behaviors:</p>
<p><strong><code>MisfireInstruction.FireNow</code></strong>: Execute immediately when recovered
<strong><code>MisfireInstruction.RescheduleNowWithExistingRepeatCount</code></strong>: Start over with remaining repeat count
<strong><code>MisfireInstruction.RescheduleNowWithRemainingRepeatCount</code></strong>: Continue as if no misfire occurred
<strong><code>MisfireInstruction.RescheduleNextWithExistingCount</code></strong>: Wait for next interval, keep original repeat count</p>
<h3>Real-World Misfire Considerations</h3>
<p>For our subscription reminder system, we chose the default <code>DoNothing</code> behavior because:</p>
<ul>
<li><strong>Business Logic</strong>: Sending yesterday's reminder today might confuse customers</li>
<li><strong>Duplicate Prevention</strong>: Our job checks for existing reminders, so running late won't cause duplicate emails</li>
<li><strong>Resource Management</strong>: We avoid overwhelming the email system after extended downtime</li>
</ul>
<p>However, for other scenarios you might choose differently:</p>
<ul>
<li><strong>Financial reporting</strong>: Use <code>FireOnceNow</code> to ensure reports are always generated</li>
<li><strong>Data synchronization</strong>: Use <code>IgnoreMisfires</code> to process all missed sync operations</li>
<li><strong>Cache warming</strong>: Use <code>DoNothing</code> since stale cache warming provides no value</li>
</ul>
<h3>Advanced Job Features</h3>
<p><strong>Error Handling and Resilience</strong>: Our job implementation includes comprehensive error handling for individual client processing, ensuring one failed email doesn't stop the entire batch:</p>
<pre><code class="language-csharp">try
{
    await emailService.SendSubscriptionExpiryReminderAsync(/*...*/);
    await LogReminderAsync(client.Id, client.SubscriptionEnd, &quot;Email sent successfully&quot;, reminderLogAppService);
}
catch (Exception ex)
{
    Logger.LogError(ex, &quot;❌ Failed to send reminder to {ClientName}&quot;, client.Name);
    await LogReminderAsync(client.Id, client.SubscriptionEnd, $&quot;Failed: {ex.Message}&quot;, reminderLogAppService);
}
</code></pre>
<p><strong>Duplicate Prevention</strong>: The job checks for existing reminders to prevent sending multiple emails on the same day, even if the job runs multiple times:</p>
<pre><code class="language-csharp">private async Task&lt;bool&gt; AlreadySentTodayAsync(Guid clientId, IReminderLogAppService reminderLogAppService)
{
    var todayReminders = await reminderLogAppService.GetByClientIdAsync(clientId);
    var today = DateTime.UtcNow.Date;
    
    return todayReminders.Any(r =&gt; r.ReminderDate.Date == today);
}
</code></pre>
<p>This implementation demonstrates how ABP's <code>QuartzBackgroundWorkerBase</code> provides a clean, powerful foundation for building robust background jobs that integrate seamlessly with your business logic while leveraging Quartz's enterprise-grade scheduling capabilities.</p>
<h2>Conclusion</h2>
<p>You've successfully built a production-ready subscription reminder system that demonstrates the powerful synergy between ABP Framework and <code>Quartz.NET</code>. This isn't just a tutorial example - it's a robust, enterprise-grade solution that handles real business requirements.</p>
<h3>What We Accomplished</h3>
<p><strong>✅ Enterprise-Grade Reliability</strong>: PostgreSQL persistence ensures jobs survive restarts and deployments<br />
<strong>✅ ABP Best Practices</strong>: Used <code>QuartzBackgroundWorkerBase</code>, <code>ICachedServiceProvider</code>, and ABP's logging infrastructure<br />
<strong>✅ Real Business Value</strong>: Automated subscription reminders with duplicate prevention and audit logging<br />
<strong>✅ Flexible Scheduling</strong>: Explored cron expressions, trigger types, and misfire handling strategies</p>
<h3>The Power of ABP + Quartz Integration</h3>
<p>The combination delivers exceptional value through automatic job discovery, persistent scheduling, built-in dependency injection, and seamless framework integration. You get enterprise reliability with developer-friendly simplicity.</p>
<h3>Final Thoughts</h3>
<p>Complex background processing doesn't have to be complicated to implement. ABP's thoughtful abstractions combined with Quartz's proven engine create a development experience that's both powerful and enjoyable.</p>
<p>Whether you're building subscription management, financial reporting, or data synchronization, these patterns provide a solid foundation for reliable, maintainable solutions.</p>
<p>You can reach sample project's source code from <a href="https://github.com/MansurBesleney/MySaaSApplication">here</a></p>
<p><strong>Happy coding, and may your background jobs never miss a beat!</strong> 🚀</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1b71c7-7c72-c67d-8037-90dc22486b53" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1b71c7-7c72-c67d-8037-90dc22486b53" medium="image" />
    </item>
  </channel>
</rss>