CQRS and MediatR in .NET: When It Is Worth It and When It Is Not

Muhammad Rizwan
2026-02-21
18 min read
CQRS and MediatR in .NET: When It Is Worth It and When It Is Not

Every other .NET tutorial these days starts the same way: install MediatR, create a command, create a handler, wire it up, done great you now have CQRS. Except you probably do not. And you probably did not need it for that particular project anyway.

I am not here to trash CQRS or MediatR. I use both in production. They solve real problems when applied to the right situations. But too many teams slap these patterns onto simple CRUD applications and end up with a codebase that has three times the files, twice the complexity, and zero measurable benefit.

So let us have an honest conversation. What is CQRS actually about? What does MediatR bring to the table? When should you use them together? And just as importantly, when should you leave them on the shelf?


What Is CQRS?

CQRS stands for Command Query Responsibility Segregation. The name sounds intimidating, but the core idea is straightforward:

Separate the code that reads data from the code that writes data.

That is it. That is the entire concept at its most basic level.

In a traditional application, you have a single model for both reading and writing. Your ProductService handles creating products, updating them, deleting them, and also fetching them for display. The same DTO might serve as both the input for creating a product and the output for displaying one.

CQRS says: split those responsibilities. Have one path for commands (things that change state) and a completely different path for queries (things that return data without changing anything).

Traditional CRUD vs CQRS

Why Would You Want This?

Because reads and writes have fundamentally different requirements in most real world applications.

Your write side cares about business rules, validation, consistency, and making sure the data is correct before persisting it. It needs to enforce invariants, check permissions, and trigger side effects like sending emails or publishing events.

Your read side cares about speed, flexibility, and giving the client exactly the shape of data it needs. It does not care about business rules because it is not changing anything. It just needs to be fast and return the right projections.

When you force both of these responsibilities through the same pipeline and the same models, you end up compromising on both. Your write models have extra fields that only the UI needs for display. Your read queries pull back entire entity graphs when the client just needed a name and a count.

CQRS lets each side be optimized independently.


What Is MediatR?

MediatR is an open source library by Jimmy Bogard that implements the mediator pattern in .NET. It has nothing to do with CQRS specifically. It is a general purpose in-process messaging library.

Here is what it does in plain terms:

  1. You create a request object (a command or a query).
  2. You create a handler that processes that specific request.
  3. MediatR sits in the middle. You send the request to MediatR, and it routes it to the correct handler.

The controller does not know which handler will process the request. The handler does not know where the request came from. MediatR is the middleman that decouples them.

On top of that, MediatR gives you pipeline behaviors which are essentially middleware that runs before and after every request. You can plug in logging, validation, caching, performance monitoring, and anything else you want without touching a single handler.

That pipeline is where the real power of MediatR lives.

MediatR Pipeline Behaviors


Setting Up CQRS with MediatR in .NET

Enough theory. Let us build something real. I will walk through a complete implementation using a product catalog as the example.

Step 1: Install the Packages

bash
dotnet add package MediatR dotnet add package FluentValidation dotnet add package FluentValidation.DependencyInjectionExtensions

MediatR handles the command and query dispatching. FluentValidation handles input validation through a pipeline behavior (more on that shortly).

Step 2: Register MediatR in Program.cs

csharp
using System.Reflection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); builder.Services.AddControllers();

This single line scans the assembly for all IRequestHandler implementations and registers them automatically. No manual DI wiring for each handler.


Writing Your First Command

A command represents an action that changes state. Think of it as a formal request to do something: create a product, update an order, delete a user.

The Command Class

csharp
using MediatR; namespace MyApp.Application.Commands.CreateProduct; public record CreateProductCommand( string Name, string Description, decimal Price, int StockQuantity ) : IRequest<CreateProductResult>; public record CreateProductResult( Guid Id, string Name, decimal Price, DateTime CreatedAt );

A few things to notice here:

  • The command is a simple record. Immutable by default. No behavior, just data.
  • It implements IRequest<CreateProductResult>, which tells MediatR what the return type will be.
  • The command contains only what is needed to perform the action. Nothing extra.

Free Newsletter

Enjoying the article? Stay in the loop.

  • Production-ready code samples every week
  • In-depth .NET, C# & React tutorials
  • Career tips & dev insights
500+ developers · No spam · Unsubscribe anytime

Join the community

Get new articles delivered every week.

No credit card · No spam · Cancel anytime · Learn more

The Command Handler

csharp
using MediatR; using MyApp.Domain.Entities; using MyApp.Application.Interfaces; namespace MyApp.Application.Commands.CreateProduct; public class CreateProductHandler : IRequestHandler<CreateProductCommand, CreateProductResult> { private readonly IProductRepository _repository; private readonly IUnitOfWork _unitOfWork; public CreateProductHandler(IProductRepository repository, IUnitOfWork unitOfWork) { _repository = repository; _unitOfWork = unitOfWork; } public async Task<CreateProductResult> Handle( CreateProductCommand request, CancellationToken cancellationToken) { var product = new Product( request.Name, request.Description, request.Price, request.StockQuantity ); await _repository.AddAsync(product, cancellationToken); await _unitOfWork.SaveChangesAsync(cancellationToken); return new CreateProductResult( product.Id, product.Name, product.Price, product.CreatedAt ); } }

The handler does one thing and does it well: takes the command, creates the entity, persists it, and returns the result. No HTTP concerns. No validation logic mixed in. Just pure business operation.

The Command Validator

csharp
using FluentValidation; namespace MyApp.Application.Commands.CreateProduct; public class CreateProductValidator : AbstractValidator<CreateProductCommand> { public CreateProductValidator() { RuleFor(x => x.Name) .NotEmpty().WithMessage("Product name is required.") .MaximumLength(200).WithMessage("Product name cannot exceed 200 characters."); RuleFor(x => x.Price) .GreaterThan(0).WithMessage("Price must be greater than zero."); RuleFor(x => x.StockQuantity) .GreaterThanOrEqualTo(0).WithMessage("Stock quantity cannot be negative."); } }

The validator lives alongside the command and handler. Everything related to creating a product sits in one folder. You do not have to go hunting through three different directories to understand the feature.


Writing Your First Query

A query is simpler. It asks for data without changing anything.

The Query Class

csharp
using MediatR; namespace MyApp.Application.Queries.GetProductById; public record GetProductByIdQuery(Guid Id) : IRequest<ProductDetailResponse?>; public record ProductDetailResponse( Guid Id, string Name, string Description, decimal Price, int StockQuantity, DateTime CreatedAt );

The Query Handler

csharp
using MediatR; using MyApp.Application.Interfaces; namespace MyApp.Application.Queries.GetProductById; public class GetProductByIdHandler : IRequestHandler<GetProductByIdQuery, ProductDetailResponse?> { private readonly IProductRepository _repository; public GetProductByIdHandler(IProductRepository repository) { _repository = repository; } public async Task<ProductDetailResponse?> Handle( GetProductByIdQuery request, CancellationToken cancellationToken) { var product = await _repository.GetByIdAsync(request.Id, cancellationToken); if (product is null) return null; return new ProductDetailResponse( product.Id, product.Name, product.Description, product.Price, product.StockQuantity, product.CreatedAt ); } }

Notice how the query handler is completely independent from the command handler. They do not share models, they do not share logic, and they can evolve independently. If you need to optimize the read path later with a raw SQL query or a cached view, you can do it here without touching the write side.


The Controller: Clean and Thin

Here is what the controller looks like when MediatR handles the dispatching:

csharp
using MediatR; using Microsoft.AspNetCore.Mvc; using MyApp.Application.Commands.CreateProduct; using MyApp.Application.Queries.GetProductById; namespace MyApp.WebAPI.Controllers; [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly IMediator _mediator; public ProductsController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<IActionResult> Create( [FromBody] CreateProductCommand command, CancellationToken ct) { var result = await _mediator.Send(command, ct); return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); } [HttpGet("{id:guid}")] public async Task<IActionResult> GetById(Guid id, CancellationToken ct) { var result = await _mediator.Send(new GetProductByIdQuery(id), ct); return result is null ? NotFound() : Ok(result); } }

The controller is dead simple. It receives a request, wraps it in a command or query, sends it through MediatR, and returns the result. No business logic. No validation. No data access. Just traffic routing.

This is exactly what controllers should be.


Pipeline Behaviors: The Real Power of MediatR

This is where most people underestimate MediatR. The handler dispatch is nice, but the pipeline behaviors are where the actual value lives.

A pipeline behavior wraps every request that passes through MediatR. Think of it like ASP.NET middleware but for your application layer instead of your HTTP layer.

Free Newsletter

Enjoying the article? Stay in the loop.

  • Production-ready code samples every week
  • In-depth .NET, C# & React tutorials
  • Career tips & dev insights
500+ developers · No spam · Unsubscribe anytime

Join the community

Get new articles delivered every week.

No credit card · No spam · Cancel anytime · Learn more

Validation Behavior

This hooks FluentValidation into the MediatR pipeline so that every command and query gets validated automatically before it reaches the handler:

csharp
using FluentValidation; using MediatR; namespace MyApp.Application.Behaviors; public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull { private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) { _validators = validators; } public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { if (!_validators.Any()) return await next(); var context = new ValidationContext<TRequest>(request); var failures = _validators .Select(v => v.Validate(context)) .SelectMany(result => result.Errors) .Where(f => f is not null) .ToList(); if (failures.Any()) throw new ValidationException(failures); return await next(); } }

Logging Behavior

Want to log every request that comes through your application layer? One class handles it for all commands and queries:

csharp
using System.Diagnostics; using MediatR; using Microsoft.Extensions.Logging; namespace MyApp.Application.Behaviors; public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull { private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger; public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) { _logger = logger; } public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { var requestName = typeof(TRequest).Name; _logger.LogInformation("Handling {RequestName}", requestName); var stopwatch = Stopwatch.StartNew(); var response = await next(); stopwatch.Stop(); _logger.LogInformation( "Handled {RequestName} in {ElapsedMs}ms", requestName, stopwatch.ElapsedMilliseconds); return response; } }

Performance Monitoring Behavior

This one logs a warning if any request takes longer than a threshold:

csharp
using System.Diagnostics; using MediatR; using Microsoft.Extensions.Logging; namespace MyApp.Application.Behaviors; public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull { private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger; private readonly Stopwatch _timer; public PerformanceBehavior(ILogger<PerformanceBehavior<TRequest, TResponse>> logger) { _logger = logger; _timer = new Stopwatch(); } public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { _timer.Start(); var response = await next(); _timer.Stop(); var elapsed = _timer.ElapsedMilliseconds; if (elapsed > 500) { _logger.LogWarning( "Long running request: {RequestName} took {ElapsedMs}ms. Request: {@Request}", typeof(TRequest).Name, elapsed, request); } return response; } }

Register All Behaviors

csharp
using MyApp.Application.Behaviors; using FluentValidation; using System.Reflection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>)); }); builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());

Now every single command and query that flows through MediatR automatically gets logged, validated, and performance monitored. You wrote this logic once. It applies everywhere. That right there is the reason people reach for MediatR.


Folder Structure

One of the less discussed but highly practical benefits of CQRS with MediatR is the way it organizes your code. Instead of grouping by technical concern (all services in one folder, all DTOs in another), you group by feature:

CQRS Folder Structure with MediatR

Every feature is self-contained. If someone new joins the team and needs to understand how product creation works, they open one folder and everything is right there: the command, the handler, and the validator. There is no treasure hunt across six directories.

This is sometimes called feature slicing and it works beautifully with CQRS.


When CQRS and MediatR Are Worth It

Now for the part that actually matters. When should you reach for this combo?

Complex Business Logic

If your application has workflows that involve multiple steps, conditional branching, side effects (sending emails, publishing events, calling external APIs), and serious validation, CQRS gives you a clean way to organize that complexity. Each command handler deals with one specific operation. The handler can focus on getting it right without worrying about polluting a god service class.

Different Read and Write Models

This is the textbook use case. Your write operations work with rich domain entities that enforce business rules. But your read operations need to return flattened DTOs, aggregated views, or joined data from multiple tables. With CQRS, you can optimize each independently. Your query handler can bypass the repository and hit a raw SQL view if that is what gives you the best performance.

Cross-Cutting Concerns

If you need consistent logging, validation, authorization, caching, or error handling across many operations, the MediatR pipeline behaviors are a massive win. You write the behavior once and it applies to every request automatically. Without MediatR, you end up copy-pasting the same try-catch and validation boilerplate into every service method.

Large Teams

When multiple developers or squads work on the same application, CQRS with feature folders reduces merge conflicts and makes code ownership clearer. Team A works on the order commands. Team B works on the product queries. Their code does not intersect.

Event-Driven Requirements

If your application needs to publish domain events when certain actions happen (order placed, payment received, user registered), MediatR has built-in support for notifications. You can publish a ProductCreatedNotification from your command handler and have multiple independent handlers react to it without coupling them together.

csharp
using MediatR; namespace MyApp.Application.Notifications; public record ProductCreatedNotification(Guid ProductId, string Name) : INotification; public class SendWelcomeEmailHandler : INotificationHandler<ProductCreatedNotification> { public Task Handle(ProductCreatedNotification notification, CancellationToken ct) { // Send email logic here return Task.CompletedTask; } } public class UpdateSearchIndexHandler : INotificationHandler<ProductCreatedNotification> { public Task Handle(ProductCreatedNotification notification, CancellationToken ct) { // Update search index logic here return Task.CompletedTask; } }

Both handlers respond to the same event independently. Adding a new reaction means adding a new handler. You do not modify existing code.


When CQRS and MediatR Are NOT Worth It

Here is where I need to be honest, because blindly applying CQRS has caused more harm than good on many projects I have reviewed.

Simple CRUD Applications

If your application is essentially a thin layer over a database where most endpoints map directly to insert, update, delete, and select operations with minimal business logic, CQRS is overkill. You are creating a command, a handler, a validator, and a response class for what could be a five line service method.

A ProductService.CreateAsync() method that calls _context.Products.Add() and SaveChangesAsync() is perfectly fine for simple scenarios.

Small Projects with One or Two Developers

The overhead of CQRS is organizational. It pays off when the codebase is large enough and the team is big enough that the structure prevents chaos. If it is just you and maybe one other developer building a 10 endpoint API, the extra files and indirection will slow you down without giving you anything in return.

Prototypes and MVPs

Speed matters more than structure when you are validating an idea. Get it working first. If the product survives and grows, you can refactor toward CQRS later. Starting with it before you even know what the product looks like is premature architecture.

When You Only Use It for the Dispatch

I have seen codebases where MediatR is installed, commands and queries are created, but there are zero pipeline behaviors. No validation behavior. No logging behavior. No caching. Just the dispatch. At that point, MediatR is adding a layer of indirection for the sake of indirection. A simple service class with constructor injected dependencies would do the same job with less ceremony.

Serverless Functions

Azure Functions, AWS Lambda, and similar platforms are already structured around single purpose handlers. Adding MediatR inside a function that only does one thing is adding a mediator to mediate between one caller and one handler. That is pointless.


The Honest Cost-Benefit Breakdown

Let me lay it out in a table so you can make a practical decision:

Factor With CQRS and MediatR Without
File count High (command, handler, validator, response per feature) Lower (service method, DTO)
Cross-cutting concerns Centralized via pipeline behaviors Manual per-method or via middleware
Testability Excellent (each handler is isolated) Good (services are testable too)
Onboarding Steeper initial learning curve Simpler to understand at first
Scalability Handles growth well Can become messy at scale
Read/Write optimization Independent by design Requires discipline to separate
Debugging Request flow is less obvious (indirection) Direct call chains are easy to follow

A Word on Event Sourcing

People often conflate CQRS with event sourcing. They are not the same thing.

CQRS is about separating read and write responsibilities. You can do this with a single relational database. Your commands write through EF Core. Your queries read through Dapper or raw SQL. Same database, different paths.

Event sourcing is about storing state as a sequence of events rather than as a current snapshot. Instead of storing that a product has a price of 29.99, you store the events: product created, price set to 24.99, price updated to 29.99. You rebuild current state by replaying events.

You can use CQRS without event sourcing. Most teams do. Event sourcing adds significant complexity around event storage, event versioning, and state rebuilding. Only consider it if you have a genuine need for an audit trail of every state change or if you need to rebuild state at any point in time.

For most .NET applications, CQRS with a single database and MediatR for the pipeline is the sweet spot.


Free Newsletter

Enjoying the article? Stay in the loop.

  • Production-ready code samples every week
  • In-depth .NET, C# & React tutorials
  • Career tips & dev insights
500+ developers · No spam · Unsubscribe anytime

Join the community

Get new articles delivered every week.

No credit card · No spam · Cancel anytime · Learn more

Testing with CQRS and MediatR

One of the genuine wins of this pattern is how cleanly each handler can be tested in isolation:

csharp
using Moq; using MyApp.Application.Commands.CreateProduct; using MyApp.Application.Interfaces; using MyApp.Domain.Entities; namespace MyApp.Tests.Commands; public class CreateProductHandlerTests { private readonly Mock<IProductRepository> _repoMock; private readonly Mock<IUnitOfWork> _uowMock; private readonly CreateProductHandler _handler; public CreateProductHandlerTests() { _repoMock = new Mock<IProductRepository>(); _uowMock = new Mock<IUnitOfWork>(); _handler = new CreateProductHandler(_repoMock.Object, _uowMock.Object); } [Fact] public async Task Handle_ValidCommand_CreatesProductAndReturnsResult() { var command = new CreateProductCommand("Laptop", "Gaming laptop", 1500m, 10); _uowMock.Setup(u => u.SaveChangesAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(1); var result = await _handler.Handle(command, CancellationToken.None); Assert.Equal("Laptop", result.Name); Assert.Equal(1500m, result.Price); Assert.NotEqual(Guid.Empty, result.Id); _repoMock.Verify( r => r.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>()), Times.Once); } [Fact] public async Task Handle_AlwaysSavesChanges() { var command = new CreateProductCommand("Mouse", "Wireless", 25m, 100); _uowMock.Setup(u => u.SaveChangesAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(1); await _handler.Handle(command, CancellationToken.None); _uowMock.Verify( u => u.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once); } }

Each handler has a clear interface: one input, one output, well defined dependencies. Mocking is straightforward. No need to set up half the application just to test one piece of logic.

You can also test the validators independently:

csharp
using FluentValidation.TestHelper; using MyApp.Application.Commands.CreateProduct; namespace MyApp.Tests.Validators; public class CreateProductValidatorTests { private readonly CreateProductValidator _validator = new(); [Fact] public void Should_HaveError_When_NameIsEmpty() { var command = new CreateProductCommand("", "desc", 10m, 5); var result = _validator.TestValidate(command); result.ShouldHaveValidationErrorFor(x => x.Name); } [Fact] public void Should_HaveError_When_PriceIsZero() { var command = new CreateProductCommand("Laptop", "desc", 0m, 5); var result = _validator.TestValidate(command); result.ShouldHaveValidationErrorFor(x => x.Price); } [Fact] public void Should_NotHaveErrors_When_CommandIsValid() { var command = new CreateProductCommand("Laptop", "Good laptop", 999m, 10); var result = _validator.TestValidate(command); result.ShouldNotHaveAnyValidationErrors(); } }

Compare this to testing a monolithic service class with 15 methods and dependencies on 8 different repositories. The CQRS approach wins on clarity every time.


A Practical Migration Path

If you have an existing application and want to adopt CQRS and MediatR gradually, here is what I would recommend:

  1. Start with one feature. Pick a complex command that has validation and side effects. Refactor it to use a command, handler, and validator with MediatR.

  2. Add the validation behavior first. This is the quickest win. Centralized validation removes duplicate code across your controllers and services immediately.

  3. Convert commands before queries. Write operations tend to benefit more from the explicit command and handler structure. Queries can stay as simple service methods until you feel the need to separate them.

  4. Do not refactor everything at once. It is completely fine to have some endpoints using MediatR and others using traditional services. Consistency is nice, but a working application is nicer.

  5. Add pipeline behaviors as pain points emerge. Need centralized logging? Add the logging behavior. Slow queries? Add a caching behavior. Do not add all the behaviors upfront just because you can.


Wrapping Up

CQRS and MediatR are powerful tools in the .NET ecosystem, but they are tools, not requirements. The decision to use them should come from a genuine need, not from a blog post telling you it is the "modern" way to build APIs.

Here is my rule of thumb:

If your application has complex business logic, cross-cutting concerns that you are tired of duplicating, multiple developers stepping on each other, or genuinely different read and write requirements, CQRS with MediatR will make your life better.

If your application is a straightforward data entry tool with simple validation and a small team, keep it simple. You can always add these patterns later when the complexity justifies it.

The best architecture is the one that fits the problem you actually have today, not the problem you think you might have in two years.


Further Reading

Share this post

About the Author

Muhammad Rizwan

Muhammad Rizwan

Software Engineer · .NET & Cloud Developer

A passionate software developer with expertise in .NET Core, C#, JavaScript, TypeScript, React and Azure. Loves building scalable web applications and sharing practical knowledge with the developer community.


Did you find this helpful?

I would love to hear your thoughts. Your feedback helps me create better content for the community.

Leave Feedback

Related Articles

Explore more posts on similar topics

Vertical Slice Architecture in .NET - Alternative to Clean Architecture

Vertical Slice Architecture in .NET - Alternative to Clean Architecture

A hands-on guide to Vertical Slice Architecture in .NET. Learn why organizing code by feature instead of by layer leads to faster development, easier maintenance, and less ceremony with real C# code, honest opinions, and practical examples.

2026-02-2024 min read
Clean Architecture in .NET - Practical Guide

Clean Architecture in .NET - Practical Guide

A hands-on walkthrough of Clean Architecture in .NET - why it matters, how to structure your projects, and real code examples you can use today. No fluff, no over-engineering, just practical patterns that actually work in production.

2026-02-2416 min read
Repository Pattern Implementation in .NET 10

Repository Pattern Implementation in .NET 10

A complete walkthrough of implementing the Repository pattern in .NET 10 with Entity Framework Core. This guide covers the generic repository, specific repositories, the Unit of Work pattern, dependency injection, testing, and real production decisions with working C# code.

2026-02-2725 min read

Patreon Exclusive

Go deeper - exclusive content every month

Members get complete source-code projects, advanced architecture deep-dives, and monthly 1:1 code reviews.

$5/mo
Supporter
  • Supporter badge on website & my eternal gratitude
  • Your name listed on the website as a supporter
  • Monthly community Q&A (comments priority)
  • Early access to every new blog post
Join for $5/mo
Most Popular
$15/mo
Developer Pro
  • All Supporter benefits plus:
  • Exclusive .NET & Azure deep-dive posts (not on blog)
  • Full source-code project downloads every month
  • Downloadable architecture blueprints & templates
  • Private community access
Join for $15/mo
Best Value
$29/mo
Architect
  • All Developer Pro benefits plus:
  • Monthly 30-min 1:1 code review session
  • Priority answers to your architecture questions
  • Exclusive system design blueprints
  • Your name/logo featured on the website
  • Monthly live Q&A sessions
  • Early access to new courses or products
Join for $29/mo
Teams
$49/mo
Enterprise Partner
  • All Architect benefits plus:
  • Your company logo on my website & blog
  • Dedicated technical consultation session
  • Featured blog post about your company
  • Priority feature requests & custom content
Join for $49/mo

Secure billing via Patreon · Cancel anytime · Card & PayPal accepted

View Patreon page →

Your Feedback Matters

Have thoughts on my content, tutorials, or resources? I read every piece of feedback and use it to improve. No account needed. It only takes a minute.

Free Newsletter

Enjoying the article? Stay in the loop.

  • Production-ready code samples every week
  • In-depth .NET, C# & React tutorials
  • Career tips & dev insights
500+ developers · No spam · Unsubscribe anytime

Join the community

Get new articles delivered every week.

No credit card · No spam · Cancel anytime · Learn more