BrandGhost
How to Implement Proxy Pattern in C#: Step-by-Step Guide

How to Implement Proxy Pattern in C#: Step-by-Step Guide

How to Implement Proxy Pattern in C#: Step-by-Step Guide

If you have ever needed to control access to an object, delay its creation, or layer on cross-cutting behavior without changing the original class, the proxy pattern in C# is your go-to structural design pattern. A proxy acts as a stand-in that sits between calling code and the real object, intercepting every call so you can add logic such as lazy initialization, caching, or authorization. In this guide, you will walk through a complete, step-by-step implementation -- from defining the subject interface all the way to wiring proxies into a dependency injection container.

The proxy pattern in C# shares some surface-level similarities with other structural patterns. The decorator design pattern also wraps an object behind the same interface, but decorators focus on adding new behavior while proxies focus on controlling access. Likewise, the adapter design pattern converts one interface to another, whereas a proxy preserves the same interface. Keeping these distinctions in mind will help you reach for the right tool in each situation.

What Is the Proxy Pattern?

The proxy pattern is a structural design pattern in which a surrogate object controls access to another object. The surrogate -- the proxy -- implements the same interface as the target object, so consumers never know they are talking to a stand-in. Under the hood, the proxy can intercept calls and add behavior before or after delegating to the real object.

There are several common proxy variants worth understanding before you start coding:

  • Virtual proxy -- Defers creation of an expensive object until it is actually needed.
  • Caching proxy -- Stores results of expensive operations and returns the cached value on subsequent calls.
  • Protection proxy -- Checks authorization or permissions before allowing access to the real object.
  • Remote proxy -- Represents an object that lives in a different process, machine, or network boundary.
  • Logging proxy -- Records calls, arguments, and results for diagnostics.

The examples in this guide focus on virtual, caching, and protection proxies because they cover the most common scenarios in C# applications and demonstrate distinct proxy responsibilities.

Step 1: Define the Subject Interface

Every proxy implementation begins with an interface that both the real object and the proxy implement. This interface is the contract that the rest of your application depends on.

For this walkthrough, imagine a document management system where loading and saving documents involves expensive database or API calls. Here is the subject interface:

public interface IDocumentService
{
    Document GetDocument(string documentId);

    void SaveDocument(Document document);
}
public sealed class Document
{
    public string Id { get; init; } = string.Empty;

    public string Title { get; init; } = string.Empty;

    public string Content { get; init; } = string.Empty;
}

The interface is intentionally small. Two methods are enough to demonstrate each proxy variant without cluttering the example. Notice that Document is a simple immutable class -- its properties use init setters, which aligns well with domain objects that should not change once created.

By depending on IDocumentService rather than a concrete class, every consumer in your codebase automatically supports any proxy you drop in later. This idea ties directly into inversion of control, where the application structure depends on abstractions rather than concrete implementations.

Step 2: Create the RealSubject

The RealSubject is the class that does the actual work. In a real system, this would hit a database, call a REST API, or perform some other expensive operation. For clarity, the example simulates that cost with a short delay:

public sealed class DocumentService : IDocumentService
{
    public Document GetDocument(string documentId)
    {
        // Simulate an expensive database or API call
        Thread.Sleep(500);

        return new Document
        {
            Id = documentId,
            Title = $"Document {documentId}",
            Content = "Full content loaded from the database.",
        };
    }

    public void SaveDocument(Document document)
    {
        // Simulate persisting to a database
        Thread.Sleep(300);

        Console.WriteLine(
            $"Document '{document.Title}' saved successfully.");
    }
}

The Thread.Sleep calls simulate latency so you can observe the proxy pattern in C# making a real difference when you add caching or lazy loading on top. In production, you would replace these with actual database queries or HTTP requests. The important point is that DocumentService contains the expensive logic and knows nothing about proxies -- it simply does its job.

Step 3: Build a Virtual Proxy

A virtual proxy delays object creation until the caller actually needs it. This is useful when constructing the real service is costly and you want to avoid paying that cost if the service is never used during a particular request.

The .NET Lazy<T> class is a natural fit here because it handles thread-safe deferred initialization out of the box:

public sealed class LazyDocumentProxy : IDocumentService
{
    private readonly Lazy<IDocumentService> _lazyService;

    public LazyDocumentProxy(Func<IDocumentService> serviceFactory)
    {
        _lazyService = new Lazy<IDocumentService>(serviceFactory);
    }

    public Document GetDocument(string documentId)
    {
        return _lazyService.Value.GetDocument(documentId);
    }

    public void SaveDocument(Document document)
    {
        _lazyService.Value.SaveDocument(document);
    }
}

The proxy accepts a factory delegate instead of the service itself. The Lazy<T> wrapper ensures the factory runs exactly once, and only when _lazyService.Value is first accessed. Every subsequent call reuses the same instance.

Why does this matter? Consider a controller or handler that receives IDocumentService through dependency injection but only calls it on certain code paths. Without a virtual proxy, the container would eagerly construct DocumentService -- including any database connections or API clients it depends on -- even if the service is never invoked. The proxy pattern in C# lets you defer that cost gracefully.

Step 4: Build a Caching Proxy

A caching proxy stores the results of expensive read operations and returns them on subsequent calls. This variant is especially valuable when the same document is requested multiple times within a short window:

public sealed class CachingDocumentProxy : IDocumentService
{
    private readonly IDocumentService _innerService;
    private readonly Dictionary<string, Document> _cache = new();

    public CachingDocumentProxy(IDocumentService innerService)
    {
        _innerService = innerService;
    }

    public Document GetDocument(string documentId)
    {
        if (_cache.TryGetValue(documentId, out var cached))
        {
            return cached;
        }

        var document = _innerService.GetDocument(documentId);
        _cache[documentId] = document;

        return document;
    }

    public void SaveDocument(Document document)
    {
        _innerService.SaveDocument(document);

        // Invalidate the cache entry so subsequent
        // reads pick up the updated version
        _cache[document.Id] = document;
    }
}

The proxy wraps any IDocumentService implementation and adds a Dictionary<string, Document> as an in-memory cache. On the first call to GetDocument, the proxy delegates to the inner service and stores the result. On every subsequent call for the same ID, the cached value is returned without touching the database.

The SaveDocument method is just as important. After saving, the proxy updates its cache entry so that stale data is never returned. In a production system you might introduce time-based expiration, a distributed cache like Redis, or a ConcurrentDictionary for thread safety. The pattern itself stays the same -- the proxy intercepts the call, checks the cache, and delegates only when necessary.

Notice that the caching proxy composes over IDocumentService, not over DocumentService directly. This means you can stack it on top of any other proxy or the real service. Composition over inheritance is one of the core strengths of structural design patterns, and it pairs well with how the facade design pattern simplifies complex subsystem interactions behind a clean interface.

Step 5: Build a Protection Proxy

A protection proxy adds authorization checks before allowing access to the underlying service. This is a clean way to enforce security rules at the service boundary without scattering authorization logic across controllers and handlers:

public sealed class AuthorizingDocumentProxy : IDocumentService
{
    private readonly IDocumentService _innerService;
    private readonly IUserContext _userContext;

    public AuthorizingDocumentProxy(
        IDocumentService innerService,
        IUserContext userContext)
    {
        _innerService = innerService;
        _userContext = userContext;
    }

    public Document GetDocument(string documentId)
    {
        if (!_userContext.HasPermission("Documents.Read"))
        {
            throw new UnauthorizedAccessException(
                "You do not have permission to read documents.");
        }

        return _innerService.GetDocument(documentId);
    }

    public void SaveDocument(Document document)
    {
        if (!_userContext.HasPermission("Documents.Write"))
        {
            throw new UnauthorizedAccessException(
                "You do not have permission to save documents.");
        }

        _innerService.SaveDocument(document);
    }
}
public interface IUserContext
{
    bool HasPermission(string permission);
}

The protection proxy pattern in C# sits between the caller and the real service, checking the current user's permissions before every operation. If the check fails, an UnauthorizedAccessException is thrown immediately -- the inner service is never invoked. This approach keeps security enforcement consistent regardless of which part of the codebase invokes the service.

By accepting IUserContext through the constructor, the proxy remains testable. In unit tests, you can supply a stub that returns true or false to verify both the happy path and the rejection path. This is the same constructor injection approach you would use with the strategy design pattern, where the behavior varies based on the injected dependency.

Step 6: Wire Proxies Into Dependency Injection

Building proxies is only half the story. You also need to wire them into your DI container so the rest of the application receives the proxied version automatically. There are two common approaches in ASP.NET Core.

Manual Decoration

The simplest approach uses the built-in IServiceCollection directly. You register the real service and then wrap it with each proxy layer:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

// Register supporting services
services.AddScoped<IUserContext, HttpUserContext>();

// Register the real service
services.AddScoped<DocumentService>();

// Register the proxy chain: authorization -> caching -> real
services.AddScoped<IDocumentService>(provider =>
{
    var realService = provider.GetRequiredService<DocumentService>();
    var cachingProxy = new CachingDocumentProxy(realService);

    var userContext = provider.GetRequiredService<IUserContext>();
    var authProxy = new AuthorizingDocumentProxy(
        cachingProxy,
        userContext);

    return authProxy;
});

When any class requests IDocumentService, the container returns the outermost proxy -- the AuthorizingDocumentProxy. That proxy delegates to the CachingDocumentProxy, which in turn delegates to the real DocumentService. Understanding how IServiceCollection in C# handles registrations helps clarify why this factory-based overload works.

This manual approach is explicit and easy to debug, but it becomes tedious when you have many decorated services.

Decoration With Scrutor

Scrutor is a popular NuGet package that adds decoration support to IServiceCollection. It lets you register proxy layers declaratively:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddScoped<IUserContext, HttpUserContext>();

// Register the real implementation
services.AddScoped<IDocumentService, DocumentService>();

// Stack proxies using Scrutor's Decorate method
services.Decorate<IDocumentService, CachingDocumentProxy>();
services.Decorate<IDocumentService, AuthorizingDocumentProxy>();

Each call to Decorate wraps the previous registration. The order matters -- the last decoration becomes the outermost layer. In this example, authorization runs first (outermost), then caching, then the real service. This is significantly cleaner than the manual approach and scales well when your project has dozens of services that need proxy layers.

Whichever approach you choose, the key benefit of the proxy pattern in C# combined with DI is that consumers never know about the proxies. A controller receives IDocumentService and calls GetDocument without any awareness that authorization and caching are happening behind the scenes.

Putting It All Together

Here is a quick console example that demonstrates the proxy chain in action without DI, so you can run it standalone and see the output:

var realService = new DocumentService();
var cachingProxy = new CachingDocumentProxy(realService);
var userContext = new SimpleUserContext(
    permissions: new[] { "Documents.Read", "Documents.Write" });
var authProxy = new AuthorizingDocumentProxy(
    cachingProxy, userContext);

IDocumentService service = authProxy;

// First call: hits authorization, misses cache, 
// calls real service
var doc = service.GetDocument("doc-42");
Console.WriteLine($"Loaded: {doc.Title}");

// Second call: hits authorization, returns cached result
var cachedDoc = service.GetDocument("doc-42");
Console.WriteLine($"Cached: {cachedDoc.Title}");

The first call to GetDocument passes through the authorization check, misses the cache, and delegates to the real service. The second call passes authorization again but returns the cached result instantly. Each proxy in the chain handles its own concern and delegates everything else.

When to Use Each Proxy Variant

Choosing the right proxy variant depends on the problem you are solving. Here is a practical breakdown to help guide your decision:

A virtual proxy is best suited for situations where the real service is expensive to construct and might not be used during every request. If your application resolves IDocumentService in a controller but only uses it on specific endpoints, the virtual proxy avoids wasted initialization.

A caching proxy makes sense when the same data is requested repeatedly within a session or request scope. It reduces redundant calls to databases or APIs and can dramatically improve response times for read-heavy workloads.

A protection proxy is ideal when you need consistent, centralized authorization enforcement. Rather than scattering if (!user.HasPermission(...)) checks across multiple controllers and services, the proxy guarantees that every call passes through the same security gate.

You can also combine these variants by stacking proxies, as shown in the DI examples. The proxy pattern in C# is composable by design -- each layer handles one concern and delegates the rest.

Frequently Asked Questions

What is the difference between the proxy pattern and the decorator pattern in C#?

Both patterns wrap an object behind the same interface, but they serve different purposes. The proxy pattern controls access to the wrapped object -- it can defer creation, cache results, or enforce authorization. The decorator pattern extends behavior by adding new responsibilities. In practice, the implementation looks similar, but the intent differs. Use a proxy when you want to gate or mediate access; use a decorator when you want to augment functionality.

Can I stack multiple proxies on the same service?

Yes. Because every proxy implements the same interface and accepts any IDocumentService through its constructor, you can chain as many proxies as needed. The outermost proxy receives the call first, processes its concern, and delegates inward. This composability is one of the strongest advantages of the proxy pattern in C#.

How does the proxy pattern work with dependency injection in ASP.NET Core?

You register the real service and then wrap it with proxy layers either manually (using a factory overload on IServiceCollection) or declaratively using Scrutor's Decorate method. Consumers request IDocumentService and receive the fully proxied chain without any code changes.

Is the caching proxy thread-safe?

The example in this guide uses a simple Dictionary<string, Document>, which is not thread-safe. For production use, swap it for a ConcurrentDictionary<string, Document> or use IMemoryCache from Microsoft.Extensions.Caching.Memory. The proxy pattern itself does not dictate the caching strategy -- it simply provides the interception point.

When should I use a virtual proxy instead of just registering the service as transient?

Transient registration creates a new instance per request, but it still creates the instance eagerly when resolved. A virtual proxy defers creation until the first method call. This matters when the real service has expensive constructor logic -- such as opening database connections or initializing large data structures -- and might not be needed on every code path.

How do I test a proxy in isolation?

Inject a mock or stub implementation of IDocumentService into the proxy's constructor. This lets you verify that the proxy applies its logic (caching hits, authorization rejections, lazy initialization) without depending on the real service. For the protection proxy, also inject a stub IUserContext to simulate different permission scenarios.

Can I use the proxy pattern with async methods?

Absolutely. Change IDocumentService methods to return Task<Document> and Task, and update each proxy to await the inner service call. The structural pattern remains identical -- the proxy intercepts, applies its logic, and delegates. Async proxies work seamlessly with ASP.NET Core's async pipeline and DI container.

Proxy Design Pattern in C#: Complete Guide with Examples

Master the proxy design pattern in C# with practical examples showing virtual proxies, protection proxies, caching proxies, and lazy loading implementations.

Decorator vs Proxy Pattern in C#: Key Differences Explained

Compare decorator vs proxy pattern in C# with side-by-side code examples, key differences explained, and guidance on when to use each pattern.

How to Implement Bridge Pattern in C#: Step-by-Step Guide

Learn how to implement the bridge pattern in C# with step-by-step code examples covering abstraction hierarchies, implementor interfaces, and DI registration.

An error has occurred. This application may no longer respond until reloaded. Reload