<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged asp</title>
<link>https://coderlegion.com/tag/asp</link>
<description>Powered by Question2Answer</description>
<item>
<title>Your Refresh Tokens Are Probably Leaking Access Right Now</title>
<link>https://coderlegion.com/16356/your-refresh-tokens-are-probably-leaking-access-right-now</link>
<description>&lt;h2 id=&quot;the-refresh-token-problem-nobody-talks-about&quot;&gt;The Refresh Token Problem Nobody Talks About&lt;/h2&gt;
&lt;p&gt;Most developers think JWT authentication is straightforward.&lt;/p&gt;
&lt;p&gt;You issue a token. You validate it. You move on.&lt;/p&gt;
&lt;p&gt;It works perfectly in local development. Clean. Predictable. Simple.&lt;/p&gt;
&lt;p&gt;Until you try to build something real.&lt;/p&gt;
&lt;p&gt;Something with real users. Real sessions. Real security expectations.&lt;/p&gt;
&lt;p&gt;That is when refresh tokens stop being &quot;just an implementation detail.&quot; They become the hardest part of your entire system.&lt;/p&gt;
&lt;p&gt;And nobody warns you about this.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-things-start-to-break&quot;&gt;Where things start to break&lt;/h3&gt;
&lt;p&gt;Access tokens are easy to understand.&lt;/p&gt;
&lt;p&gt;Short lived. Stateless. Disposable.&lt;/p&gt;
&lt;p&gt;You can reason about them quickly. They come in, they do their job, they expire. Done.&lt;/p&gt;
&lt;p&gt;But refresh tokens are different.&lt;/p&gt;
&lt;p&gt;They introduce state into a system that was supposed to be stateless. And suddenly you are asking questions that most tutorials never answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where do you store refresh tokens safely?&lt;/li&gt;
&lt;li&gt;Do you allow multiple active sessions per user?&lt;/li&gt;
&lt;li&gt;How do you rotate tokens without breaking users mid-session?&lt;/li&gt;
&lt;li&gt;What happens when a token is stolen?&lt;/li&gt;
&lt;li&gt;How do you actually revoke access?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At this point, the system stops being &quot;just JWT auth.&quot; It becomes a session management problem.&lt;/p&gt;
&lt;p&gt;And that is a completely different beast.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-real-production-issue&quot;&gt;The real production issue&lt;/h3&gt;
&lt;p&gt;The real challenge is not issuing tokens.&lt;/p&gt;
&lt;p&gt;It is controlling identity over time.&lt;/p&gt;
&lt;p&gt;Because once a refresh token exists, you have essentially created a long lived key to your system. A key that can outlive passwords. That can survive logouts if you are not careful. That can be stolen and used for days or weeks.&lt;/p&gt;
&lt;p&gt;Now you need to answer something uncomfortable:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do I take access away after I have already given it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Most implementations do not really solve this. They either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ignore revocation completely&lt;/li&gt;
&lt;li&gt;Rely on short expirations and hope the user experience survives&lt;/li&gt;
&lt;li&gt;Build custom logic that becomes fragile over time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these scale cleanly in real systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-failure-patterns-show-up-later&quot;&gt;The failure patterns show up later&lt;/h3&gt;
&lt;p&gt;You do not notice the problem immediately.&lt;/p&gt;
&lt;p&gt;It shows up slowly.&lt;/p&gt;
&lt;p&gt;A user logs out but still has access somewhere. A stolen token keeps working until expiry. Multiple devices behave inconsistently. Session state becomes impossible to reason about.&lt;/p&gt;
&lt;p&gt;And the worst part?&lt;/p&gt;
&lt;p&gt;Everything still &lt;em&gt;looks&lt;/em&gt; correct in code. The auth middleware passes. The refresh endpoint returns a 200. Your logs show nothing obviously wrong.&lt;/p&gt;
&lt;p&gt;But your system is leaking access.&lt;/p&gt;
&lt;p&gt;I have seen this happen on production systems with thousands of users. The team did everything by the book. And still, refresh tokens became the weak link.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-most-tutorials-fail-you&quot;&gt;Why most tutorials fail you&lt;/h3&gt;
&lt;p&gt;Tutorials show you how to &lt;em&gt;issue&lt;/em&gt; a refresh token. They rarely show you how to &lt;em&gt;manage&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;They will give you a nice endpoint that returns a refresh token and an access token. Maybe they store the refresh token in a database. And then… they stop.&lt;/p&gt;
&lt;p&gt;They do not tell you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to handle token reuse on the same device&lt;/li&gt;
&lt;li&gt;How to detect a stolen token when the attacker and real user both try to refresh&lt;/li&gt;
&lt;li&gt;What to do when two concurrent requests try to rotate the same token&lt;/li&gt;
&lt;li&gt;How to clean up expired tokens without leaking user data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You learn these things the hard way. Usually after an incident. Usually during a long night of debugging.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-actually-matters-in-production&quot;&gt;What actually matters in production&lt;/h3&gt;
&lt;p&gt;Once you hit production, refresh tokens stop being about authentication.&lt;/p&gt;
&lt;p&gt;They become about &lt;strong&gt;control&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here is what actually matters:&lt;/p&gt;
&lt;h4 id=&quot;rotation-strategies&quot;&gt;Rotation strategies&lt;/h4&gt;
&lt;p&gt;Do you issue a new refresh token on every refresh? Or keep the same one? Each approach has trade offs. Rotation is safer but harder to get right.&lt;/p&gt;
&lt;h4 id=&quot;reuse-detection&quot;&gt;Reuse detection&lt;/h4&gt;
&lt;p&gt;If a refresh token is used twice, someone probably stole it. Your system needs to detect this and revoke everything immediately.&lt;/p&gt;
&lt;h4 id=&quot;revocation-logic&quot;&gt;Revocation logic&lt;/h4&gt;
&lt;p&gt;Can you remotely kill a refresh token? Can you revoke all tokens for a user? Can you revoke a specific device? These are not optional in serious systems.&lt;/p&gt;
&lt;h4 id=&quot;storage-safety&quot;&gt;Storage safety&lt;/h4&gt;
&lt;p&gt;HttpOnly cookies vs mobile storage vs web workers. Each has different risk profiles. Most people get this wrong because the browser fights you at every step.&lt;/p&gt;
&lt;h4 id=&quot;session-tracking&quot;&gt;Session tracking&lt;/h4&gt;
&lt;p&gt;How do you know what devices are active? How do you show users their sessions? How do you let them remotely log out of an old phone they lost?&lt;/p&gt;
&lt;p&gt;Most developers only learn this after rebuilding the system more than once.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-hidden-cost-of-doing-it-yourself&quot;&gt;The hidden cost of doing it yourself&lt;/h3&gt;
&lt;p&gt;I have built JWT auth from scratch four times.&lt;/p&gt;
&lt;p&gt;Every time, I thought &quot;this time will be different.&quot; Every time, refresh tokens humbled me.&lt;/p&gt;
&lt;p&gt;The hidden cost is not the initial implementation. It is the ongoing maintenance. The edge cases you missed. The subtle bugs that only show up under load or with specific race conditions.&lt;/p&gt;
&lt;p&gt;One team I worked with had a refresh token bug for eight months. Eight months. They thought logout was working. It was not. Tokens kept refreshing silently in the background. Users showed up as &quot;active&quot; weeks after leaving the platform.&lt;/p&gt;
&lt;p&gt;That is the kind of problem that erodes trust in your system.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-ended-up-doing&quot;&gt;What I ended up doing&lt;/h3&gt;
&lt;p&gt;After going through this a few times, I stopped treating it like a one off implementation.&lt;/p&gt;
&lt;p&gt;I sat down and asked: what would a production ready refresh token system actually need to include?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proper rotation with reuse detection&lt;/li&gt;
&lt;li&gt;Revocation that actually works&lt;/li&gt;
&lt;li&gt;Multiple session support with device tracking&lt;/li&gt;
&lt;li&gt;Clean handling of concurrent refresh requests&lt;/li&gt;
&lt;li&gt;Secure storage guidance for different client types&lt;/li&gt;
&lt;li&gt;Role based access control layered on top&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not a tutorial version. A production shaped one.&lt;/p&gt;
&lt;p&gt;I packaged it into a clean, reusable setup for .NET APIs. It handles the hard parts so you do not have to debug race conditions at 2 AM.&lt;/p&gt;
&lt;p&gt;If you are building anything with real users and real sessions, it might save you the rebuild I went through.&lt;/p&gt;
&lt;p&gt;You can find it here:&lt;br&gt;
&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://yaman95.gumroad.com/l/advanced-plug-and-play-dot-net-api-security-kit&quot;&gt;https://yaman95.gumroad.com/l/advanced-plug-and-play-dot-net-api-security-kit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final thought&lt;/h3&gt;
&lt;p&gt;JWT auth is not the hard part.&lt;/p&gt;
&lt;p&gt;The hard part is everything around it. The part that decides whether your system stays secure after it goes live. The part that most tutorials skip because it is not flashy.&lt;/p&gt;
&lt;p&gt;Refresh tokens are not a feature you add at the end. They are a design decision that shapes everything from your database schema to your logout flow to your incident response plan.&lt;/p&gt;
&lt;p&gt;Don't learn this the hard way like I did.&lt;/p&gt;
&lt;p&gt;Start with the hard questions early. And if you want a head start, you know where to look.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16356/your-refresh-tokens-are-probably-leaking-access-right-now</guid>
<pubDate>Fri, 01 May 2026 22:00:15 +0000</pubDate>
</item>
<item>
<title># The Rule Pattern in C#, and Why It Works So Well with the Result Pattern</title>
<link>https://coderlegion.com/14143/the-rule-pattern-in-c-and-why-it-works-so-well-with-the-result-pattern</link>
<description>&lt;h2 id=&quot;the-rule-pattern-in-c-and-why-it-works-so-well-with-the-result-pattern&quot;&gt;The Rule Pattern in C#, and Why It Works So Well with the Result Pattern&lt;/h2&gt;
&lt;p&gt;Business rules rarely stay in one place.&lt;/p&gt;
&lt;p&gt;A small validation starts in a command handler. Another check appears in a service. A third gets added somewhere in the domain. A few weeks later, the real business policy is scattered across the application, and nobody can see it clearly anymore.&lt;/p&gt;
&lt;p&gt;That is exactly where the Rule pattern becomes useful.&lt;/p&gt;
&lt;p&gt;At its core, the idea is simple. Instead of expressing business decisions as anonymous conditionals spread throughout the system, you give each rule a name and wrap it in its own object. Martin Fowler describes this style through the Specification pattern, where the statement of what satisfies a candidate is separated from the candidate itself, and those specifications can be combined into richer logic. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/apsupp/spec.pdf?utm_source=chatgpt.com&quot; title=&quot;Specifications&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;In practical C# applications, especially the ones using CQRS or domain-driven design, that gives you something powerful: business rules that are explicit, reusable, and easy to reason about. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/apsupp/spec.pdf?utm_source=chatgpt.com&quot; title=&quot;Specifications&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/p&gt;
&lt;h3 id=&quot;what-the-rule-pattern-really-solves&quot;&gt;What the Rule pattern really solves&lt;/h3&gt;
&lt;p&gt;The biggest problem is not that business checks exist. The problem is that they often exist in the wrong shape.&lt;/p&gt;
&lt;p&gt;When rules are buried inside handlers, services, or controller actions, they are hard to spot, hard to test in isolation, and very easy to duplicate. The code still works, but the business language disappears.&lt;/p&gt;
&lt;p&gt;The Rule pattern fixes that by turning each business condition into a named concept. Instead of asking developers to interpret a chain of low-level checks, the code starts speaking in terms the business would actually recognize. That is very much in line with DDD’s emphasis on modeling around the real language of the domain. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice?utm_source=chatgpt.com&quot; title=&quot;Designing a DDD-oriented microservice - .NET&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;In other words, the pattern is not just about validation. It is about clarity.&lt;/p&gt;
&lt;h3 id=&quot;why-it-is-often-called-the-specification-pattern&quot;&gt;Why it is often called the Specification pattern&lt;/h3&gt;
&lt;p&gt;If the shape feels familiar, that is because it usually is. What many teams call a Rule pattern is very close to what Fowler documented as Specification. The core idea is to represent business criteria as composable objects rather than raw conditionals, and then combine them with logical operations like AND, OR, and NOT. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/apsupp/spec.pdf?utm_source=chatgpt.com&quot; title=&quot;Specifications&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;That is why this pattern tends to feel so natural in real systems. Business rules are rarely isolated. More often, they are layered:&lt;/p&gt;
&lt;p&gt;A customer must be verified.&lt;br&gt;
An order must reach a minimum amount.&lt;br&gt;
A credit limit must not be exceeded.&lt;/p&gt;
&lt;p&gt;On their own, each rule is simple. The real value appears when they can be combined into a meaningful policy.&lt;/p&gt;
&lt;h3 id=&quot;why-this-pattern-becomes-more-valuable-as-the-system-grows&quot;&gt;Why this pattern becomes more valuable as the system grows&lt;/h3&gt;
&lt;p&gt;At first, a few inline checks seem harmless. But as the application grows, the cost of hidden business logic grows with it.&lt;/p&gt;
&lt;p&gt;Rules get copied into multiple workflows.&lt;br&gt;
One endpoint enforces them, another forgets.&lt;br&gt;
A handler applies three checks, a background job applies only two.&lt;br&gt;
Tests start focusing on branches instead of policies.&lt;/p&gt;
&lt;p&gt;When rules become standalone concepts, those problems start to shrink. You can test a rule directly. You can reuse it in multiple flows. You can combine it with other rules. Most importantly, you can read the application flow and understand what it is trying to protect.&lt;/p&gt;
&lt;p&gt;That is one of the reasons this pattern fits so well in systems that care about maintainability.&lt;/p&gt;
&lt;h3 id=&quot;where-the-result-pattern-fits&quot;&gt;Where the Result pattern fits&lt;/h3&gt;
&lt;p&gt;The Rule pattern answers one question:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is this action allowed according to the business?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Result pattern answers another:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happened when we tried to perform it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That separation is the reason the two patterns fit together so well.&lt;/p&gt;
&lt;p&gt;The rule evaluates business truth.&lt;br&gt;
The result communicates application outcome.&lt;/p&gt;
&lt;p&gt;Vladimir Khorikov’s writing on Result-based error handling makes this distinction especially useful for expected failures. Instead of throwing exceptions for normal business rejections, you can return an explicit failure outcome and keep the flow predictable. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://enterprisecraftsmanship.com/posts/error-handling-exception-or-result/?utm_source=chatgpt.com&quot; title=&quot;Error handling: Exception or Result?&quot;&gt;Enterprise Craftsmanship&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;That gives you a very clean mental model:&lt;/p&gt;
&lt;p&gt;The domain decides.&lt;br&gt;
The application reports.&lt;/p&gt;
&lt;h3 id=&quot;why-this-pairing-feels-so-clean-in-practice&quot;&gt;Why this pairing feels so clean in practice&lt;/h3&gt;
&lt;p&gt;A good rule object should not care about HTTP, API responses, logging, UI wording, or transport concerns. Its responsibility is much smaller. It answers whether a business condition holds.&lt;/p&gt;
&lt;p&gt;The application layer then takes that answer and decides what to do next. If the rule fails, the application can return a failure result. If it passes, the use case continues.&lt;/p&gt;
&lt;p&gt;This separation lines up well with Microsoft’s DDD guidance, which treats validations as invariants and emphasizes that aggregates are responsible for enforcing consistency across state changes. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-model-layer-validations?utm_source=chatgpt.com&quot; title=&quot;Designing validations in the domain model layer - .NET&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;That is why the combination works so well. The rule keeps business logic clean. The result keeps flow control clean.&lt;/p&gt;
&lt;h3 id=&quot;a-simple-order-example-without-the-code&quot;&gt;A simple order example, without the code&lt;/h3&gt;
&lt;p&gt;Imagine an order placement use case with three policies:&lt;/p&gt;
&lt;p&gt;The order must meet a minimum amount.&lt;br&gt;
The customer must be verified.&lt;br&gt;
The order must stay within the customer’s credit limit.&lt;/p&gt;
&lt;p&gt;Without a rule-based approach, those checks often end up inline inside the command handler. That works, but the handler slowly becomes the place where business policy lives.&lt;/p&gt;
&lt;p&gt;With a rule-based approach, those policies become named rules. The handler simply coordinates the workflow: load data, build the order, evaluate the rule set, return a success or failure result, and persist when everything is valid.&lt;/p&gt;
&lt;p&gt;That is a much healthier separation of concerns.&lt;/p&gt;
&lt;p&gt;The handler stops being the place where business logic is invented, and becomes the place where business logic is orchestrated.&lt;/p&gt;
&lt;h3 id=&quot;the-biggest-benefit-is-not-flexibility-it-is-readability&quot;&gt;The biggest benefit is not flexibility, it is readability&lt;/h3&gt;
&lt;p&gt;People often talk about this pattern in terms of reuse or composability, and those are real benefits.&lt;/p&gt;
&lt;p&gt;But the biggest benefit is readability.&lt;/p&gt;
&lt;p&gt;When rules are named well, the code starts reading like a business conversation instead of a technical negotiation. That matters more than people think. Clear code reduces mistakes, speeds up maintenance, and makes onboarding easier for everyone who touches the system later.&lt;/p&gt;
&lt;p&gt;A long chain of conditionals tells you how the system behaves.&lt;/p&gt;
&lt;p&gt;A well-named rule tells you why.&lt;/p&gt;
&lt;h3 id=&quot;one-important-design-note&quot;&gt;One important design note&lt;/h3&gt;
&lt;p&gt;There is one trap worth mentioning.&lt;/p&gt;
&lt;p&gt;Just because a rule is expressed cleanly does not automatically mean it belongs in the application layer.&lt;/p&gt;
&lt;p&gt;Some rules are just use-case checks. Those can live comfortably near the workflow.&lt;/p&gt;
&lt;p&gt;But some rules are true invariants of the domain. Those should usually be protected by the aggregate itself. Microsoft’s guidance is very clear on this point: aggregates are responsible for enforcing invariants across state changes. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-model-layer-validations?utm_source=chatgpt.com&quot; title=&quot;Designing validations in the domain model layer - .NET&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;So the right question is not only, “Can I model this as a rule?”&lt;/p&gt;
&lt;p&gt;The better question is, “Who is responsible for protecting this rule?”&lt;/p&gt;
&lt;p&gt;If the answer is “the domain itself,” then the aggregate should not rely on the handler to remember it.&lt;/p&gt;
&lt;h3 id=&quot;where-this-pattern-can-go-wrong&quot;&gt;Where this pattern can go wrong&lt;/h3&gt;
&lt;p&gt;Like most good patterns, this one becomes dangerous when overused.&lt;/p&gt;
&lt;p&gt;Not every &lt;code&gt;if&lt;/code&gt; statement needs to become a rule object.&lt;br&gt;
Not every workflow needs an entire rules engine.&lt;br&gt;
Not every tiny validation deserves its own abstraction.&lt;/p&gt;
&lt;p&gt;If the business logic is trivial and local, introducing rule classes may create more ceremony than value.&lt;/p&gt;
&lt;p&gt;The pattern shines when the rules are meaningful, reusable, and important enough to deserve a name. If the condition is just a one-off technical check, a plain conditional may still be the better choice.&lt;/p&gt;
&lt;p&gt;The goal is not abstraction for its own sake. The goal is to make business intent easier to see.&lt;/p&gt;
&lt;h3 id=&quot;why-the-result-pattern-keeps-the-design-honest&quot;&gt;Why the Result pattern keeps the design honest&lt;/h3&gt;
&lt;p&gt;The Result pattern adds a second layer of clarity.&lt;/p&gt;
&lt;p&gt;A failure is no longer hidden behind a thrown exception for something the system fully expected. Instead, the caller gets an explicit outcome. That makes business rejections feel normal rather than exceptional, which is often the right model for application workflows. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://enterprisecraftsmanship.com/posts/error-handling-exception-or-result/?utm_source=chatgpt.com&quot; title=&quot;Error handling: Exception or Result?&quot;&gt;Enterprise Craftsmanship&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;When paired with rules, the flow becomes very natural:&lt;/p&gt;
&lt;p&gt;Evaluate the business policy.&lt;br&gt;
If it fails, return a failure result.&lt;br&gt;
If it passes, continue the operation.&lt;/p&gt;
&lt;p&gt;That is simple, predictable, and easy to test.&lt;/p&gt;
&lt;h3 id=&quot;final-thought-1&quot;&gt;Final thought&lt;/h3&gt;
&lt;p&gt;The Rule pattern is one of those ideas that looks modest at first, but pays off more and more as the codebase grows.&lt;/p&gt;
&lt;p&gt;It helps you name business policies.&lt;br&gt;
It helps you reuse them.&lt;br&gt;
It helps you combine them.&lt;br&gt;
And when paired with the Result pattern, it gives your application a very clean way to both enforce rules and communicate outcomes. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/apsupp/spec.pdf?utm_source=chatgpt.com&quot; title=&quot;Specifications&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;If your handlers are starting to fill up with business conditionals, this pattern is often a strong sign that the code wants a better language.&lt;/p&gt;
&lt;p&gt;And that, more than anything else, is what good domain design is really about.&lt;/p&gt;
&lt;p&gt;Source code :&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/RulePatternApplication&quot;&gt;https://github.com/stevsharp/RulePatternApplication&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;p&gt;Martin Fowler, &lt;em&gt;Specifications&lt;/em&gt;. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/apsupp/spec.pdf?utm_source=chatgpt.com&quot; title=&quot;Specifications&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Microsoft Learn, &lt;em&gt;Designing validations in the domain model layer&lt;/em&gt;. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-model-layer-validations?utm_source=chatgpt.com&quot; title=&quot;Designing validations in the domain model layer - .NET&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Microsoft Learn, &lt;em&gt;Designing a DDD-oriented microservice&lt;/em&gt;. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice?utm_source=chatgpt.com&quot; title=&quot;Designing a DDD-oriented microservice - .NET&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Vladimir Khorikov, &lt;em&gt;Error handling: Exception or Result?&lt;/em&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://enterprisecraftsmanship.com/posts/error-handling-exception-or-result/?utm_source=chatgpt.com&quot; title=&quot;Error handling: Exception or Result?&quot;&gt;Enterprise Craftsmanship&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Vladimir Khorikov, &lt;em&gt;Advanced error handling techniques&lt;/em&gt;. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://enterprisecraftsmanship.com/posts/advanced-error-handling-techniques/?utm_source=chatgpt.com&quot; title=&quot;Advanced error handling techniques&quot;&gt;Enterprise Craftsmanship&lt;/a&gt;)&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14143/the-rule-pattern-in-c-and-why-it-works-so-well-with-the-result-pattern</guid>
<pubDate>Tue, 07 Apr 2026 10:01:22 +0000</pubDate>
</item>
<item>
<title>ASP.NET Core Custom Logger — Save Logs to Database (SQL)</title>
<link>https://coderlegion.com/10763/asp-net-core-custom-logger-save-logs-to-database-sql</link>
<description>&lt;p&gt;&lt;strong&gt;ASP.NET Core&lt;/strong&gt; already gives you solid logging out of the box, and if you want richer sinks and structured logs, Serilog is a common choice.&lt;/p&gt;
&lt;p&gt;But sometimes you cannot use a ready made sink, because the company already has a legacy logging table with a fixed schema. In that case, you build a custom database logger that writes into that table.&lt;/p&gt;
&lt;p&gt;This tutorial includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database table&lt;/li&gt;
&lt;li&gt;Logger options&lt;/li&gt;
&lt;li&gt;Custom logger&lt;/li&gt;
&lt;li&gt;Logger provider&lt;/li&gt;
&lt;li&gt;Background queue (Channel)&lt;/li&gt;
&lt;li&gt;Dapper SQL insert&lt;/li&gt;
&lt;li&gt;Registration in Program.cs&lt;/li&gt;
&lt;li&gt;Usage example&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-create-the-sql-table&quot;&gt;1️⃣ Create the SQL table&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE dbo.AppLogs
(
    LogId     BIGINT IDENTITY PRIMARY KEY,
    Ts        DATETIME2(3) NOT NULL,
    Level     NVARCHAR(20) NOT NULL,
    Category  NVARCHAR(200) NOT NULL,
    TraceId   NVARCHAR(64) NULL,
    Message   NVARCHAR(MAX) NOT NULL,
    Exception NVARCHAR(MAX) NULL,
    Scopes    NVARCHAR(MAX) NULL
);

CREATE INDEX IX_AppLogs_Ts    ON dbo.AppLogs(Ts);
CREATE INDEX IX_AppLogs_Level ON dbo.AppLogs(Level);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-add-nuget-packages&quot;&gt;2️⃣ Add NuGet packages&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Dapper
dotnet add package Microsoft.Data.SqlClient
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-logger-options&quot;&gt;3️⃣ Logger options&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class DbLogOptions
{
    public string ConnectionString { get; set; } = string.Empty;
    public LogLevel MinLevel { get; set; } = LogLevel.Information;

    // Max number of log entries waiting in memory
    public int MaxQueue { get; set; } = 5000;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-log-entry-model&quot;&gt;4️⃣ Log entry model&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class DbLogEntry
{
    public DateTime Ts { get; set; }
    public string Level { get; set; } = &quot;&quot;;
    public string Category { get; set; } = &quot;&quot;;
    public string? TraceId { get; set; }
    public string Message { get; set; } = &quot;&quot;;
    public string? Exception { get; set; }
    public string? Scopes { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-custom-database-logger&quot;&gt;5️⃣ Custom database logger&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Diagnostics;
using System.Text.Json;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;

public sealed class DbLogger : ILogger
{
    private readonly string _category;
    private readonly DbLogOptions _options;
    private readonly Channel&amp;lt;DbLogEntry&amp;gt; _channel;
    private readonly IExternalScopeProvider? _scopeProvider;

    public DbLogger(
        string category,
        DbLogOptions options,
        Channel&amp;lt;DbLogEntry&amp;gt; channel,
        IExternalScopeProvider? scopeProvider)
    {
        _category = category;
        _options = options;
        _channel = channel;
        _scopeProvider = scopeProvider;
    }

    public IDisposable BeginScope&amp;lt;TState&amp;gt;(TState state) =&amp;gt;
        _scopeProvider?.Push(state) ?? NullScope.Instance;

    public bool IsEnabled(LogLevel logLevel) =&amp;gt; logLevel &amp;gt;= _options.MinLevel;

    public void Log&amp;lt;TState&amp;gt;(
        LogLevel logLevel,
        EventId eventId,
        TState state,
        Exception? exception,
        Func&amp;lt;TState, Exception?, string&amp;gt; formatter)
    {
        if (!IsEnabled(logLevel)) return;
        if (formatter is null) return;

        var traceId = Activity.Current?.TraceId.ToString();

        string? scopesJson = null;
        if (_scopeProvider is not null)
        {
            var scopes = new List&amp;lt;object?&amp;gt;();
            _scopeProvider.ForEachScope((s, list) =&amp;gt; list.Add(s), scopes);

            if (scopes.Count &amp;gt; 0)
                scopesJson = JsonSerializer.Serialize(scopes);
        }

        var entry = new DbLogEntry
        {
            Ts = DateTime.UtcNow,
            Level = logLevel.ToString(),
            Category = _category,
            TraceId = traceId,
            Message = formatter(state, exception),
            Exception = exception?.ToString(),
            Scopes = scopesJson
        };

        // If queue is full, this returns false and the log is dropped
        _ = _channel.Writer.TryWrite(entry);
    }

    private sealed class NullScope : IDisposable
    {
        public static readonly NullScope Instance = new();
        public void Dispose() { }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6-logger-provider-writes-to-sql-using-dapper-&quot;&gt;6️⃣ Logger provider (writes to SQL using Dapper)&lt;/h3&gt;
&lt;p&gt;This provider uses a bounded channel and a single background reader so request threads never block on SQL.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Threading.Channels;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

public sealed class DbLoggerProvider : ILoggerProvider, ISupportExternalScope
{
    private readonly DbLogOptions _options;
    private readonly Channel&amp;lt;DbLogEntry&amp;gt; _channel;
    private readonly CancellationTokenSource _cts = new();
    private IExternalScopeProvider? _scopeProvider;

    public DbLoggerProvider(IOptions&amp;lt;DbLogOptions&amp;gt; options)
    {
        _options = options.Value;

        var channelOptions = new BoundedChannelOptions(_options.MaxQueue)
        {
            SingleReader = true,
            SingleWriter = false,
            FullMode = BoundedChannelFullMode.DropOldest
        };

        _channel = Channel.CreateBounded&amp;lt;DbLogEntry&amp;gt;(channelOptions);

        _ = Task.Run(ProcessQueueAsync);
    }

    public ILogger CreateLogger(string categoryName) =&amp;gt;
        new DbLogger(categoryName, _options, _channel, _scopeProvider);

    public void SetScopeProvider(IExternalScopeProvider scopeProvider) =&amp;gt;
        _scopeProvider = scopeProvider;

    public void Dispose()
    {
        _cts.Cancel();
        _channel.Writer.TryComplete();
        _cts.Dispose();
    }

    private async Task ProcessQueueAsync()
    {
        const string sql = @&quot;
INSERT INTO dbo.AppLogs (Ts, Level, Category, TraceId, Message, Exception, Scopes)
VALUES (@Ts, @Level, @Category, @TraceId, @Message, @Exception, @Scopes);&quot;;

        try
        {
            while (await _channel.Reader.WaitToReadAsync(_cts.Token))
            {
                while (_channel.Reader.TryRead(out var entry))
                {
                    await using var conn = new SqlConnection(_options.ConnectionString);
                    await conn.ExecuteAsync(sql, entry);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // graceful shutdown
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;7-add-registration-extension&quot;&gt;7️⃣ Add registration extension&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public static class DbLoggerExtensions
{
    public static ILoggingBuilder AddDbLogger(
        this ILoggingBuilder builder,
        Action&amp;lt;DbLogOptions&amp;gt; configure)
    {
        builder.Services.Configure(configure);
        builder.Services.AddSingleton&amp;lt;ILoggerProvider, DbLoggerProvider&amp;gt;();
        return builder;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;8-register-in-program-cs&quot;&gt;8️⃣ Register in Program.cs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole();

builder.Logging.AddDbLogger(options =&amp;gt;
{
    options.ConnectionString = builder.Configuration.GetConnectionString(&quot;DefaultConnection&quot;)!;
    options.MinLevel = LogLevel.Information;
    options.MaxQueue = 5000;
});

var app = builder.Build();

app.MapGet(&quot;/test&quot;, (ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
{
    logger.LogInformation(&quot;Hello from DB Logger&quot;);
    logger.LogError(new Exception(&quot;Test&quot;), &quot;Something failed&quot;);
    return Results.Ok(&quot;OK&quot;);
});

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-we-used-channels-in-the-db-logger&quot;&gt;Why we used Channels in the DB logger&lt;/h3&gt;
&lt;p&gt;Your logger needs to accept logs from many producers (web requests, background jobs, services) and write them to the DB via one efficient writer without blocking request threads or losing control when traffic spikes. Channels are a great fit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Non blocking producers&lt;/li&gt;
&lt;li&gt;Backpressure with bounded capacity&lt;/li&gt;
&lt;li&gt;Single reader, many writers&lt;/li&gt;
&lt;li&gt;Async, allocation friendly&lt;/li&gt;
&lt;li&gt;Graceful shutdown&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source code:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/DbLoggerSample&quot;&gt;https://github.com/stevsharp/DbLoggerSample&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10763/asp-net-core-custom-logger-save-logs-to-database-sql</guid>
<pubDate>Fri, 30 Jan 2026 09:30:43 +0000</pubDate>
</item>
<item>
<title>Carter in a CQRS API, advantages and alternatives.</title>
<link>https://coderlegion.com/9342/carter-in-a-cqrs-api-advantages-and-alternatives</link>
<description>&lt;h3 id=&quot;carter-in-a-cqrs-api-advantages-and-alternatives-&quot;&gt;Carter in a CQRS API, advantages and alternatives.&lt;/h3&gt;
&lt;p&gt;In this project I wanted the API layer to stay small and structured. I like minimal APIs, but I don’t like having 200 endpoints inside &lt;code&gt;Program.cs&lt;/code&gt;. That’s why I used &lt;strong&gt;Carter&lt;/strong&gt;, it keeps the minimal API style and gives you a module system to organize endpoints by feature.&lt;/p&gt;
&lt;p&gt;Repo used in this article:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://github.com/stevsharp/ChainOfResponsibilityWithMediatR
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;advantages-of-carter&quot;&gt;Advantages of Carter&lt;/h3&gt;
&lt;h4 id=&quot;1-feature-based-organization-without-controllers&quot;&gt;1) Feature-based organization without controllers&lt;/h4&gt;
&lt;p&gt;Carter pushes you to group endpoints in modules, for example &lt;code&gt;PurchaseOrdersModule&lt;/code&gt;. This fits well with CQRS because you already think in features (Create, Approve, Reject, Get, List).&lt;/p&gt;
&lt;h4 id=&quot;2-minimal-api-feeling-but-more-maintainable&quot;&gt;2) Minimal API feeling, but more maintainable&lt;/h4&gt;
&lt;p&gt;You still write endpoints in a minimal way (no heavy controller attributes), but you don’t end up with a giant &lt;code&gt;Program.cs&lt;/code&gt;. Microsoft itself suggests structuring and organizing minimal APIs once they grow, Carter just gives a ready approach for it.&lt;/p&gt;
&lt;h4 id=&quot;3-cleaner-api-layer-when-using-mediatr&quot;&gt;3) Cleaner API layer when using MediatR&lt;/h4&gt;
&lt;p&gt;When you combine Carter + MediatR, the endpoint usually becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;map route&lt;/li&gt;
&lt;li&gt;bind request&lt;/li&gt;
&lt;li&gt;call &lt;code&gt;Send&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;return result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So your API layer stays thin and does not become “business logic + validation + database code”.&lt;/p&gt;
&lt;h4 id=&quot;4-testing-and-readability&quot;&gt;4) Testing and readability&lt;/h4&gt;
&lt;p&gt;Modules are easy to scan. If you want to see all Purchase Orders endpoints, you open one file. That’s a small thing, but in real teams it saves time.&lt;/p&gt;
&lt;h4 id=&quot;5-works-nicely-with-swagger-and-standard-middleware&quot;&gt;5) Works nicely with Swagger and standard middleware&lt;/h4&gt;
&lt;p&gt;Carter is still based on ASP.NET Core endpoints, so you can keep standard middleware, and Swagger still works (with the normal setup).&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;alternatives&quot;&gt;Alternatives&lt;/h3&gt;
&lt;h4 id=&quot;alternative-1-controllers&quot;&gt;Alternative 1, Controllers&lt;/h4&gt;
&lt;p&gt;Controllers (heavier): &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you want strong conventions and attributes everywhere&lt;/li&gt;
&lt;li&gt;you already have a big MVC style codebase&lt;/li&gt;
&lt;li&gt;your team prefers the familiar controller structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Less good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you want very thin endpoints and feature modules&lt;/li&gt;
&lt;li&gt;you end up with “controller bloat” and many attributes&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;alternative-2-pure-minimal-apis-no-carter-&quot;&gt;Alternative 2, Pure Minimal APIs (no Carter)&lt;/h4&gt;
&lt;p&gt;Minimal APIs are great, and they are official. If your API is small, this is the simplest approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;small service with few endpoints&lt;/li&gt;
&lt;li&gt;you want the simplest possible setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Less good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;endpoints grow and you need structure&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Program.cs&lt;/code&gt; becomes huge&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Microsoft has guidance on organizing minimal APIs because this is a real pain point once the API gets bigger.&lt;/p&gt;
&lt;h4 id=&quot;alternative-3-fastendpoints-another-popular-option-&quot;&gt;Alternative 3, FastEndpoints (another popular option)&lt;/h4&gt;
&lt;p&gt;FastEndpoints is another library with a very feature oriented style, it also keeps endpoints separated and organized.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you want a strong endpoint-per-class structure&lt;/li&gt;
&lt;li&gt;you like conventions for validation, mapping, and request/response patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Less good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you want the thinnest layer possible over native ASP.NET Core endpoints&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;alternative-4-minimal-apis-with-endpoint-groups-manual-modules-&quot;&gt;Alternative 4, Minimal APIs with Endpoint Groups (manual modules)&lt;/h4&gt;
&lt;p&gt;You can also do your own “module system” without extra libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;extension methods like &lt;code&gt;app.MapPurchaseOrderEndpoints()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;group routes with &lt;code&gt;MapGroup(&quot;/api/purchase-orders&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a valid approach and stays 100% framework-only.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you want no external dependency&lt;/li&gt;
&lt;li&gt;you like simple grouping patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Less good when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you still need to enforce structure across a team&lt;/li&gt;
&lt;li&gt;endpoint definitions become scattered if not disciplined&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-i-still-chose-carter-here&quot;&gt;Why I still chose Carter here&lt;/h3&gt;
&lt;p&gt;For this project size, Carter gave me the best balance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;minimal code style&lt;/li&gt;
&lt;li&gt;good organization&lt;/li&gt;
&lt;li&gt;no heavy framework feeling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And with CQRS + MediatR, it keeps the flow very clean: endpoint → mediator → handler.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references-1&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/CarterCommunity/Carter&quot;&gt;Carter official repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0&quot;&gt;Microsoft guidance on organizing minimal APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-8.0&amp;amp;tabs=visual-studio&quot;&gt;Swagger for ASP.NET Core (Swashbuckle, &lt;code&gt;AddEndpointsApiExplorer&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/ChainOfResponsibilityWithMediatR&quot;&gt;Project repo used in this article&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/9342/carter-in-a-cqrs-api-advantages-and-alternatives</guid>
<pubDate>Wed, 07 Jan 2026 11:35:52 +0000</pubDate>
</item>
<item>
<title>How I Built a CQRS Approval Flow with MediatR, Carter, FluentValidation, and SQLite</title>
<link>https://coderlegion.com/9090/how-i-built-a-cqrs-approval-flow-with-mediatr-carter-fluentvalidation-and-sqlite</link>
<description>&lt;h3 id=&quot;the-tech-stack-i-used-and-why-updated-with-repository-and-unit-of-work-&quot;&gt;The tech stack I used and why (updated, with Repository and Unit of Work)&lt;/h3&gt;
&lt;p&gt;This project is small, but I wanted it to feel like a real API you can extend. The repository is here:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://github.com/stevsharp/ChainOfResponsibilityWithMediatR
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;carter-for-endpoints&quot;&gt;Carter for endpoints&lt;/h4&gt;
&lt;p&gt;I used &lt;strong&gt;Carter&lt;/strong&gt; because I like minimal APIs, but I still want structure. Carter lets you group endpoints by feature (modules) without big controllers, so the API layer stays clean. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application?utm_source=chatgpt.com&quot; title=&quot;Implementing the Repository and Unit of Work Patterns in ...&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;h4 id=&quot;cqrs-with-mediatr&quot;&gt;CQRS with MediatR&lt;/h4&gt;
&lt;p&gt;CQRS is the separation of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Commands&lt;/strong&gt; (write, change state)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queries&lt;/strong&gt; (read, return data)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;strong&gt;MediatR&lt;/strong&gt;, the endpoint just does &lt;code&gt;mediator.Send(...)&lt;/code&gt; and the logic lives in a handler. This keeps endpoints thin and makes features easy to follow. &lt;/p&gt;
&lt;h4 id=&quot;icommand-and-iquery-marker-interfaces&quot;&gt;ICommand and IQuery marker interfaces&lt;/h4&gt;
&lt;p&gt;I added &lt;code&gt;ICommand&lt;/code&gt; and &lt;code&gt;IQuery&lt;/code&gt; as simple marker interfaces on top of MediatR requests. This is not a “framework”, it is only a clean way to tag what is a write and what is a read.&lt;/p&gt;
&lt;p&gt;The reason is pipeline behaviors. With these markers, I can run transactions only for commands, and avoid wrapping queries. &lt;/p&gt;
&lt;h4 id=&quot;mediatr-pipeline-behaviors&quot;&gt;MediatR pipeline behaviors&lt;/h4&gt;
&lt;p&gt;Pipeline behaviors are like middleware around every handler (&lt;code&gt;IPipelineBehavior&amp;lt;TRequest, TResponse&amp;gt;&lt;/code&gt;). (&lt;/p&gt;
&lt;p&gt;In this project I use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LoggingBehavior&lt;/strong&gt;, so I see execution time and request names&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ValidationBehavior&lt;/strong&gt;, so invalid requests never reach business logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TransactionBehavior&lt;/strong&gt;, so commands run inside a transaction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps handlers focused, no repeated boilerplate.&lt;/p&gt;
&lt;h4 id=&quot;fluentvalidation&quot;&gt;FluentValidation&lt;/h4&gt;
&lt;p&gt;I used &lt;strong&gt;FluentValidation&lt;/strong&gt; because it keeps validation clean and testable. It also supports assembly scanning registration, so validators are discovered automatically. &lt;/p&gt;
&lt;h4 id=&quot;repository-pattern-why-it-exists-here-&quot;&gt;Repository pattern (why it exists here)&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;Repository&lt;/strong&gt; is used as a boundary between the application and persistence. In simple words, the application talks to an interface like &lt;code&gt;IPurchaseOrderRepository&lt;/code&gt;, and Infrastructure decides how the data is stored (EF Core, SQLite, later maybe something else).&lt;/p&gt;
&lt;p&gt;Conceptually, a repository “looks like” an in-memory collection for your domain objects, but it hides the database mapping details. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the classic definition from Fowler&lt;/strong&gt;. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/eaaCatalog/repository.html?utm_source=chatgpt.com&quot; title=&quot;Repository&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;In this project, repositories help me keep handlers free from EF Core code, so handlers read more like business use cases.&lt;/p&gt;
&lt;h4 id=&quot;unit-of-work-and-how-it-connects-to-dbcontext-&quot;&gt;Unit of Work (and how it connects to DbContext)&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;Unit of Work&lt;/strong&gt; groups changes and commits them together. Fowler describes it as tracking everything you do in a business transaction and coordinating writing changes to the database. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/eaaCatalog/unitOfWork.html?utm_source=chatgpt.com&quot; title=&quot;Unit of Work&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Important detail: in EF Core, &lt;code&gt;DbContext&lt;/code&gt; is already designed to be used as a unit of work. Microsoft docs even explain that a &lt;code&gt;DbContext&lt;/code&gt; instance is typically used for a single unit of work, then you call &lt;code&gt;SaveChanges&lt;/code&gt; and dispose it. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/&quot; title=&quot;DbContext Lifetime, Configuration, and Initialization - EF Core | Microsoft Learn&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;So why still have &lt;code&gt;IUnitOfWork&lt;/code&gt; in the Application layer?&lt;/p&gt;
&lt;p&gt;Because it is not only about “needing” it, it is about controlling dependencies. Sometimes you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;handlers to never call EF directly&lt;/li&gt;
&lt;li&gt;one &lt;code&gt;SaveChangesAsync&lt;/code&gt; per command (enforced in one place)&lt;/li&gt;
&lt;li&gt;the possibility to swap persistence later without touching application code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So &lt;code&gt;IUnitOfWork&lt;/code&gt; is an Application abstraction, Infrastructure implements it by calling &lt;code&gt;DbContext.SaveChangesAsync&lt;/code&gt;. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/&quot; title=&quot;DbContext Lifetime, Configuration, and Initialization - EF Core | Microsoft Learn&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;h4 id=&quot;ef-core-sqlite&quot;&gt;EF Core + SQLite&lt;/h4&gt;
&lt;p&gt;EF Core is the persistence technology, SQLite is the lightweight database so the project runs locally with minimal setup. For a demo project, it is perfect.&lt;/p&gt;
&lt;p&gt;Also, EF Core saving is straightforward: you change tracked entities, then call &lt;code&gt;SaveChanges&lt;/code&gt; or &lt;code&gt;SaveChangesAsync&lt;/code&gt; to persist. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/saving/?utm_source=chatgpt.com&quot; title=&quot;Saving Data - EF Core&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/p&gt;
&lt;h4 id=&quot;swagger-swashbuckle-&quot;&gt;Swagger (Swashbuckle)&lt;/h4&gt;
&lt;p&gt;Swagger is here so you can test endpoints fast without Postman. It gives visibility to request and response contracts, which is very useful in demos and early development.&lt;/p&gt;
&lt;h4 id=&quot;domain-patterns-chain-of-responsibility-and-specification&quot;&gt;Domain patterns, Chain of Responsibility and Specification&lt;/h4&gt;
&lt;p&gt;The business part is the approval flow. I used &lt;strong&gt;Chain of Responsibility&lt;/strong&gt; to avoid one big &lt;code&gt;if/else if&lt;/code&gt; method, and I used &lt;strong&gt;Specification&lt;/strong&gt; style rules so conditions stay readable and composable. This makes it easy to add new approval levels without rewriting old logic. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/8906/chain-of-responsibility-inside-cqrs-with-mediatr-a-small-practical-example&quot; title=&quot;Chain of Responsibility inside CQRS (with MediatR), a small practical example - Coder Legion&quot;&gt;Coder Legion&lt;/a&gt;)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references-2&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;GitHub repository:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://github.com/stevsharp/ChainOfResponsibilityWithMediatR
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Original article on CoderLegion:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://coderlegion.com/8906/chain-of-responsibility-inside-cqrs-with-mediatr-a-small-practical-example
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;How to use Carter in a CQRS API, advantages and alternatives : &lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://coderlegion.com/9342/carter-in-a-cqrs-api-advantages-and-alternatives
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;MediatR pipeline behaviors (&lt;code&gt;IPipelineBehavior&lt;/code&gt;): &lt;/li&gt;
&lt;li&gt;FluentValidation DI scanning: &lt;/li&gt;
&lt;li&gt;Repository pattern (Fowler): (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/eaaCatalog/repository.html?utm_source=chatgpt.com&quot; title=&quot;Repository&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Unit of Work pattern (Fowler): (&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/eaaCatalog/unitOfWork.html?utm_source=chatgpt.com&quot; title=&quot;Unit of Work&quot;&gt;martinfowler.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;EF Core: DbContext as unit of work + SaveChanges lifecycle: (&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/&quot; title=&quot;DbContext Lifetime, Configuration, and Initialization - EF Core | Microsoft Learn&quot;&gt;Microsoft Learn&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/9090/how-i-built-a-cqrs-approval-flow-with-mediatr-carter-fluentvalidation-and-sqlite</guid>
<pubDate>Tue, 30 Dec 2025 10:40:18 +0000</pubDate>
</item>
<item>
<title>Building a Simple Cron Scheduler in .NET with Cronos</title>
<link>https://coderlegion.com/7920/building-a-simple-cron-scheduler-in-net-with-cronos</link>
<description>&lt;h3 id=&quot;1-introduction&quot;&gt;1. Introduction&lt;/h3&gt;
&lt;p&gt;Most .NET apps need some kind of background work. Sending emails, generating reports, cleaning data, all of these are better when they run automatically on a schedule, instead of being triggered by users.&lt;/p&gt;
&lt;p&gt;You do not always need a heavy framework like Quartz or Hangfire. For many scenarios a lightweight cron based scheduler in a console or worker service is more than enough.&lt;/p&gt;
&lt;p&gt;In this tutorial you will build a small but powerful console scheduler that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uses &lt;strong&gt;Cronos&lt;/strong&gt;, a library for parsing cron expressions and calculating next run times&lt;/li&gt;
&lt;li&gt;Runs &lt;strong&gt;multiple jobs&lt;/strong&gt; with different cron expressions&lt;/li&gt;
&lt;li&gt;Executes jobs &lt;strong&gt;in parallel&lt;/strong&gt;, so a slow job does not block the rest&lt;/li&gt;
&lt;li&gt;Handles &lt;strong&gt;graceful shutdown&lt;/strong&gt; using &lt;code&gt;CancellationToken&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cronos itself does not execute jobs, it only parses cron expressions and calculates occurrences, which makes it perfect as a building block for your own scheduler. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/NetCoreCronSchedulerDemo&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-prerequisites&quot;&gt;2. Prerequisites&lt;/h3&gt;
&lt;p&gt;You will need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.NET 8 SDK or newer&lt;/li&gt;
&lt;li&gt;Basic C# knowledge&lt;/li&gt;
&lt;li&gt;NuGet package &lt;strong&gt;Cronos&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-project-setup&quot;&gt;3. Project setup&lt;/h3&gt;
&lt;p&gt;Create a new console project and add Cronos.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet new console -n ConsoleCronSchedulerDemo
cd ConsoleCronSchedulerDemo

dotnet add package Cronos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your final structure will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ConsoleCronSchedulerDemo/
 ├─ Program.cs
 ├─ Jobs/
 │   ├─ ICronJob.cs
 │   ├─ EmailJob.cs
 │   ├─ ReportJob.cs
 │   └─ Scheduler.cs
 └─ README.md  (optional)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-defining-the-cron-job-contract&quot;&gt;4. Defining the cron job contract&lt;/h3&gt;
&lt;p&gt;First, create a &lt;code&gt;Jobs&lt;/code&gt; folder and add &lt;code&gt;ICronJob.cs&lt;/code&gt;. This interface describes what every scheduled job must provide.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/ICronJob.cs
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleCronSchedulerDemo.Jobs;

public interface ICronJob
{
    /// &amp;lt;summary&amp;gt;
    /// Human friendly name, used in logs.
    /// &amp;lt;/summary&amp;gt;
    string Name { get; }
    /// &amp;lt;summary&amp;gt;
    /// Cron expression in standard 5 field format
    /// for example &quot;*/1 * * * *&quot;.
    /// &amp;lt;/summary&amp;gt;
    string CronExpression { get; }
    /// &amp;lt;summary&amp;gt;
    /// Job logic.
    /// &amp;lt;/summary&amp;gt;
    Task ExecuteAsync(CancellationToken stoppingToken);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each job knows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its &lt;strong&gt;name&lt;/strong&gt;, for logging&lt;/li&gt;
&lt;li&gt;Its &lt;strong&gt;cron expression&lt;/strong&gt;, a simple string&lt;/li&gt;
&lt;li&gt;&lt;h3 id=&quot;the-work-it-needs-to-perform-exposed-as-executeasync&quot;&gt;The &lt;strong&gt;work&lt;/strong&gt; it needs to perform, exposed as &lt;code&gt;ExecuteAsync&lt;/code&gt;&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;5-implementing-concrete-jobs&quot;&gt;5. Implementing concrete jobs&lt;/h3&gt;
&lt;p&gt;Now create two simple demo jobs.&lt;/p&gt;
&lt;h4 id=&quot;5-1-emailjob&quot;&gt;5.1 EmailJob&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/EmailJob.cs
using System;
using System.Threading;
using System.Threading.Tasks;

public class EmailJob(string cronExpression, IEmailService emailService) : ICronJob
{
    private readonly IEmailService _emailService = emailService;
    public string Name =&amp;gt; &quot;EmailJob&quot;;
    public string CronExpression { get; } = cronExpression;

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await _emailService.SendReportAsync();

        Console.WriteLine($&quot;[{Name}] Email sent at {DateTimeOffset.Now}&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-2-reportjob&quot;&gt;5.2 ReportJob&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/ReportJob.cs
using System;
using System.Threading;
using System.Threading.Tasks;

public class ReportJob(string cronExpression, IReportService reportService) : ICronJob
{
    private readonly IReportService _reportService = reportService;
    public string Name =&amp;gt; &quot;ReportJob&quot;;
    public string CronExpression { get; } = cronExpression;

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await _reportService.GenerateReportAsync();

        Console.WriteLine($&quot;[{Name}] Report generated at {DateTimeOffset.Now}&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These jobs are just writing to the console, but in a real application they could send emails, generate reports, call APIs, clean a database, or whatever your business needs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-the-scheduler-that-uses-cronos&quot;&gt;6. The scheduler that uses Cronos&lt;/h3&gt;
&lt;p&gt;Now you create a scheduler that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Takes a list of &lt;code&gt;ICronJob&lt;/code&gt; implementations&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;Cronos&lt;/strong&gt; to parse each cron expression and compute the next occurrence&lt;/li&gt;
&lt;li&gt;Loops continuously, checking if any job is due&lt;/li&gt;
&lt;li&gt;Runs each due job in its own &lt;code&gt;Task&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Respects a &lt;code&gt;CancellationToken&lt;/code&gt; so you can shut down cleanly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create &lt;code&gt;Jobs/Scheduler.cs&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/Scheduler.cs
public sealed class Scheduler
{
    private readonly ILogger&amp;lt;Scheduler&amp;gt; _logger;
    private readonly List&amp;lt;(ICronJob job, CronExpression expression)&amp;gt; _jobs;
    private readonly TimeZoneInfo _timeZoneInfo;
    private readonly SemaphoreSlim _semaphore;
    private readonly SchedulerSettings _settings;

    public Scheduler(
        IEnumerable&amp;lt;ICronJob&amp;gt; cronJobs,
        ILogger&amp;lt;Scheduler&amp;gt; logger,
        SchedulerSettings settings)
    {
        _logger = logger;
        _settings = settings ?? new SchedulerSettings();
        _jobs = [.. cronJobs.Select(job =&amp;gt; (job, CronExpression.Parse(job.CronExpression)))];
        _timeZoneInfo = TimeZoneInfo.Local;

        var max = _settings.MaxConcurrentJobs &amp;gt; 0 ? _settings.MaxConcurrentJobs : 1;

        _semaphore = new SemaphoreSlim(max);
    }
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;stoppingToken&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public async Task StartAsync(CancellationToken stoppingToken)
    {
        var nextRuns = _jobs.ToDictionary(
            j =&amp;gt; j.job.Name,
            j =&amp;gt; j.expression.GetNextOccurrence(DateTimeOffset.Now, _timeZoneInfo));

        _logger.LogInformation(&quot;Scheduler started with {JobCount} jobs, max {Max} concurrent.&quot;, _jobs.Count, _settings.MaxConcurrentJobs);

        while (!stoppingToken.IsCancellationRequested)
        {
            var now = DateTimeOffset.Now;

            foreach (var (job, expression) in _jobs)
            {
                var next = nextRuns[job.Name];

                if (!next.HasValue || next.Value &amp;gt; now)
                    continue;

                _logger.LogInformation(&quot;Running {Job} at {Now}&quot;, job.Name, now);

                _ = Task.Run(() =&amp;gt; RunJobWithRetryAsync(job, stoppingToken), stoppingToken);

                nextRuns[job.Name] = expression.GetNextOccurrence(now.AddSeconds(1), _timeZoneInfo);

                _logger.LogInformation(&quot;Next run of {Job} scheduled at {Next}&quot;,job.Name, nextRuns[job.Name]);
            }

            try
            {
                await Task.Delay(1000, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                _logger.LogInformation(&quot;Scheduler cancellation requested.&quot;);

                break;
            }
        }

        _logger.LogInformation(&quot;Scheduler stopped.&quot;);
    }
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;job&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;ct&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private async Task RunJobWithRetryAsync(ICronJob job, CancellationToken ct)
    {
        bool acquired = false;
        try
        {
            await _semaphore.WaitAsync(ct);

            acquired = true;

            var maxRetries = Math.Max(0, _settings.MaxRetries);
            var attempt = 0;

            while (!ct.IsCancellationRequested)
            {
                attempt++;

                try
                {
                    await job.ExecuteAsync(ct);

                    _logger.LogInformation(&quot;{Job} completed successfully (attempt {Attempt}).&quot;, job.Name, attempt);

                    return;
                }
                catch (OperationCanceledException) when (ct.IsCancellationRequested)
                {
                    _logger.LogInformation(&quot;{Job} cancelled during execution.&quot;, job.Name);

                    return;
                }
                catch (Exception ex)
                {
                    if (attempt &amp;gt; maxRetries)
                    {
                        _logger.LogError(ex, &quot;{Job} failed after {Attempts} attempts.&quot;, job.Name, attempt - 1);
                        return;
                    }

                    var delay = GetBackoffDelay(attempt);

                    _logger.LogWarning(ex, &quot;{Job} failed on attempt {Attempt}. Retrying in {Delay}...&quot;, job.Name, attempt, delay);

                    await Task.Delay(delay, ct);
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation(&quot;{Job} scheduling cancelled before start.&quot;, job.Name);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Unexpected error while scheduling/executing {Job}.&quot;, job.Name);
        }
        finally
        {
            if (acquired)
            {
                _semaphore.Release();
            }
        }
    }
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;attempt&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private TimeSpan GetBackoffDelay(int attempt)
    {
        var baseSeconds = Math.Max(1, _settings.RetryDelaySeconds);
        // Exponential backoff: 1, 2, 4, 8, ...
        var exponent = Math.Min(attempt - 1, 10);

        var seconds = baseSeconds * (int)Math.Pow(2, exponent);

        // Add small jitter to avoid thundering herd
        var jitterMs = Random.Shared.Next(100, 500);

        var delay = TimeSpan.FromSeconds(seconds) + TimeSpan.FromMilliseconds(jitterMs);

        var maxDelay = TimeSpan.FromMinutes(2);

        return delay &amp;lt;= maxDelay ? delay : maxDelay;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-use-cronos-here&quot;&gt;Why use Cronos here&lt;/h4&gt;
&lt;p&gt;Cronos gives you &lt;code&gt;CronExpression.Parse(string)&lt;/code&gt; and &lt;code&gt;GetNextOccurrence&lt;/code&gt; which calculates the next valid date time in a given time zone. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You do not have to manually calculate future dates&lt;/li&gt;
&lt;li&gt;Time zones and daylight saving changes are handled according to cron semantics (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/NetCoreCronSchedulerDemo&quot;&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your scheduler is now focused on one thing, deciding when to run jobs and starting tasks, and Cronos handles the calendar rules.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-wiring-everything-in-program-cs&quot;&gt;7. Wiring everything in Program.cs&lt;/h3&gt;
&lt;p&gt;Finally, wire everything together in &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;using Microsoft.Extensions.Configuration;&lt;br&gt;
using Microsoft.Extensions.DependencyInjection;&lt;br&gt;
using Microsoft.Extensions.Logging;&lt;/p&gt;
&lt;p&gt;using NetCoreCronSchedulerDemo.Jobs;&lt;br&gt;
using NetCoreCronSchedulerDemo.Model;&lt;br&gt;
using NetCoreCronSchedulerDemo.Sevice;&lt;/p&gt;
&lt;p&gt;class Program&lt;br&gt;
{&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static async Task Main(string[] args)
{
    // Build configuration
    var configuration = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile(&quot;appsettings.json&quot;, optional: false, reloadOnChange: true)
        .Build();

    // Setup DI
    var services = new ServiceCollection();
    services.AddLogging(config =&amp;gt; config.AddConsole());
    services.AddSingleton&amp;lt;IEmailService, EmailService&amp;gt;();
    services.AddSingleton&amp;lt;IReportService, ReportService&amp;gt;();

    var schedulerSettings = configuration.GetSection(&quot;SchedulerSettings&quot;).Get&amp;lt;SchedulerSettings&amp;gt;() ?? throw new InvalidOperationException(&quot;SchedulerSettings section is missing in appsettings.json.&quot;);
    
    services.AddSingleton(schedulerSettings);
    
    // Load jobs from config
    var cronJobsConfig = configuration.GetSection(&quot;CronJobs&quot;).Get&amp;lt;List&amp;lt;CronJobConfig&amp;gt;&amp;gt;();

    if (cronJobsConfig == null || cronJobsConfig.Count == 0)
    {
        throw new InvalidOperationException(&quot;No cron jobs configured in appsettings.json.&quot;);
    }

    foreach (var jobConfig in cronJobsConfig)
    {
        if (jobConfig.Name == &quot;EmailJob&quot;)
        {
            var emailService = services.BuildServiceProvider().GetService&amp;lt;IEmailService&amp;gt;() 
                ?? throw new InvalidOperationException(&quot;IEmailService is not registered in the service collection.&quot;);
            
            services.AddSingleton&amp;lt;ICronJob&amp;gt;(new EmailJob(jobConfig.CronExpression, emailService));
        }
        else if (jobConfig.Name == &quot;ReportJob&quot;)
        {
            var reportService = services.BuildServiceProvider().GetService&amp;lt;IReportService&amp;gt;() 
                ?? throw new InvalidOperationException(&quot;IReportService is not registered in the service collection.&quot;);

            services.AddSingleton&amp;lt;ICronJob&amp;gt;(new ReportJob(jobConfig.CronExpression, reportService));
        }

    }

    services.AddSingleton&amp;lt;Scheduler&amp;gt;();

    var provider = services.BuildServiceProvider();

    var scheduler = provider.GetRequiredService&amp;lt;Scheduler&amp;gt;();

    Console.WriteLine(&quot;========================================&quot;);

    Console.WriteLine(&quot;Console Cron Scheduler (Advanced) started.&quot;);

    Console.WriteLine(&quot;========================================&quot;);

    await scheduler.StartAsync(CancellationToken.None);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Run the application.

```bash
dotnet run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see something similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Console Cron Scheduler started.
Running ReportJob at 2025-11-28T18:00:00.0000000+02:00
Next run of ReportJob scheduled at 2025-11-28T18:01:00.0000000+02:00
Running EmailJob at 2025-11-28T18:00:00.0000000+02:00
Next run of EmailJob scheduled at 2025-11-28T20:00:00.0000000+02:00
[2025-11-28T18:00:02.0000000+02:00] ReportJob finished.
[2025-11-28T18:00:05.0000000+02:00] EmailJob finished.
...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-understanding-the-flow&quot;&gt;8. Understanding the flow&lt;/h3&gt;
&lt;p&gt;To recap how everything works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Program&lt;/code&gt; creates a list of &lt;code&gt;ICronJob&lt;/code&gt; implementations&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Scheduler&lt;/code&gt; wraps each job in a &lt;code&gt;ScheduledJob&lt;/code&gt; that holds the &lt;code&gt;CronExpression&lt;/code&gt; and &lt;code&gt;NextRun&lt;/code&gt; time&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The scheduler loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Looks at current time&lt;/li&gt;
&lt;li&gt;Checks which jobs are due&lt;/li&gt;
&lt;li&gt;Starts each due job in a &lt;code&gt;Task.Run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Asks Cronos for the next occurrence and updates &lt;code&gt;NextRun&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The loop waits one second and repeats until &lt;code&gt;CancellationToken&lt;/code&gt; is cancelled&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;because-each-job-runs-on-its-own-task-a-long-running-email-job-will-not-block-a-quick-report-job-&quot;&gt;Because each job runs on its own task, a long running email job will not block a quick report job.&lt;/h3&gt;
&lt;h3 id=&quot;10-conclusion&quot;&gt;10. Conclusion&lt;/h3&gt;
&lt;p&gt;With a few small classes you have built a reusable cron scheduler around the Cronos library that can run multiple jobs in parallel, respect time zones, and shut down cleanly.&lt;/p&gt;
&lt;p&gt;Cronos gives you clean handling of cron expressions and their next occurrences, and your scheduler focuses on orchestrating job execution. This pattern can be reused in console apps, worker services, or even as part of a larger ASP.NET Core backend.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/NetCoreCronSchedulerDemo&quot;&gt;Source Code : https://github.com/stevsharp/NetCoreCronSchedulerDemo&lt;/a&gt;&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/7920/building-a-simple-cron-scheduler-in-net-with-cronos</guid>
<pubDate>Fri, 28 Nov 2025 18:29:26 +0000</pubDate>
</item>
<item>
<title>️ From Spaghetti to Scalable: Refactoring a Legacy .NET App Architecture</title>
<link>https://coderlegion.com/7633/from-spaghetti-to-scalable-refactoring-a-legacy-net-app-architecture</link>
<description>&lt;p&gt;Every developer eventually inherits a legacy .NET project the kind where every method talks to everything, and a single change breaks three other features. &lt;/p&gt;
&lt;p&gt;We had one of those too. And instead of rewriting from scratch, we decided to refactor and evolve it step by step. Here’s what made the difference &lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Started with understanding, not rewriting&lt;/strong&gt;.&lt;br&gt;
We mapped dependencies and identified the real pain points before touching a single line of code.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Introduced layers gradually&lt;/strong&gt;.&lt;br&gt;
Added separation between API, business logic, and data layers — even if it wasn’t “perfect” at first.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Applied Dependency Injection&lt;/strong&gt;.&lt;br&gt;
Breaking hard-coded dependencies gave us flexibility and made testing possible for the first time.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Moved toward Clean Architecture&lt;/strong&gt;.&lt;br&gt;
Organized the solution around use cases and domain logic, not around technical layers.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Incremental refactoring &amp;gt; big bang rewrites&lt;/strong&gt;.&lt;br&gt;
We refactored feature by feature while keeping the app running — zero downtime.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Added automated tests along the way.&lt;/strong&gt;&lt;br&gt;
Unit tests became our safety net for each refactored module.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Introduced modern tools gradually&lt;/strong&gt;.&lt;br&gt;
Switched to EF Core, MediatR, and background jobs via Hangfire — one at a time.&lt;/p&gt;
&lt;p&gt;After months of iteration, we transformed a fragile legacy system into a modular, testable, and scalable architecture — without losing business continuity.&lt;/p&gt;
&lt;p&gt; Have you ever refactored a legacy .NET project? What approach worked best for you?&lt;/p&gt;
&lt;h2 id=&quot;dotnet-softwarearchitecture-refactoring-cleanarchitecture-csharp-softwareengineering-backend-devleadership-legacycode&quot;&gt;dotnet #softwarearchitecture #refactoring #cleanarchitecture #csharp #softwareengineering #backend #devleadership #legacycode&lt;/h2&gt;
</description>
<category>Backend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7633/from-spaghetti-to-scalable-refactoring-a-legacy-net-app-architecture</guid>
<pubDate>Sat, 22 Nov 2025 20:34:12 +0000</pubDate>
</item>
<item>
<title>Tutorial: Building a .NET 9 Console App with Hangfire and Channels</title>
<link>https://coderlegion.com/7559/tutorial-building-a-net-9-console-app-with-hangfire-and-channels</link>
<description>&lt;h2 id=&quot;tutorial-building-a-net-9-console-app-with-hangfire-and-channels&quot;&gt;Tutorial: Building a .NET 9 Console App with Hangfire and Channels&lt;/h2&gt;
&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;In this tutorial, you will build a .NET 8 console application that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Polls data from an API, or simulates it&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;System.Threading.Channels&lt;/strong&gt; for producer consumer messaging&lt;/li&gt;
&lt;li&gt;Runs background tasks using &lt;strong&gt;Hosted Services&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Enqueues jobs in &lt;strong&gt;Hangfire&lt;/strong&gt; using in memory storage&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-use-system-threading-channels&quot;&gt;Why Use System.Threading.Channels&lt;/h3&gt;
&lt;p&gt;Traditional producer consumer patterns often rely on &lt;strong&gt;BlockingCollection&lt;/strong&gt; or custom queues, which can introduce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thread contention&lt;/li&gt;
&lt;li&gt;Blocking calls&lt;/li&gt;
&lt;li&gt;Complex synchronization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;System.Threading.Channels&lt;/code&gt; provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Asynchronous, non blocking communication between producers and consumers&lt;/li&gt;
&lt;li&gt;Built in backpressure when using bounded channels&lt;/li&gt;
&lt;li&gt;High performance with low allocation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DataPollingService&lt;/strong&gt; is the producer, it writes work items into the channel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ChannelConsumerService&lt;/strong&gt; is the consumer, it reads items and enqueues Hangfire jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-hosted-services&quot;&gt;Why Hosted Services&lt;/h3&gt;
&lt;p&gt;Hosted services, &lt;code&gt;BackgroundService&lt;/code&gt;, integrate with the .NET Generic Host. You get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Graceful lifecycle management, start and stop&lt;/li&gt;
&lt;li&gt;Dependency injection&lt;/li&gt;
&lt;li&gt;Logging and configuration&lt;/li&gt;
&lt;li&gt;Non blocking background loops&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This app uses two hosted services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DataPollingService&lt;/code&gt; simulates fetching data and writes work items to the channel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChannelConsumerService&lt;/code&gt; reads from the channel and enqueues Hangfire jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-backgroundservice&quot;&gt;What is &lt;code&gt;BackgroundService&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BackgroundService&lt;/code&gt; is a base class that implements &lt;code&gt;IHostedService&lt;/code&gt; and gives you a long running &lt;code&gt;ExecuteAsync&lt;/code&gt; loop. It plugs into the Generic Host, so you get DI, logging, and coordinated shutdown.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use cases&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Poll APIs or message brokers&lt;/li&gt;
&lt;li&gt;Drain a queue or channel&lt;/li&gt;
&lt;li&gt;Run internal maintenance tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Key points&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;ExecuteAsync(CancellationToken)&lt;/code&gt; as your main loop&lt;/li&gt;
&lt;li&gt;Respect the cancellation token for fast shutdown&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;PeriodicTimer&lt;/code&gt; over &lt;code&gt;Task.Delay&lt;/code&gt; in recurring loops&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-hangfire&quot;&gt;What is Hangfire&lt;/h3&gt;
&lt;p&gt;Hangfire is a background job processor for .NET. You enqueue a method call, Hangfire persists and executes it with workers. It supports fire and forget, delayed, recurring, continuations, and batches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it helps here&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The consumer stays lightweight, it only enqueues work&lt;/li&gt;
&lt;li&gt;Processing is retried and observable with a dashboard&lt;/li&gt;
&lt;li&gt;You can swap storage later for durability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Common job types&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fire and forget, &lt;code&gt;BackgroundJob.Enqueue(() =&amp;gt; DoWork())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Delayed, &lt;code&gt;BackgroundJob.Schedule(() =&amp;gt; DoWork(), TimeSpan.FromMinutes(5))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Recurring, &lt;code&gt;RecurringJob.AddOrUpdate(&quot;nightly&quot;, () =&amp;gt; DoWork(), &quot;0 2 * * *&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notes, in memory storage is fine for demos, not for production. Jobs should be idempotent.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-system-threading-channels&quot;&gt;What is &lt;code&gt;System.Threading.Channels&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Channels are high performance, async friendly, in process queues. You get a &lt;code&gt;ChannelWriter&amp;lt;T&amp;gt;&lt;/code&gt; for producers and a &lt;code&gt;ChannelReader&amp;lt;T&amp;gt;&lt;/code&gt; for consumers. Channels work with &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;, and avoid blocking threads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why they shine&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Async reads and writes&lt;/li&gt;
&lt;li&gt;Bounded channels provide backpressure&lt;/li&gt;
&lt;li&gt;Options like &lt;code&gt;SingleReader&lt;/code&gt; and &lt;code&gt;SingleWriter&lt;/code&gt; reduce overhead when true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Fast read loop&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;while (await reader.WaitToReadAsync(ct))
    while (reader.TryRead(out var item))
        await HandleAsync(item, ct);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;steps-to-build&quot;&gt;Steps to Build&lt;/h3&gt;
&lt;h4 id=&quot;1-create-the-project&quot;&gt;1. Create the Project&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet new console -n CustomerSyncConsole
cd CustomerSyncConsole
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-add-nuget-packages-1&quot;&gt;2. Add NuGet Packages&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Hangfire
dotnet add package Hangfire.MemoryStorage
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Logging.Console
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-implement-the-code&quot;&gt;3. Implement the Code&lt;/h4&gt;
&lt;p&gt;Below is a complete &lt;code&gt;Program.cs&lt;/code&gt; with comments. It uses an unbounded channel for simplicity. See the optional bounded channel section that follows if you want backpressure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Threading.Channels;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace CustomerSyncConsole;

public sealed record CustomerWorkItem(int CustomerId, string PayloadId, DateTimeOffset ReceivedAt, string RawJson);

public interface ICustomerJobs
{
    Task ProcessCustomer(int customerId, string payloadId, CancellationToken ct = default);
}

public sealed class CustomerJobs(ILogger&amp;lt;CustomerJobs&amp;gt; logger) : ICustomerJobs
{
    private readonly ILogger&amp;lt;CustomerJobs&amp;gt; _logger = logger;

    public async Task ProcessCustomer(int customerId, string payloadId, CancellationToken ct = default)
    {
        _logger.LogInformation(&quot;Hangfire job started for Customer {CustomerId}, Payload {PayloadId}&quot;, customerId, payloadId);
        await Task.Delay(TimeSpan.FromSeconds(2), ct);
        _logger.LogInformation(&quot;Hangfire job completed for Customer {CustomerId}, Payload {PayloadId}&quot;, customerId, payloadId);
    }
}

public interface IStorage
{
    Task SaveAsync(string payloadId, string rawJson, CancellationToken ct);
}

public sealed class InMemoryStorage(ILogger&amp;lt;InMemoryStorage&amp;gt; logger) : IStorage
{
    private readonly ILogger&amp;lt;InMemoryStorage&amp;gt; _logger = logger;
    private readonly Dictionary&amp;lt;string, string&amp;gt; _store = new();

    public Task SaveAsync(string payloadId, string rawJson, CancellationToken ct)
    {
        _store[payloadId] = rawJson;
        _logger.LogInformation(&quot;Saved payload {PayloadId} in-memory (size={Size}).&quot;, payloadId, _store.Count);
        return Task.CompletedTask;
    }
}

public sealed class DataPollingService(
    ILogger&amp;lt;DataPollingService&amp;gt; logger,
    Channel&amp;lt;CustomerWorkItem&amp;gt; channel,
    IConfiguration config,
    IStorage storage) : BackgroundService
{
    private readonly ILogger&amp;lt;DataPollingService&amp;gt; _logger = logger;
    private readonly ChannelWriter&amp;lt;CustomerWorkItem&amp;gt; _writer = channel.Writer;
    private readonly IConfiguration _config = config;
    private readonly IStorage _storage = storage;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var intervalSeconds = _config.GetValue(&quot;Polling:IntervalSeconds&quot;, 10);
        var timer = new PeriodicTimer(TimeSpan.FromSeconds(intervalSeconds));

        _logger.LogInformation(&quot;Polling started. Interval={Interval}s&quot;, intervalSeconds);

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                var simulated = new[]
                {
                    new IncomingDto { CustomerId = 1, PayloadId = Guid.NewGuid().ToString(&quot;N&quot;) },
                    new IncomingDto { CustomerId = 2, PayloadId = Guid.NewGuid().ToString(&quot;N&quot;) },
                };

                foreach (var dto in simulated)
                {
                    var raw = System.Text.Json.JsonSerializer.Serialize(dto);
                    await _storage.SaveAsync(dto.PayloadId, raw, stoppingToken);

                    var item = new CustomerWorkItem(dto.CustomerId, dto.PayloadId, DateTimeOffset.UtcNow, raw);
                    await _writer.WriteAsync(item, stoppingToken);
                    _logger.LogInformation(&quot;Queued work item for Customer {CustomerId}, Payload {PayloadId}&quot;, dto.CustomerId, dto.PayloadId);
                }
            }
        }
        finally
        {
            _writer.TryComplete();
        }
    }

    private sealed class IncomingDto
    {
        public int CustomerId { get; set; }
        public string PayloadId { get; set; } = Guid.NewGuid().ToString(&quot;N&quot;);
    }
}

public sealed class ChannelConsumerService(
    Channel&amp;lt;CustomerWorkItem&amp;gt; channel,
    IBackgroundJobClient backgroundJobClient,
    ILogger&amp;lt;ChannelConsumerService&amp;gt; logger) : BackgroundService
{
    private readonly ChannelReader&amp;lt;CustomerWorkItem&amp;gt; _reader = channel.Reader;
    private readonly IBackgroundJobClient _backgroundJobClient = backgroundJobClient;
    private readonly ILogger&amp;lt;ChannelConsumerService&amp;gt; _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(&quot;Channel consumer started.&quot;);
        await foreach (var item in _reader.ReadAllAsync(stoppingToken))
        {
            if (item.CustomerId == 1)
            {
                var jobId = _backgroundJobClient.Enqueue&amp;lt;ICustomerJobs&amp;gt;(job =&amp;gt; job.ProcessCustomer(item.CustomerId, item.PayloadId, CancellationToken.None));
                _logger.LogInformation(&quot;Enqueued Hangfire job {JobId} for Customer {CustomerId}, Payload {PayloadId}&quot;, jobId, item.CustomerId, item.PayloadId);
            }
        }
        _logger.LogInformation(&quot;Channel consumer stopping.&quot;);
    }
}

public static class Program
{
    public static async Task Main(string[] args)
    {
        using var host = Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration(cfg =&amp;gt;
            {
                cfg.AddInMemoryCollection(new Dictionary&amp;lt;string, string?&amp;gt;
                {
                    [&quot;Polling:IntervalSeconds&quot;] = &quot;10&quot;
                });
            })
            .ConfigureLogging(logging =&amp;gt;
            {
                logging.ClearProviders();
                logging.AddSimpleConsole(o =&amp;gt;
                {
                    o.TimestampFormat = &quot;HH:mm:ss &quot;;
                    o.SingleLine = true;
                });
                logging.SetMinimumLevel(LogLevel.Information);
            })
            .ConfigureServices((ctx, services) =&amp;gt;
            {
                // Unbounded for simplicity in this tutorial
                services.AddSingleton(Channel.CreateUnbounded&amp;lt;CustomerWorkItem&amp;gt;());

                // Hangfire in memory
                services.AddHangfire(config =&amp;gt; config.UseMemoryStorage());
                services.AddHangfireServer();

                services.AddSingleton&amp;lt;ICustomerJobs, CustomerJobs&amp;gt;();
                services.AddSingleton&amp;lt;IStorage, InMemoryStorage&amp;gt;();
                services.AddHostedService&amp;lt;DataPollingService&amp;gt;();
                services.AddHostedService&amp;lt;ChannelConsumerService&amp;gt;();
            })
            .Build();

        await host.RunAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;optional-switch-to-a-bounded-channel&quot;&gt;Optional, switch to a bounded channel&lt;/h3&gt;
&lt;p&gt;For services that can receive spikes, prefer bounded channels to get backpressure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;services.AddSingleton(_ =&amp;gt;
{
    var opts = new BoundedChannelOptions(capacity: 512)
    {
        FullMode = BoundedChannelFullMode.Wait,
        SingleReader = true,   // one consumer service
        SingleWriter = true    // one producer service
    };
    return Channel.CreateBounded&amp;lt;CustomerWorkItem&amp;gt;(opts);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the unbounded registration with the code above.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;best-practices&quot;&gt;Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pass &lt;code&gt;CancellationToken&lt;/code&gt; to every &lt;code&gt;WriteAsync&lt;/code&gt; and &lt;code&gt;ReadAllAsync&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Complete the writer during shutdown, then await reader completion&lt;/li&gt;
&lt;li&gt;Make Hangfire jobs idempotent, retries can occur&lt;/li&gt;
&lt;li&gt;Consider persistent Hangfire storage for real workloads, for example SQL Server or SQLite&lt;/li&gt;
&lt;li&gt;Expose channel depth as a metric if you need visibility, current size versus capacity&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;run-it&quot;&gt;Run it&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see logs from both services. The producer writes items into the channel. The consumer enqueues Hangfire jobs for customer 1. The Hangfire server executes those jobs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references-3&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/CustomerJobRunner&quot;&gt;Source Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/dotnet/api/system.threading.channels&quot;&gt;System.Threading.Channels Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services&quot;&gt;BackgroundService and Hosted Services in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.hangfire.io/&quot;&gt;Hangfire Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/7559/tutorial-building-a-net-9-console-app-with-hangfire-and-channels</guid>
<pubDate>Thu, 20 Nov 2025 07:56:42 +0000</pubDate>
</item>
<item>
<title>Decorate services in ASP.NET Core, step by step</title>
<link>https://coderlegion.com/7347/decorate-services-in-asp-net-core-step-by-step</link>
<description>&lt;p&gt;Decorator pattern, in short&lt;/p&gt;
&lt;p&gt;The decorator pattern is a structural design pattern that lets you add behavior to an object at runtime by wrapping it with another object that has the same interface. The wrapper forwards calls to the inner object, then adds extra work before or after the call. You keep the original contract, you can stack multiple decorators, and you avoid subclass explosion by preferring composition over inheritance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key points&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Intent, attach additional responsibilities to an object dynamically&lt;/li&gt;
&lt;li&gt;How, wrap an object that implements the same interface and forward&lt;br&gt;
   calls&lt;/li&gt;
&lt;li&gt;Good for cross cutting concerns like logging, retry, caching,&lt;br&gt;
metrics, authorization&lt;/li&gt;
&lt;li&gt;Benefits, core code stays simple, behaviors are pluggable and&lt;br&gt;
testable, decorators compose&lt;/li&gt;
&lt;li&gt;Trade offs, more indirection and wiring, the order of decorators&lt;br&gt;
matters&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Why decorate in ASP.NET Core&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Decoration lets you add cross cutting behavior around a service without touching its core code, for example retry, logging, metrics, caching. It keeps concerns separated and easy to test.&lt;/p&gt;
&lt;p&gt;Packages&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Scrutor
dotnet add package Polly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Core service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;

public interface IInvoicesClient
{
    Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default);
}

public sealed class InvoicesClient : IInvoicesClient
{
    private readonly HttpClient _http;
    private readonly string _baseUrl;
    private readonly Func&amp;lt;string&amp;gt; _getToken;
    private static readonly JsonSerializerOptions _json = new() { PropertyNameCaseInsensitive = true };

    public InvoicesClient(HttpClient http, string baseUrl, Func&amp;lt;string&amp;gt; getToken)
    {
        _http = http;
        _baseUrl = baseUrl.TrimEnd('/');
        _getToken = getToken;
    }

    public async Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default)
    {
        var url = BuildUrl(_baseUrl, filter, orderBy, include, pageIndex, pageSize, transactionCurrency, skipRounding);

        using var req = new HttpRequestMessage(HttpMethod.Get, url)
        {
#if NET5_0_OR_GREATER
            VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
#endif
        };

        var token = _getToken?.Invoke();
        if (!string.IsNullOrEmpty(token))
            req.Headers.Authorization = new AuthenticationHeaderValue(&quot;Bearer&quot;, token);

        using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
        resp.EnsureSuccessStatusCode();

        await using var stream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
        return await JsonSerializer.DeserializeAsync&amp;lt;PagedResultDtoOfInvoice&amp;gt;(stream, _json, ct).ConfigureAwait(false)
               ?? new PagedResultDtoOfInvoice();
    }

    private static string BuildUrl(
        string baseUrl, string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding)
    {
        var q = System.Web.HttpUtility.ParseQueryString(string.Empty);
        if (!string.IsNullOrWhiteSpace(filter)) q[&quot;filter&quot;] = filter;
        if (!string.IsNullOrWhiteSpace(orderBy)) q[&quot;orderBy&quot;] = orderBy;
        if (!string.IsNullOrWhiteSpace(include)) q[&quot;include&quot;] = include;
        if (pageIndex.HasValue) q[&quot;pageIndex&quot;] = pageIndex.Value.ToString();
        if (pageSize.HasValue) q[&quot;pageSize&quot;] = pageSize.Value.ToString();
        if (transactionCurrency.HasValue) q[&quot;transactionCurrency&quot;] = transactionCurrency.Value ? &quot;true&quot; : &quot;false&quot;;
        if (skipRounding.HasValue) q[&quot;skipRounding&quot;] = skipRounding.Value ? &quot;true&quot; : &quot;false&quot;;
        return $&quot;{baseUrl}/invoices?{q}&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Decorators&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Polly;
using System.Net.Sockets;

public sealed class RetryInvoicesClientDecorator : IInvoicesClient
{
    private readonly IInvoicesClient _inner;
    private readonly IAsyncPolicy _retry;

    public RetryInvoicesClientDecorator(IInvoicesClient inner)
    {
        _inner = inner;
        _retry = Policy
            .Handle&amp;lt;HttpRequestException&amp;gt;()
            .Or&amp;lt;IOException&amp;gt;()
            .Or&amp;lt;SocketException&amp;gt;()
            .Or&amp;lt;TaskCanceledException&amp;gt;(ex =&amp;gt; !ex.CancellationToken.IsCancellationRequested)
            .WaitAndRetryAsync(3,
                attempt =&amp;gt; TimeSpan.FromSeconds(Math.Pow(2, attempt)) +
                           TimeSpan.FromMilliseconds(Random.Shared.Next(0, 750)));
    }

    public Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default)
    =&amp;gt;
        _retry.ExecuteAsync(t =&amp;gt;
            _inner.GetInvoicesAsync(filter, orderBy, include, pageIndex, pageSize, transactionCurrency, skipRounding, t),
            ct);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logging decorator&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class LoggingInvoicesClientDecorator : IInvoicesClient
{
    private readonly IInvoicesClient _inner;
    private readonly ILogger&amp;lt;LoggingInvoicesClientDecorator&amp;gt; _log;

    public LoggingInvoicesClientDecorator(IInvoicesClient inner, ILogger&amp;lt;LoggingInvoicesClientDecorator&amp;gt; log)
    { _inner = inner; _log = log; }

    public async Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        try
        {
            _log.LogInformation(&quot;Invoices request pageIndex={PageIndex} pageSize={PageSize}&quot;, pageIndex, pageSize);
            var res = await _inner.GetInvoicesAsync(filter, orderBy, include, pageIndex, pageSize, transactionCurrency, skipRounding, ct);
            _log.LogInformation(&quot;Invoices response count={Count} in {Elapsed} ms&quot;, res?.Data?.Count ?? 0, sw.ElapsedMilliseconds);
            return res;
        }
        catch (Exception ex)
        {
            _log.LogError(ex, &quot;Invoices failed in {Elapsed} ms&quot;, sw.ElapsedMilliseconds);
            throw;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Registration with Scrutor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Scrutor;
using System.Net;

builder.Services.AddHttpClient(&quot;Invoices&quot;, c =&amp;gt;
{
    c.Timeout = Timeout.InfiniteTimeSpan;
    c.DefaultRequestHeaders.Accept.ParseAdd(&quot;application/json&quot;);
})
.ConfigurePrimaryHttpMessageHandler(() =&amp;gt; new SocketsHttpHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
    PooledConnectionLifetime = TimeSpan.FromMinutes(2),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
    MaxConnectionsPerServer = 8
});

builder.Services.AddTransient&amp;lt;IInvoicesClient&amp;gt;(sp =&amp;gt;
{
    var http = sp.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;().CreateClient(&quot;Invoices&quot;);
    var cfg = sp.GetRequiredService&amp;lt;IConfiguration&amp;gt;();
    var baseUrl = cfg[&quot;Erp:BaseUrl&quot;] ?? &quot;&quot;;
    var tokenProvider = sp.GetRequiredService&amp;lt;ITokenProvider&amp;gt;();
    return new InvoicesClient(http, baseUrl, () =&amp;gt; tokenProvider.GetToken());
});

builder.Services.Decorate&amp;lt;IInvoicesClient, LoggingInvoicesClientDecorator&amp;gt;();
builder.Services.Decorate&amp;lt;IInvoicesClient, RetryInvoicesClientDecorator&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Native DI without Scrutor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Services.AddHttpClient(&quot;Invoices&quot;, c =&amp;gt;
{
    c.Timeout = Timeout.InfiniteTimeSpan;
    c.DefaultRequestHeaders.Accept.ParseAdd(&quot;application/json&quot;);
});

builder.Services.AddTransient&amp;lt;IInvoicesClient&amp;gt;(sp =&amp;gt;
{
    var http = sp.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;().CreateClient(&quot;Invoices&quot;);
    var cfg = sp.GetRequiredService&amp;lt;IConfiguration&amp;gt;();
    var baseUrl = cfg[&quot;Erp:BaseUrl&quot;] ?? &quot;&quot;;
    var tokenProvider = sp.GetRequiredService&amp;lt;ITokenProvider&amp;gt;();

    IInvoicesClient svc = new InvoicesClient(http, baseUrl, () =&amp;gt; tokenProvider.GetToken());
    svc = new LoggingInvoicesClientDecorator(svc, sp.GetRequiredService&amp;lt;ILogger&amp;lt;LoggingInvoicesClientDecorator&amp;gt;&amp;gt;());
    svc = new RetryInvoicesClientDecorator(svc);
    return svc;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DelegatingHandler for transport concerns&lt;/p&gt;
&lt;p&gt;Use handlers for wire level logic, keep decorators for service level logic.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class LoggingHandler : DelegatingHandler
{
    private readonly ILogger&amp;lt;LoggingHandler&amp;gt; _log;
    public LoggingHandler(ILogger&amp;lt;LoggingHandler&amp;gt; log) =&amp;gt; _log = log;

    protected override async Task&amp;lt;HttpResponseMessage&amp;gt; SendAsync(HttpRequestMessage request, CancellationToken ct)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var response = await base.SendAsync(request, ct);
        _log.LogInformation(&quot;{Method} {Uri} -&amp;gt; {Status} in {Elapsed} ms&quot;,
            request.Method, request.RequestUri, (int)response.StatusCode, sw.ElapsedMilliseconds);
        return response;
    }
}

builder.Services.AddTransient&amp;lt;LoggingHandler&amp;gt;();
builder.Services.AddHttpClient(&quot;Invoices&quot;, c =&amp;gt; { /* defaults above */ })
    .AddHttpMessageHandler&amp;lt;LoggingHandler&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Best practices checklist&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One retry layer only.&lt;/li&gt;
&lt;li&gt;Per call timeout with CancelAfter.&lt;/li&gt;
&lt;li&gt;ResponseHeadersRead for large payloads.&lt;/li&gt;
&lt;li&gt;Small page size for large ranges.&lt;/li&gt;
&lt;li&gt;Keep HttpClient immutable after registration.&lt;/li&gt;
&lt;li&gt;Put auth headers on the request message.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/App-vNext/Polly&quot;&gt;Polly, official docs and GitHub.&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/khellang/Scrutor&quot;&gt;Scrutor, GitHub and NuGet.&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory&quot;&gt;Microsoft docs, HttpClientFactory with resilience patterns.&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/AspNetCore.DecoratorSamples&quot;&gt;Source Code&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/7347/decorate-services-in-asp-net-core-step-by-step</guid>
<pubDate>Fri, 14 Nov 2025 11:21:56 +0000</pubDate>
</item>
<item>
<title>Global Exception Handler vs Custom Middleware in ASP.NET Core</title>
<link>https://coderlegion.com/7028/global-exception-handler-vs-custom-middleware-in-asp-net-core</link>
<description>&lt;p&gt;&lt;strong&gt;The problem we’re solving&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Stuff goes wrong: timeouts, not-found resources, validation fails. You want one consistent error response and clean logs, without sprinkling try/catch everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two good options&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Option A — Global Exception Handler (recommended default)&lt;/p&gt;
&lt;p&gt;Think of it as the framework’s built-in safety net. You plug in one handler and ASP.NET Core turns exceptions into ProblemDetails JSON automatically.&lt;/p&gt;
&lt;p&gt;Why people like it&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Minimal code, fewer edge cases. &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Plays nice with content negotiation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One central place to map exception → HTTP status.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use when: you want &lt;strong&gt;standard errors&lt;/strong&gt; and consistency across the app.&lt;/p&gt;
&lt;p&gt;Option B — Custom Exception Middleware&lt;/p&gt;
&lt;p&gt;Your own IMiddleware that wraps the pipeline in &lt;strong&gt;try/catch&lt;/strong&gt; and writes whatever JSON shape you want.&lt;/p&gt;
&lt;p&gt;Why people use it&lt;/p&gt;
&lt;p&gt;-Full control over the response body and headers.&lt;/p&gt;
&lt;p&gt;-Easy to keep a legacy “envelope” format.&lt;/p&gt;
&lt;p&gt;-Use when: clients require a specific, non-ProblemDetails format or you need special side effects.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision guide&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;-Standard JSON errors and less boilerplate → Global handler&lt;/p&gt;
&lt;p&gt;-Legacy/bespoke response body → Middleware&lt;/p&gt;
&lt;p&gt;-Want a single place to map exceptions to statuses → Global handler&lt;/p&gt;
&lt;p&gt;-Heavy custom logic on errors (headers, counters) → Middleware&lt;/p&gt;
&lt;p&gt;-Using MediatR and want clean handlers → Use a MediatR behavior to log, then rethrow so the global handler shapes the response&lt;/p&gt;
&lt;p&gt;1) Global handler wiring (Program.cs)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Services.AddExceptionHandler&amp;lt;GlobalExceptionHandler&amp;gt;();

app.UseExceptionHandler(); // put this early in the pipeline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The handler itself (GlobalExceptionHandler.cs)&lt;/p&gt;
&lt;p&gt;using Microsoft.AspNetCore.Diagnostics;&lt;br&gt;
using Microsoft.AspNetCore.Mvc;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger&amp;lt;GlobalExceptionHandler&amp;gt; _logger;
    private readonly IProblemDetailsService _pds;
    private readonly IHostEnvironment _env;

    public GlobalExceptionHandler(ILogger&amp;lt;GlobalExceptionHandler&amp;gt; logger, IProblemDetailsService pds, IHostEnvironment env)
        =&amp;gt; (_logger, _pds, _env) = (logger, pds, env);

    public async ValueTask&amp;lt;bool&amp;gt; TryHandleAsync(HttpContext http, Exception ex, CancellationToken ct)
    {
        var (status, title) = ex switch
        {
            ValidationException =&amp;gt; (422, &quot;Validation failed&quot;),
            NotFoundException   =&amp;gt; (404, &quot;Not found&quot;),
            UnauthorizedAccessException =&amp;gt; (401, &quot;Unauthorized&quot;),
            TimeoutException    =&amp;gt; (503, &quot;Service unavailable&quot;),
            Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException =&amp;gt; (409, &quot;Concurrency conflict&quot;),
            TaskCanceledException =&amp;gt; (408, &quot;Request timeout&quot;),
            OperationCanceledException =&amp;gt; (StatusCodes.Status499ClientClosedRequest, &quot;Client closed request&quot;),
            HttpRequestException =&amp;gt; (502, &quot;Bad gateway&quot;),
            _ =&amp;gt; (500, &quot;Unexpected error&quot;)
        };

        if (status is 503 or 408) http.Response.Headers.TryAdd(&quot;Retry-After&quot;, &quot;5&quot;);

        http.Response.StatusCode = status;

        var pd = new ProblemDetails
        {
            Status = status,
            Title = title,
            Detail = _env.IsDevelopment() ? ex.Message : null,
            Instance = http.Request.Path
        };
        if (ex is ValidationException ve) pd.Extensions[&quot;errors&quot;] = ve.Errors;

        _logger.LogError(ex, &quot;Unhandled exception -&amp;gt; HTTP {Status}&quot;, status);

        return await _pds.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = http,
            ProblemDetails = pd,
            Exception = ex
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Custom middleware skeleton&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class ExceptionEnvelopeMiddleware : IMiddleware
{
    private readonly ILogger&amp;lt;ExceptionEnvelopeMiddleware&amp;gt; _logger;
    private readonly IHostEnvironment _env;
    public ExceptionEnvelopeMiddleware(ILogger&amp;lt;ExceptionEnvelopeMiddleware&amp;gt; logger, IHostEnvironment env)
        =&amp;gt; (_logger, _env) = (logger, env);

    public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
    {
        try { await next(ctx); }
        catch (Exception ex)
        {
            if (ctx.Response.HasStarted) throw;
            _logger.LogError(ex, &quot;Unhandled exception&quot;);

            ctx.Response.Clear();
            ctx.Response.StatusCode = ex switch
            {
                ValidationException =&amp;gt; StatusCodes.Status422UnprocessableEntity,
                NotFoundException   =&amp;gt; StatusCodes.Status404NotFound,
                TimeoutException    =&amp;gt; StatusCodes.Status503ServiceUnavailable,
                _ =&amp;gt; StatusCodes.Status500InternalServerError
            };
            ctx.Response.ContentType = &quot;application/json&quot;;

            var body = new
            {
                title = ctx.Response.StatusCode == 422 ? &quot;Validation failed&quot; :
                        ctx.Response.StatusCode == 404 ? &quot;Not found&quot; :
                        ctx.Response.StatusCode == 503 ? &quot;Service unavailable&quot; : &quot;Unexpected error&quot;,
                status = ctx.Response.StatusCode,
                traceId = ctx.TraceIdentifier,
                detail = _env.IsDevelopment() ? ex.Message : null,
                errors = ex is ValidationException ve ? ve.Errors : null
            };
            await ctx.Response.WriteAsJsonAsync(body);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Using MediatR? Add a behavior&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;using MediatR;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class UnhandledExceptionBehavior&amp;lt;TRequest,TResponse&amp;gt; : IPipelineBehavior&amp;lt;TRequest,TResponse&amp;gt;
    where TRequest : notnull
{
    private readonly ILogger&amp;lt;UnhandledExceptionBehavior&amp;lt;TRequest,TResponse&amp;gt;&amp;gt; _logger;
    public UnhandledExceptionBehavior(ILogger&amp;lt;UnhandledExceptionBehavior&amp;lt;TRequest,TResponse&amp;gt;&amp;gt; logger) =&amp;gt; _logger = logger;

    public async Task&amp;lt;TResponse&amp;gt; Handle(TRequest request, RequestHandlerDelegate&amp;lt;TResponse&amp;gt; next, CancellationToken ct)
    {
        try { return await next(); }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Unhandled exception in MediatR for {Request}&quot;, typeof(TRequest).Name);
            throw; // let the global handler shape ProblemDetails
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Common pitfalls (and easy wins)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;-Place the handler early. Put UseExceptionHandler() before routing and authorization.&lt;/p&gt;
&lt;p&gt;-Don’t leak stack traces in production. Keep Detail empty unless in Development.&lt;/p&gt;
&lt;p&gt;-Streaming/gRPC: once the response starts, you can’t switch to JSON. Log and stop.&lt;/p&gt;
&lt;p&gt;-Map the big ones: validation → 422, not found → 404, timeouts → 503, concurrency → 409.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you just want clean, predictable errors, use the &lt;strong&gt;global handle&lt;/strong&gt;r and be done. If your clients expect a specific JSON envelope, use a custom middleware. With MediatR, add a behavior to keep handlers tidy and let the handler (or middleware) do the final shaping.&lt;/p&gt;
&lt;p&gt;Further Reading :&lt;/p&gt;
&lt;p&gt;Error handling &amp;amp; ProblemDetails (official docs)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0&amp;amp;utm_source=chatgpt.com&quot;&gt;Handle errors in ASP.NET Core (overview). &lt;br&gt;
Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler?view=aspnetcore-9.0&quot;&gt;Handle errors in ASP.NET Core APIs (includes UseExceptionHandler patterns). &lt;br&gt;
Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Middleware&lt;/strong&gt; (how it works &amp;amp; how to write your own)&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0&quot;&gt;Middleware fundamentals (ordering, short-circuiting). &lt;br&gt;
Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MediatR pipeline behaviors&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/LuckyPennySoftware/MediatR&quot;&gt;Jimmy Bogard (creator) — pipeline posts &amp;amp; patterns. &lt;br&gt;
Jimmy Bogard&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluentvalidation.net/&quot;&gt;FluentValidation in ASP.NET Core (official docs) for a validation behavior that throws a ValidationException. &lt;br&gt;
fluentvalidation.net&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source Code &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp?tab=repositories&quot;&gt;https://github.com/stevsharp?tab=repositories&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/7028/global-exception-handler-vs-custom-middleware-in-asp-net-core</guid>
<pubDate>Tue, 04 Nov 2025 09:38:14 +0000</pubDate>
</item>
<item>
<title>How to Bind a MudSelect from an External Source in Blazor Using MudBlazor</title>
<link>https://coderlegion.com/1832/how-to-bind-a-mudselect-from-an-external-source-in-blazor-using-mudblazor</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor&quot;&gt;Blazor&lt;/a&gt; is a modern framework from Microsoft that allows you to build interactive web applications using C# and .NET, both on the server and the client, without needing JavaScript. It brings the full power of .NET to the browser via WebAssembly or server-side rendering.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mudblazor.com/&quot;&gt;MudBlazor&lt;/a&gt; is a popular UI component library built specifically for Blazor. It offers clean, responsive components following Google's Material Design system, making it incredibly easy to create modern, beautiful, and interactive web UIs entirely in C#.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Use Case&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s say you need to:&lt;/p&gt;
&lt;p&gt;Display a multi-select dropdown&lt;/p&gt;
&lt;p&gt;Load the options from an external service&lt;/p&gt;
&lt;p&gt;Sync selected values with the parent component&lt;/p&gt;
&lt;p&gt;Reuse this logic throughout your application&lt;/p&gt;
&lt;p&gt;We'll walk through a real-world pattern using MudSelect, bound to a list of brands (dtoBrands) provided by a service.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A Blazor project with MudBlazor installed&lt;/p&gt;
&lt;p&gt;A service that returns the brand data&lt;br&gt;
A DTO like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BrandDto
{
    public string Text { get; set; }
    public string Value { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Sample Service Interface&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface IBrandService
{
    Task&amp;lt;IEnumerable&amp;lt;BrandDto&amp;gt;&amp;gt; GetBrandsAsync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; The MudSelect Markup&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;MudSelect T=&quot;BrandDto&quot;
           Label=&quot;@Label&quot;
           MultiSelection=&quot;true&quot;
           Clearable=&quot;true&quot;
           @bind-SelectedValues=&quot;InternalSelectedValues&quot;
           ToStringFunc=&quot;item =&amp;gt; item.Text&quot;&amp;gt;
    @if (dtoBrands.Any())
    {
        @foreach (var item in dtoBrands)
        {
            &amp;lt;MudSelectItem T=&quot;BrandDto&quot; Value=&quot;item&quot;&amp;gt;
                @item.Text
            &amp;lt;/MudSelectItem&amp;gt;
        }
    }
&amp;lt;/MudSelect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚙️ Component Logic&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@inject IBrandService BrandService

@code {
    [Parameter]
    public string Label { get; set; } = &quot;Select Brands&quot;;

    [Parameter]
    public IEnumerable&amp;lt;BrandDto&amp;gt; SelectedValues { get; set; } = new HashSet&amp;lt;BrandDto&amp;gt;();

    [Parameter]
    public EventCallback&amp;lt;IEnumerable&amp;lt;BrandDto&amp;gt;&amp;gt; SelectedValuesChanged { get; set; }

    protected IEnumerable&amp;lt;BrandDto&amp;gt; dtoBrands = new List&amp;lt;BrandDto&amp;gt;();

    private IEnumerable&amp;lt;BrandDto&amp;gt; _internalSelectedValues = new HashSet&amp;lt;BrandDto&amp;gt;();

    private IEnumerable&amp;lt;BrandDto&amp;gt; InternalSelectedValues
    {
        get =&amp;gt; _internalSelectedValues;
        set
        {
            if (_internalSelectedValues != value)
            {
                _internalSelectedValues = value;
                SelectedValuesChanged.InvokeAsync(_internalSelectedValues);
            }
        }
    }

    protected override async Task OnInitializedAsync()
    {
        dtoBrands = await BrandService.GetBrandsAsync();

        _internalSelectedValues = new HashSet&amp;lt;BrandDto&amp;gt;(
            dtoBrands.Where(x =&amp;gt; SelectedValues.Any(s =&amp;gt; s.Value == x.Value))
        );
    }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        await base.SetParametersAsync(parameters);

        _internalSelectedValues = new HashSet&amp;lt;BrandDto&amp;gt;(
            dtoBrands.Where(x =&amp;gt; SelectedValues.Any(s =&amp;gt; s.Value == x.Value))
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; How It Works&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1170082142962582969&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;♻️ Example Usage&lt;/p&gt;
&lt;p&gt;Imagine turning this into a reusable component, like BrandSelector.razor. You’d use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;BrandSelector Label=&quot;Choose Brands&quot;
               SelectedValues=&quot;@selectedBrands&quot;
               SelectedValuesChanged=&quot;@OnSelectedBrandsChanged&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Why This Pattern Rocks&lt;/p&gt;
&lt;p&gt;By combining Blazor's powerful component model with MudBlazor's modern UI toolkit, you get:&lt;/p&gt;
&lt;p&gt;✅ Fully data-driven dropdowns&lt;/p&gt;
&lt;p&gt;✅ Type-safe C# components&lt;/p&gt;
&lt;p&gt;✅ No need for JavaScript&lt;/p&gt;
&lt;p&gt;✅ A consistent look and feel&lt;/p&gt;
&lt;p&gt;And all of it is maintainable and testable across large-scale apps.&lt;/p&gt;
&lt;p&gt;✅ Final Thoughts&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blazor&lt;/strong&gt; and &lt;strong&gt;MudBlazor&lt;/strong&gt; are a dream combo for building modern web apps in C#. By binding a MudSelect to an external service, you're creating a highly flexible and reusable pattern that fits beautifully in a clean architecture.&lt;/p&gt;
&lt;p&gt; Useful Links&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-9.0&quot;&gt;Blazor documentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mudblazor.com/&quot;&gt;MudBlazor official site&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mudblazor.com/api/mudselect#pages&quot;&gt;MudSelect documentation&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/1832/how-to-bind-a-mudselect-from-an-external-source-in-blazor-using-mudblazor</guid>
<pubDate>Thu, 10 Apr 2025 09:15:42 +0000</pubDate>
</item>
<item>
<title>Optimistic vs. Pessimistic Concurrency in EF Core (with Table Hints)</title>
<link>https://coderlegion.com/1720/optimistic-vs-pessimistic-concurrency-in-ef-core-with-table-hints</link>
<description>&lt;p&gt;Many experienced developers are familiar with database transactions but often overlook the key differences between optimistic and pessimistic concurrency control in Entity Framework Core (EF Core). &lt;/p&gt;
&lt;p&gt;Understanding both approaches and how EF Core handles concurrency tokens is essential for building scalable and safe data operations, especially in systems with high transaction rates.&lt;/p&gt;
&lt;p&gt;Concurrency control determines how multiple users or processes interact with the same data simultaneously. While optimistic concurrency is the default in EF Core, pessimistic concurrency is often misunderstood, misused, or completely ignored  even by seniors engineers.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations&quot;&gt; Related Read: Understanding and Using Table Hints in SQL Server&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can find the full code example at: &lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCoreExamples/tree/Efcore-Concurrency&quot;&gt;https://github.com/stevsharp/EfCoreExamples/tree/Efcore-Concurrency&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; What Is a Concurrency Token?&lt;/p&gt;
&lt;p&gt;A concurrency token is a property that EF Core watches to detect whether a row has been modified since it was loaded. It's typically used in optimistic concurrency scenarios.&lt;/p&gt;
&lt;p&gt;EF Core uses this token during &lt;strong&gt;SaveChangesAsync()&lt;/strong&gt; to generate a WHERE clause that checks if the value is unchanged , if it has changed, a &lt;strong&gt;DbUpdateConcurrencyException&lt;/strong&gt; is thrown.&lt;/p&gt;
&lt;p&gt; How to Mark a Property as a Concurrency Token&lt;/p&gt;
&lt;p&gt;✅ Using [ConcurrencyCheck] Attribute:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Product
{
    public int Id { get; set; }

    [ConcurrencyCheck]
    public int Stock { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Using [Timestamp] (for SQL Server rowversion column):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Timestamp]
public byte[] RowVersion { get; set; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Fluent Configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;modelBuilder.Entity&amp;lt;Product&amp;gt;()
    .Property(p =&amp;gt; p.Stock)
    .IsConcurrencyToken();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use any column as a concurrency token — even logical fields like LastModified, but &lt;strong&gt; (aka &lt;/strong&gt;) is the most reliable for SQL Server.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Optimistic Concurrency Control&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Optimistic concurrency assumes conflicts are rare, so it doesn’t lock any data during updates. Instead, it tracks changes using concurrency tokens and throws an exception if a conflict is detected during SaveChangesAsync().&lt;/p&gt;
&lt;p&gt;✅ When to Use&lt;/p&gt;
&lt;p&gt;Read-heavy apps with occasional writes (e.g., blog, e-commerce frontend)&lt;/p&gt;
&lt;p&gt;Systems where retries are acceptable&lt;/p&gt;
&lt;p&gt; How to Implement in EF Core with a Concurrency Token&lt;/p&gt;
&lt;p&gt;Entity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Product
{
    public int Id { get; set; }
    public int Stock { get; set; }

    [Timestamp] // This is the concurrency token
    public byte[] RowVersion { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Save and Handle Concurrency:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;var product = await context.Products.FindAsync(1);&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (product.Stock &amp;gt; 0)
{
    product.Stock -= 1;

    try
    {
        await context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var databaseValues = await ex.Entries.Single().GetDatabaseValuesAsync();
        ex.Entries.Single().OriginalValues.SetValues(databaseValues);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Pessimistic Concurrency Control&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pessimistic concurrency prevents conflicts altogether by locking the rows being read or modified. This ensures no one else can change the data until the transaction is complete.&lt;/p&gt;
&lt;p&gt;❗ Important: EF Core does not support table hints like `` in LINQ. These must be applied using raw SQL.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations&quot;&gt; Related Read: Understanding and Using Table Hints in SQL Server&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Example: Locking Bank Account Row&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using (var context = new AppDbContext())
{
    using var transaction = await context.Database.BeginTransactionAsync();

    var account = await context.Accounts
        .FromSqlRaw(&quot;SELECT * FROM Accounts WITH (UPDLOCK, ROWLOCK) WHERE Id = {0}&quot;, 1)
        .AsTracking() // Ensures the entity is tracked for updates
        .FirstOrDefaultAsync();

    if (account != null)
    {
        account.Balance -= 100;

        await context.SaveChangesAsync();
    }

    await transaction.CommitAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Why SaveChangesAsync() Alone Is Not Always Enough&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When using FromSqlRaw(), EF Core may not track the entity correctly for updates unless explicitly instructed.&lt;/p&gt;
&lt;p&gt;Using .AsTracking() ensures EF will detect changes.&lt;/p&gt;
&lt;p&gt;Without it, you would need to manually mark the entity as modified:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context.Entry(account).State = EntityState.Modified;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8344402934722218379&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;✅ Final Thoughts&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concurrency tokens&lt;/strong&gt; (like RowVersion) play a central role in optimistic concurrency, enabling EF Core to detect conflicts without using locks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimistic concurrency&lt;/strong&gt; is ideal for most applications where data collisions are rare and performance is key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pessimistic concurrency&lt;/strong&gt; is essential in financial or high-risk systems, where consistency outweighs performance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core does not support table hints like&lt;/strong&gt; &lt;code&gt;` in LINQ, but you can **leverage them via **&lt;/code&gt;`, with .AsTracking() to ensure updates are recognized.&lt;/p&gt;
&lt;p&gt;Many developers — even experienced ones — underestimate how concurrency should be managed in EF Core. The wrong approach can result in subtle bugs, lost data, or race conditions in production.&lt;/p&gt;
&lt;p&gt;  References &amp;amp; Further Reading&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations&quot;&gt;Microsoft Docs – Handling Concurrency Conflicts in EF Core&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations&quot;&gt;Microsoft Docs – Concurrency Tokens&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/stevsharp/understanding-and-using-table-hints-in-sql-server-4iia&quot;&gt;Dev.to – Understanding and Using Table Hints in SQL Server&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/sql-queries?tabs=sqlserver&quot;&gt;Microsoft Docs – Raw SQL Queries in EF Core&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table?view=sql-server-ver16&quot;&gt;SQL Server – Table Hints in Transact-SQL&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1720/optimistic-vs-pessimistic-concurrency-in-ef-core-with-table-hints</guid>
<pubDate>Mon, 31 Mar 2025 10:01:25 +0000</pubDate>
</item>
<item>
<title>EF Core Bulk Updates and Deletes: Performance vs Change Tracking</title>
<link>https://coderlegion.com/1640/ef-core-bulk-updates-and-deletes-performance-vs-change-tracking</link>
<description>&lt;p&gt;Entity Framework Core 7 introduced two powerful methods for bulk operations: &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ExecuteUpdateAsync&lt;/strong&gt; and &lt;strong&gt;ExecuteDeleteAsync&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;These methods are designed for high-performance scenarios where you need to update or delete multiple records efficiently without loading them into memory. They work great, but there's one caveat: EF Core's Change Tracker is not aware of these changes.&lt;/p&gt;
&lt;p&gt;In this article, we explore how these methods work, why they bypass the &lt;strong&gt;Change Tracker&lt;/strong&gt;, and most importantly—how you can audit or track changes manually when using them.&lt;/p&gt;
&lt;p&gt;If you're not familiar with EF Core's Change Tracker, check out this &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/change-tracking/&quot;&gt;introductory guide on Change Tracking&lt;/a&gt; to understand how EF Core monitors entity states.&lt;/p&gt;
&lt;p&gt;Why Use ExecuteUpdateAsync and ExecuteDeleteAsync?&lt;/p&gt;
&lt;p&gt;These methods:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate direct SQL UPDATE and DELETE statements.&lt;/li&gt;
&lt;li&gt;Do not load entities into memory, improving performance.&lt;/li&gt;
&lt;li&gt;Bypass change tracking, which avoids unnecessary overhead.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example Usage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Bulk update
await context.Users
    .Where(u =&amp;gt; u.LastLogin &amp;lt; DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(setters =&amp;gt; setters.SetProperty(u =&amp;gt; u.IsInactive, true));

// Bulk delete
await context.Users
    .Where(u =&amp;gt; u.IsInactive)
    .ExecuteDeleteAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Problem: No Change Tracking&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Because EF Core does not load entities into memory during these operations, the Change Tracker never sees them. This means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;No automatic change detection.&lt;/li&gt;
&lt;li&gt;No SaveChangesAsync() involvement.&lt;/li&gt;
&lt;li&gt;No triggers for audit or domain events.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, how do you track what was changed or deleted?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.Auditing Options for Bulk Operations&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Manual Pre-Query Before Execution&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fetch affected entities before performing the batch operation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var affectedUsers = await context.Users
    .Where(u =&amp;gt; u.LastLogin &amp;lt; DateTime.UtcNow.AddYears(-1))
    .Select(u =&amp;gt; new { u.Id, u.Name, OldValue = u.IsInactive })
    .ToListAsync();

await context.Users
    .Where(u =&amp;gt; u.LastLogin &amp;lt; DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(setters =&amp;gt; setters.SetProperty(u =&amp;gt; u.IsInactive, true));

// Log changes manually
foreach (var user in affectedUsers)
{
    Console.WriteLine($&quot;User {user.Name} updated: IsInactive {user.OldValue} → true&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Precise control over what you log.&lt;/p&gt;
&lt;p&gt;Access to old values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Requires an extra query.&lt;/p&gt;
&lt;p&gt;May impact performance for large datasets.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Audit Table Logging&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Save the affected data into an audit table before executing the bulk operation.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await context.AuditLogs.AddRangeAsync(affectedUsers.Select(user =&amp;gt; new AuditLog
{
    EntityName = &quot;User&quot;,
    EntityId = user.Id,
    ChangeType = &quot;Updated&quot;,
    ChangedAt = DateTime.UtcNow,
    Description = $&quot;User {user.Name} was updated: IsInactive {user.OldValue} → true&quot;
}));

await context.SaveChangesAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Database Triggers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Define SQL triggers to capture changes directly in the database.&lt;/p&gt;
&lt;p&gt;Example :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TRIGGER trg_User_Delete
ON Users
AFTER DELETE
AS
BEGIN
    INSERT INTO AuditLogs (EntityName, EntityId, ChangeType, ChangedAt, Description)
    SELECT 'User', Id, 'Deleted', GETUTCDATE(), CONCAT('User ', Name, ' was deleted') FROM deleted;
END;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pros:&lt;/p&gt;
&lt;p&gt;Fully automated.&lt;/p&gt;
&lt;p&gt;Ensures auditing even if EF Core is bypassed.&lt;/p&gt;
&lt;p&gt;Cons:&lt;/p&gt;
&lt;p&gt;Less flexible.&lt;/p&gt;
&lt;p&gt;Harder to manage and version control.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;EF Core Interceptors (Advanced)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Capture executed SQL commands using DbCommandInterceptor.&lt;/p&gt;
&lt;p&gt;Limitation:&lt;/p&gt;
&lt;p&gt;You can log the SQL query but not the previous values of affected entities.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AuditInterceptor : DbCommandInterceptor
{
    public override InterceptionResult&amp;lt;DbDataReader&amp;gt; ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult&amp;lt;DbDataReader&amp;gt; result)
    {
        Console.WriteLine($&quot;SQL Executed: {command.CommandText}&quot;);
        return base.ReaderExecuting(command, eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Registering the Interceptor : &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ApplicationDbContext : DbContext
{
    public DbSet&amp;lt;User&amp;gt; Users { get; set; }
    public DbSet&amp;lt;AuditLog&amp;gt; AuditLogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(&quot;YourConnectionStringHere&quot;);
        optionsBuilder.AddInterceptors(new AuditInterceptor());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EF Core's ExecuteUpdateAsync and ExecuteDeleteAsync offer incredible performance for bulk data operations but come at the cost of losing automatic change tracking.&lt;/p&gt;
&lt;p&gt;If you need to audit or track changes, the best approach is to manually query the affected data before execution or use a dedicated audit system such as audit tables or SQL triggers.&lt;/p&gt;
&lt;p&gt;Choose the method that balances performance and auditing needs for your application.&lt;/p&gt;
&lt;p&gt;What do you think? Do you prefer performance with manual tracking, or do you rely on EF Core's change tracking? Let us know your approach!&lt;/p&gt;
&lt;p&gt;You can find the full code example at: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCoreExamples/tree/BulkUpdateAndDeletes&quot;&gt;https://github.com/stevsharp/EfCoreExamples/tree/BulkUpdateAndDeletes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#bulk-updates-and-deletes&quot;&gt;Microsoft Docs - EF Core 7: ExecuteUpdate and ExecuteDelete&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/change-tracking/&quot;&gt;Microsoft Docs - Change Tracking in EF Core&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&quot;&gt;Microsoft Docs - Interceptors in EF Core&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dotnet/efcore&quot;&gt;Community Discussion - EF Core GitHub Issue on Change Tracking and Bulk Operations&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1640/ef-core-bulk-updates-and-deletes-performance-vs-change-tracking</guid>
<pubDate>Mon, 24 Mar 2025 20:25:26 +0000</pubDate>
</item>
<item>
<title>EF Core: Lazy Loading, Eager Loading, and Loading Data on Demand</title>
<link>https://coderlegion.com/1401/ef-core-lazy-loading-eager-loading-and-loading-data-on-demand</link>
<description>&lt;p&gt;When working with &lt;strong&gt;Entity Framework Core (EF Core),&lt;/strong&gt; there are different strategies for loading related data from the database.&lt;/p&gt;
&lt;p&gt;So, how do you load a collection or other &lt;strong&gt;related objects?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here are the main options:&lt;/p&gt;
&lt;p&gt;1️⃣ Eager Loading (Include)&lt;/p&gt;
&lt;p&gt;Loads related data immediately with the main entity using .Include().&lt;/p&gt;
&lt;p&gt;Example: Load an Order with its Customer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var order = context.Orders
    .Include(o =&amp;gt; o.Customer) // Load related Customer
    .First();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example: Load a Customer with Their Orders&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var customer = context.Customers
    .Include(c =&amp;gt; c.Orders) // Load related Orders
    .First();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅Pros:&lt;/p&gt;
&lt;p&gt;✔ Avoids multiple database queries (N+1 problem)&lt;/p&gt;
&lt;p&gt;✔ Simple and easy to use&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Can load unnecessary data, increasing query size&lt;/p&gt;
&lt;p&gt;❌ Slower if you load too many relationships&lt;/p&gt;
&lt;p&gt;2️⃣ Explicit Loading (Entry().Reference() / Entry().Collection())&lt;/p&gt;
&lt;p&gt;Loads related data on demand (manually) using .Entry().&lt;/p&gt;
&lt;p&gt;Example: Load a Single Related Entity&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var order = context.Orders.First();

// Load related Customer manually
context.Entry(order).Reference(o =&amp;gt; o.Customer).Load();

Console.WriteLine(order.Customer.Name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example: Load a Collection of Related Entities&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var customer = context.Customers.First();

// Load related Orders manually
context.Entry(customer).Collection(c =&amp;gt; c.Orders).Load();

Console.WriteLine($&quot;Orders count: {customer.Orders.Count}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Pros:&lt;/p&gt;
&lt;p&gt;✔ More control over when data is loaded&lt;/p&gt;
&lt;p&gt;✔ Avoids unnecessary queries&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Requires additional queries&lt;/p&gt;
&lt;p&gt;❌ Must remember to load data explicitly&lt;/p&gt;
&lt;p&gt;3️⃣ Lazy Loading (Requires Proxies)&lt;/p&gt;
&lt;p&gt;Loads related data only when accessed.&lt;/p&gt;
&lt;p&gt;Install :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Microsoft.EntityFrameworkCore.Proxies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Steps to Enable Lazy Loading&lt;/p&gt;
&lt;p&gt;Enable in DbContext:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.UseLazyLoadingProxies().UseSqlite(&quot;Data Source=mydb.db&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make navigation properties virtual&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public virtual Customer Customer { get; set; } = default!;
public virtual List&amp;lt;Order&amp;gt; Orders { get; set; } = new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example Usage : &lt;/p&gt;
&lt;p&gt;var order = context.Orders.First();&lt;/p&gt;
&lt;p&gt;// Lazy loaded automatically when accessed&lt;/p&gt;
&lt;p&gt;Console.WriteLine(order.Customer.Name);&lt;/p&gt;
&lt;p&gt;✅ Pros:&lt;/p&gt;
&lt;p&gt;✔ Simplifies code (data is loaded automatically)&lt;/p&gt;
&lt;p&gt;✔ Works without modifying queries&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Requires proxies (extra package)&lt;/p&gt;
&lt;p&gt;❌ Can lead to multiple database hits (N+1 problem)&lt;/p&gt;
&lt;p&gt;4️⃣ Projection (Select with Anonymous Types)&lt;/p&gt;
&lt;p&gt;Loads only specific data instead of full entities.&lt;/p&gt;
&lt;p&gt;Example: Load Only Needed Fields&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var orders = context.Orders
    .Select(o =&amp;gt; new 
    { 
        o.OrderNumber, 
        CustomerName = o.Customer.Name 
    })
    .ToList();

foreach (var order in orders)
{
    Console.WriteLine($&quot;{order.OrderNumber} - {order.CustomerName}&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Pros:&lt;/p&gt;
&lt;p&gt;✔ Best for performance (loads only required fields)&lt;/p&gt;
&lt;p&gt;✔ Reduces unnecessary data transfer&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Cannot track entities (only works for read-only queries)&lt;/p&gt;
&lt;p&gt;Summary Table&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=18412293215726215640&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt; Best Practices&lt;br&gt;
Use Eager Loading (Include()) when related data is always needed.&lt;/p&gt;
&lt;p&gt;Use Explicit Loading if you want manual control.&lt;/p&gt;
&lt;p&gt;Use Lazy Loading only if necessary (it can cause performance issues).&lt;/p&gt;
&lt;p&gt;Use Projection (Select) when you need only some fields.&lt;/p&gt;
&lt;p&gt;Source Code&lt;/p&gt;
&lt;p&gt;You can find the complete source code for this tutorial at:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCoreExamples&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here are some useful references to learn more about EF Core : &lt;/p&gt;
&lt;p&gt; Official Microsoft Documentation&lt;br&gt;
Entity Framework Core Loading Related Data&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/related-data&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/related-data&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Covers Eager Loading, Explicit Loading, and Lazy Loading with examples.&lt;br&gt;
EF Core: Configuring Lazy Loading&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/related-data/lazy&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/related-data/lazy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Explains how to enable Lazy Loading with proxies.&lt;br&gt;
EF Core: Using Include() for Eager Loading&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Details how to use .Include() and .ThenInclude().&lt;br&gt;
EF Core Performance Considerations&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/performance/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/performance/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Best practices for optimizing queries and avoiding performance issues like N+1 queries.&lt;/p&gt;
&lt;p&gt; Community &amp;amp; Blog Articles&lt;br&gt;
EF Core Lazy Loading vs. Eager Loading - Which One Should You Use?&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.thinktecture.com/en/entity-framework-core/lazy-loading-vs-eager-loading/&quot;&gt;https://www.thinktecture.com/en/entity-framework-core/lazy-loading-vs-eager-loading/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A detailed comparison of when to use each strategy.&lt;br&gt;
EF Core Best Practices: Avoiding Performance Pitfalls&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.michalbialecki.com/2020/11/29/entity-framework-core-performance-best-practices/&quot;&gt;https://www.michalbialecki.com/2020/11/29/entity-framework-core-performance-best-practices/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Covers when to use .Include(), lazy loading, and projection.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1401/ef-core-lazy-loading-eager-loading-and-loading-data-on-demand</guid>
<pubDate>Thu, 06 Mar 2025 16:31:00 +0000</pubDate>
</item>
<item>
<title>EF Core Global Query Filters: A Complete Guide</title>
<link>https://coderlegion.com/1323/ef-core-global-query-filters-a-complete-guide</link>
<description>&lt;p&gt;Entity Framework Core (EF Core) Global Query Filters allow you to apply filters to queries at the model level so that they automatically apply to all queries involving a specific entity. &lt;/p&gt;
&lt;p&gt;This is particularly useful for scenarios like soft deletes, multi-tenancy, and user-based data filtering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is a Global Query Filter?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A global filter is a LINQ predicate that applies automatically to all queries on a specific entity type. &lt;/p&gt;
&lt;p&gt;This means that any time you query the database, EF Core applies the filter implicitly, reducing the need for adding Where clauses manually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use Cases&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Soft Deletes – Automatically filter out entities where IsDeleted =&lt;br&gt;
true.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-Tenancy – Ensure that a tenant only accesses their own data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User-Based Filtering – Restrict users to see only their own records.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How to Define a Global Query Filter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Global filters are defined in the OnModelCreating method inside the &lt;strong&gt;DbContext&lt;/strong&gt; class using the HasQueryFilter method.&lt;/p&gt;
&lt;p&gt;Example : Soft Delete Implementation  ..&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BaseEntity
{
    public bool IsDeleted { get; set; }
}

public class Product : BaseEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet&amp;lt;Product&amp;gt; Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;Product&amp;gt;()
            .HasQueryFilter(p =&amp;gt; !p.IsDeleted);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Now, every query on Products automatically filters out records where &lt;strong&gt;IsDeleted&lt;/strong&gt; = true.&lt;/p&gt;
&lt;p&gt;Querying Products&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var products = context.Products.ToList(); // Implicitly applies &quot;WHERE IsDeleted = 0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bypassing the Global Filter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can explicitly disable the filter using .IgnoreQueryFilters().&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var allProducts = context.Products.IgnoreQueryFilters().ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Applying Filters Dynamically (Multi-Tenancy Example)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppDbContext : DbContext
{
    private readonly int _tenantId;

    public AppDbContext(DbContextOptions&amp;lt;AppDbContext&amp;gt; options, ITenantProvider tenantProvider)
        : base(options)
    {
        _tenantId = tenantProvider.GetTenantId();
    }

    public DbSet&amp;lt;Order&amp;gt; Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;Order&amp;gt;()
            .HasQueryFilter(o =&amp;gt; o.TenantId == _tenantId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;strong&gt;Now, EF Core automatically filters records based on _tenantId.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limitations and Considerations&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Filters do not apply to raw SQL queries (FromSqlRaw).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigation properties respect global filters (e.g., if Order has OrderDetails, the filter applies to them too).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure filters do not cause unexpected performance issues on large datasets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can define multiple query filters per entity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Combining Multiple Filters&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;modelBuilder.Entity&amp;lt;Product&amp;gt;()
    .HasQueryFilter(p =&amp;gt; !p.IsDeleted)
    .HasQueryFilter(p =&amp;gt; p.Status == &quot;Active&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM Products WHERE IsDeleted = 0 AND Status = 'Active'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Disabling All Global Filters&lt;/p&gt;
&lt;p&gt;You can disable all filters for a specific context query using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var allOrders = context.Orders.IgnoreQueryFilters().ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Conclusion&lt;br&gt;
EF Core’s Global Query Filters help enforce consistent rules across queries without adding redundant Where clauses. They are a powerful feature for soft deletes, multi-tenancy, and user-based access control. However, they should be used carefully, considering potential performance impacts.&lt;/p&gt;
&lt;p&gt;Source Code&lt;/p&gt;
&lt;p&gt;You can find the complete source code for this tutorial at:&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCoreAndGlobalFilter&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here are some useful references to learn more about EF Core Global Query Filters and Soft Delete:&lt;/p&gt;
&lt;p&gt; Official EF Core Documentation&lt;br&gt;
Global Query Filters&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/filters&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/filters&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Entity Framework Core SQLite Provider&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/providers/sqlite/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/providers/sqlite/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Managing Data with EF Core&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Community Articles &amp;amp; Tutorials&lt;br&gt;
EF Core: Global Query Filters for Soft Deletes&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.thinktecture.com/en/entity-framework-core/global-query-filters/&quot;&gt;https://www.thinktecture.com/en/entity-framework-core/global-query-filters/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Implementing Soft Delete in EF Core&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/swlh/soft-delete-in-entity-framework-core-6c6b7a9784c7&quot;&gt;https://medium.com/swlh/soft-delete-in-entity-framework-core-6c6b7a9784c7&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Using SQLite with EF Core in Console Applications&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://code-maze.com/using-sqlite-with-entity-framework-core/&quot;&gt;https://code-maze.com/using-sqlite-with-entity-framework-core/&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1323/ef-core-global-query-filters-a-complete-guide</guid>
<pubDate>Sun, 02 Mar 2025 09:42:51 +0000</pubDate>
</item>
</channel>
</rss>