Skip to main content

Command Palette

Search for a command to run...

MediatR Pipeline Behaviors vs Decorator Pattern vs DI Interceptors in ASP.NET Core: Enterprise Cross-Cutting Concern Decision Guide

Published
โ€ข13 min read
MediatR Pipeline Behaviors vs Decorator Pattern vs DI Interceptors in ASP.NET Core: Enterprise Cross-Cutting Concern Decision Guide

Every enterprise ASP.NET Core codebase eventually confronts the same problem: you need logging, validation, caching, auditing, or retry logic applied consistently across dozens โ€” sometimes hundreds โ€” of service operations, without scattering that logic everywhere it is needed. The HTTP pipeline solves this for inbound requests. But what about the service layer beneath it? That is where three competing mechanisms enter the picture: MediatR pipeline behaviors, the decorator pattern registered through your DI container, and DI-level interceptors via libraries like Castle Windsor or Scrutor. Each can implement cross-cutting concerns at the service layer. Each carries a different cost in coupling, testability, flexibility, and operational complexity. Enterprise teams that pick the wrong one pay for it through years of rigid pipelines, untestable handlers, or architecturally inconsistent codebases.

๐ŸŽ Want implementation-ready .NET source code you can drop straight into your project? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. ๐Ÿ‘‰ https://www.patreon.com/CodingDroplets


Why the Service Layer Is the Hard Part

ASP.NET Core middleware handles HTTP-level cross-cutting concerns elegantly. Authentication, CORS, response compression, request logging โ€” these live comfortably in Program.cs and operate on HttpContext. The problem is that most enterprise logic does not live at the HTTP boundary. It lives in application services, command handlers, domain operations, and use cases that may be invoked from HTTP endpoints, background jobs, message consumers, or CLI tools. Middleware cannot reach those. You need a different strategy.

The three main approaches each represent a distinct architectural commitment:

  • MediatR pipeline behaviors require adoption of the Mediator pattern and introduce MediatR as a dependency throughout your application layer.

  • The decorator pattern uses your standard DI container (Microsoft.Extensions.DependencyInjection or Scrutor) to wrap service registrations without coupling the decorated type to any pipeline.

  • DI interceptors (via Castle Windsor, Autofac, or similar containers) use dynamic proxies to intercept method calls at the container level, requiring no changes to the decorated service but introducing a heavier container dependency.

Understanding the architectural implications โ€” not just the mechanics โ€” is what separates teams that make this decision well from teams that paint themselves into a corner.


MediatR Pipeline Behaviors: The Full Picture

What They Are

IPipelineBehavior<TRequest, TResponse> is MediatR's hook for wrapping command and query handlers. You register behaviors globally, and each behavior runs for every request type flowing through the mediator unless you apply marker interfaces or type constraints to limit scope. A validation behavior checks IValidator<TRequest> for registered validators. A logging behavior records timing. A caching behavior checks a store before invoking the handler. They compose naturally โ€” each behavior calls the next handler in the chain via a delegate โ€” forming an explicit, traceable pipeline.

Enterprise Strengths

Explicit pipeline visibility. The behavior chain is a first-class concept. New team members can read the registered behaviors in Program.cs and immediately understand what wraps every handler. This explicitness is a serious advantage in enterprise codebases where institutional knowledge is fragile.

Clean handler isolation. Handlers have zero awareness of validation, logging, or caching concerns. Each handler's single responsibility is domain logic. This purity makes unit testing handlers dramatically simpler.

Type-targeted application. Using marker interfaces like ICacheableQuery or ITransactional, you can make behaviors activate conditionally without polluting every handler that does not need them. This solves the "5% of handlers need caching, 95% should not pay the cost" problem cleanly.

Framework alignment. MediatR is the de facto standard for CQRS in .NET. If your team already uses it, pipeline behaviors are the idiomatic extension point. Fighting this by layering another mechanism creates conceptual duplication.

Enterprise Limitations

MediatR lock-in. Once your application layer is organized around IRequest<TResponse> and IRequestHandler, it is difficult to extract that logic without a significant rewrite. This is an acceptable trade-off in most enterprise applications but it is a real architectural commitment.

Global scope requires discipline. A behavior registered globally runs for every handler unless you explicitly filter it. Teams that add behaviors without considering scope can introduce unintended overhead or side effects across the entire command/query surface.

Not suited for non-MediatR services. If parts of your codebase do not use MediatR โ€” legacy services, third-party adapters, domain services not organized as commands โ€” pipeline behaviors cannot reach them.


The Decorator Pattern: DI-Native and Container-Portable

What It Is

The decorator pattern wraps a service implementation with another implementation of the same interface. The outer decorator adds behavior โ€” logging, caching, retry โ€” then delegates to the inner implementation. With Microsoft.Extensions.DependencyInjection natively, this requires manual registration. With Scrutor (a popular thin extension), you can use .Decorate<IOrderService, CachingOrderService>() to chain decorators cleanly without verbose manual registration code.

Enterprise Strengths

Zero framework dependency on the decorated type. The service being decorated knows nothing about the decorator. It implements its interface and nothing else. This makes the underlying service fully portable and testable in complete isolation.

Works with any service, not just command handlers. Domain services, repository interfaces, notification senders, external API adapters โ€” any interface-registered service can be decorated. This universality makes the decorator pattern the right default for teams not fully committed to CQRS.

Standard DI semantics. Decorators are registered and resolved through the same DI container your team already understands. There is no separate pipeline concept to learn, debug, or explain to new engineers.

Granular targeting. Unlike global MediatR behaviors, each decorator is explicitly applied to a specific service. This makes accidental over-application impossible. You cannot accidentally apply a caching decorator to a service that should never cache.

Enterprise Limitations

DI registration verbosity at scale. In large codebases with dozens of decorated services, the registration code grows. Scrutor mitigates this substantially, but the registration surface is still larger than a single MediatR behavior.

Chaining creates nesting. Multiple decorators applied to the same service create a nesting structure (LoggingDecorator<CachingDecorator<OrderService>>) that can become confusing when debugging. Resolving the correct decorator in tests requires careful setup.

Interface discipline is mandatory. The decorator pattern only works when services are registered against interfaces. Teams with direct concrete registrations or service locator patterns cannot retrofit decorators easily.


DI Interceptors: Power at the Cost of Container Coupling

What They Are

DI interceptors use dynamic proxy generation โ€” typically via Castle Windsor's IInterceptor or Autofac's interception module โ€” to intercept method calls at the container level. Unlike decorators, you do not write a wrapper class per interface. You write a single interceptor class and instruct the container to apply it to matching registrations. The container generates a proxy type at runtime that routes calls through the interceptor before reaching the real implementation.

Enterprise Strengths

Write once, apply broadly. A single logging interceptor can be applied to every service matching a naming convention or attribute. This eliminates the per-interface wrapper overhead that decorators require at scale.

Non-invasive for existing code. Retrofitting cross-cutting concerns onto a codebase that was not designed for them is significantly easier with interceptors. Existing services require no modification.

Attribute-driven targeting. Libraries like Castle Windsor support attribute-based interception, letting you mark individual methods rather than applying behavior at the class or registration level.

Enterprise Limitations

Container coupling is the price of admission. Interceptors require a DI container that supports dynamic proxy generation. Switching from Castle Windsor or Autofac to Microsoft.Extensions.DependencyInjection natively is painful when interceptors are embedded throughout. This is a non-trivial architectural constraint in a world where .NET teams increasingly standardize on the built-in container.

Runtime proxy overhead and debugging friction. Interceptors generate types at runtime. Stack traces are polluted with proxy frames, making debugging harder. Performance overhead โ€” while typically small โ€” is not zero and can compound in hot paths.

Implicit magic. Unlike the explicit registration of decorators or the visible behavior chain in MediatR, interceptors are applied through conventions or attributes that are easy to miss when reading the codebase. Onboarding complexity increases.


The Decision Framework: Which Mechanism Fits When

The choice is not about which approach is best in the abstract. It is about which approach fits your team's current architecture, dependencies, and growth trajectory.

Choose MediatR pipeline behaviors when:

  • Your application layer is already organized around CQRS with commands and queries

  • You want a single, visible, easily auditable pipeline for all handler cross-cutting concerns

  • Your team values explicitness over convention

  • You need selective behavior activation through marker interfaces without modifying handlers

Choose the decorator pattern when:

  • Not all your services flow through MediatR

  • You want DI-native, container-portable cross-cutting concerns

  • You need granular, per-service control over which behaviors apply

  • Your team prioritizes testability and hates invisible magic

Choose DI interceptors when:

  • You need to retrofit cross-cutting concerns across a large existing codebase with minimal changes

  • You are already committed to a container that supports dynamic proxies (Castle Windsor, Autofac)

  • You need method-level granularity that neither MediatR behaviors nor service-level decorators can provide efficiently

Avoid mixing all three indiscriminately. The most common enterprise mistake is using MediatR behaviors for handlers, decorators for some domain services, and interceptors for legacy adapters โ€” resulting in three separate pipelines with no unified visibility. Teams spending time debugging this combination almost universally conclude they should have committed to one primary mechanism earlier.


Composition Boundaries: Where Each Mechanism Lives Architecturally

Understanding where each mechanism operates in a layered architecture clarifies the choice.

MediatR pipeline behaviors live in the Application layer. They have access to application-level services (validators, caches, unit of work), and they operate per-request in the CQRS pipeline. They are the right home for application-layer policies โ€” validation before handling, transaction wrapping around handling, audit logging of commands.

Decorators live at the service registration boundary โ€” conceptually between layers. A caching decorator for IProductRepository lives in Infrastructure; a logging decorator for INotificationService lives wherever that service is registered. This flexibility is both a strength and a governance challenge: teams must decide where decorator registrations belong and enforce it.

DI interceptors also live at the composition root โ€” typically in the startup/host configuration โ€” but their runtime behavior reaches into whatever layer the intercepted service belongs to. This transversal nature is powerful for infrastructure concerns (audit logging, metrics) but dangerous when interceptors start containing business logic.


Performance Considerations Enterprise Teams Overlook

MediatR behaviors add one delegate invocation per behavior per request. In practice, this is unmeasurable in normal API workloads. The real performance concern is behaviors doing expensive work (synchronous I/O, large object allocations) that should be async or avoided.

Decorators add one virtual dispatch per decorator per call. This is also negligible. The concern is accidental double-decoration (a service decorated twice due to a registration mistake) or circular decoration (a service that decorates itself through shared state).

DI interceptors add reflection and proxy overhead per method call. For the vast majority of business logic, this is not a bottleneck. However, in tight loops or high-frequency services called millions of times per second, the overhead is measurable. Profile before assuming it matters.


Operational and Team Considerations

Onboarding clarity. MediatR behaviors win here. The pipeline is explicit and documented by convention. Decorators require knowing where to look in registration code. Interceptors require understanding proxy generation โ€” a concept unfamiliar to developers newer to .NET.

Testability. The decorator pattern produces the most testable code โ€” each decorator is a concrete class that wraps a known interface, trivially mockable. MediatR behaviors are also testable in isolation. DI interceptors require container setup in tests, which adds friction.

Refactoring safety. When interfaces change, decorators and behaviors break at compile time. DI interceptors using string-based method matching break at runtime โ€” a significant regression risk in enterprise codebases without comprehensive integration test coverage.


The Hybrid Approach That Actually Works

For teams with mature codebases, the pragmatic answer is often a constrained hybrid:

  • MediatR pipeline behaviors for all command/query handler cross-cutting concerns (validation, auditing, transactions)

  • Decorators via Scrutor for domain service and repository cross-cutting concerns that live outside the CQRS pipeline

  • No DI interceptors unless inheriting a codebase already committed to Castle Windsor or Autofac

This hybrid is disciplined โ€” two mechanisms with clear boundaries โ€” rather than three mechanisms applied opportunistically. The key governance rule: pick the mechanism based on which layer the service lives in, not based on which was easiest to implement at the time.


โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!


Frequently Asked Questions

Q: Can I use MediatR pipeline behaviors without adopting full CQRS? Yes. MediatR can be used as a simple in-process mediator without strict CQRS discipline. You can route service calls through IMediator for behaviors while maintaining a service-oriented rather than command/query-oriented design. However, this creates a hybrid that can confuse teams about when to use commands versus plain service calls. Establish a clear convention before going this route.

Q: Does the decorator pattern work without Scrutor? Yes. With Microsoft.Extensions.DependencyInjection natively, you register decorators manually: register the inner implementation as a concrete type, then register the outer decorator resolving the inner from the container. Scrutor simply automates this pattern with a cleaner syntax. For small codebases with few decorators, manual registration is fine.

Q: Is Autofac still worth adopting in 2026 just for interceptors? For most greenfield enterprise projects, no. The built-in ASP.NET Core DI container has matured significantly. Unless you are inheriting an Autofac codebase, the additional complexity and coupling of adopting a third-party container solely for interceptors is hard to justify. The decorator pattern achieves similar goals with native tooling.

Q: How do I avoid a MediatR behavior accidentally running for every handler when I only want it for some? Use a marker interface. Define ICacheable or ITransactional on your request type, then in the behavior's constructor or Handle method, check whether TRequest implements the marker. Behaviors do not activate their logic for requests that do not implement the marker. This is the standard pattern for conditional behavior activation.

Q: Can pipeline behaviors, decorators, and DI interceptors be unit tested the same way? Not quite. Decorators are the easiest โ€” instantiate the decorator with a mock inner service and call the method directly. MediatR behaviors require constructing a RequestHandlerDelegate mock but are still straightforward. DI interceptors are hardest โ€” they require a configured container or a specific proxy framework setup in the test, adding overhead and coupling tests to the container configuration.

Q: What happens to cross-cutting concerns when I invoke a command handler directly without going through MediatR? The pipeline behaviors are bypassed entirely. This is a common source of bugs when teams unit-test handlers directly (intentionally bypassing behaviors) but then discover that their production integration tests fail because a behavior they forgot about is not applied. Always be explicit about whether your tests intend to exercise the full pipeline or the handler in isolation.

Q: Is there a performance difference significant enough to influence the choice between these three approaches? In the vast majority of enterprise API workloads, no. All three mechanisms introduce overhead in the microseconds-to-nanoseconds range per operation. The performance choice that matters is not which mechanism but what the cross-cutting concern does โ€” a behavior that makes a synchronous database call will dominate all mechanism overhead by orders of magnitude. Profile the concern's implementation, not the wrapping mechanism.

More from this blog

C

Coding Droplets

119 posts