BrandGhost
Proxy Design Pattern in C#: Complete Guide with Examples

Proxy Design Pattern in C#: Complete Guide with Examples

Proxy Design Pattern in C#: Complete Guide with Examples

When you need to control access to an object -- whether that means deferring expensive initialization, enforcing security rules, or caching results -- the proxy design pattern in C# is the structural pattern designed for exactly that job. A proxy acts as a stand-in for another object, implementing the same interface so that client code never needs to know it's not dealing with the real thing. This indirection gives you a powerful hook to add logic before or after the real object does its work.

In this complete guide, we'll cover the core structure of the proxy design pattern, walk through four practical proxy variations with full C# code examples, explore how proxies integrate with dependency injection in .NET, and address common questions. By the end, you'll have a clear understanding of when and how to apply the proxy pattern in your own projects.

What Is the Proxy Design Pattern?

The proxy design pattern is a structural design pattern from the Gang of Four (GoF) catalog that provides a surrogate or placeholder for another object to control access to it. Rather than letting clients interact with the real object directly, you introduce a proxy that sits between the client and the real subject. The proxy handles the request, optionally performs additional work, and then delegates to the real object when appropriate.

The pattern involves three core participants. The Subject is an interface or abstract class that defines the contract both the real object and the proxy must follow. The Real Subject is the actual object that does the heavy lifting -- it contains the core business logic or resource. The Proxy implements the same interface as the real subject, holds a reference to it, and controls access to it.

This structure looks similar to the decorator design pattern in C#, and that's intentional -- both patterns use composition and shared interfaces. The key difference is intent. Decorators add behavior to an object. Proxies control access to an object. A decorator always delegates to the wrapped component; a proxy might delay that delegation, deny it entirely, or cache the result to avoid delegating again.

Core Proxy Structure in C#

Let's start with the foundational structure that the proxy design pattern builds on. We'll use a document loading scenario to keep things concrete:

public interface IDocument
{
    string GetContent();
}

public class RealDocument : IDocument
{
    private readonly string _content;

    public RealDocument(string filePath)
    {
        Console.WriteLine(
            $"Loading document from {filePath}...");

        // Simulate expensive file I/O
        Thread.Sleep(2000);
        _content = $"Content of {filePath}";
    }

    public string GetContent()
    {
        return _content;
    }
}

public class DocumentProxy : IDocument
{
    private readonly string _filePath;
    private RealDocument? _realDocument;

    public DocumentProxy(string filePath)
    {
        _filePath = filePath;
    }

    public string GetContent()
    {
        _realDocument ??= new RealDocument(_filePath);
        return _realDocument.GetContent();
    }
}

The client code works against IDocument and never knows whether it's holding a proxy or the real document:

IDocument document = new DocumentProxy("report.pdf");

// No loading happens yet -- the proxy defers creation
Console.WriteLine("Document proxy created.");

// Loading happens here, on first access
string content = document.GetContent();
Console.WriteLine(content);

This is the essence of the proxy design pattern. The proxy implements the same interface, holds a reference to the real subject (or the information needed to create it), and controls when and how delegation occurs.

Virtual Proxy: Lazy Loading Expensive Resources

A virtual proxy is the most common variation of the proxy design pattern. It defers creation of an expensive object until the client actually needs it. This is especially useful when your application creates many objects upfront but only accesses a subset of them during any given session.

Consider an image gallery where each image file is large and expensive to load into memory:

public interface IImage
{
    void Display();
    int GetSizeInBytes();
}

public class HighResolutionImage : IImage
{
    private readonly string _fileName;
    private readonly byte[] _imageData;

    public HighResolutionImage(string fileName)
    {
        _fileName = fileName;
        Console.WriteLine(
            $"Loading high-res image: {fileName}");

        // Simulate loading a large file
        _imageData = new byte[10_000_000];
        new Random().NextBytes(_imageData);
    }

    public void Display()
    {
        Console.WriteLine(
            $"Displaying {_fileName} " +
            $"({_imageData.Length:N0} bytes)");
    }

    public int GetSizeInBytes() => _imageData.Length;
}

public class ImageProxy : IImage
{
    private readonly string _fileName;
    private HighResolutionImage? _realImage;

    public ImageProxy(string fileName)
    {
        _fileName = fileName;
    }

    public void Display()
    {
        _realImage ??= new HighResolutionImage(_fileName);
        _realImage.Display();
    }

    public int GetSizeInBytes()
    {
        if (_realImage is null)
        {
            return 0;
        }

        return _realImage.GetSizeInBytes();
    }
}

The gallery can create hundreds of ImageProxy instances without consuming memory for images the user never scrolls to:

List<IImage> gallery =
[
    new ImageProxy("photo1.jpg"),
    new ImageProxy("photo2.jpg"),
    new ImageProxy("photo3.jpg"),
];

// Only the image the user clicks on gets loaded
gallery[1].Display();

This lazy loading approach is conceptually related to how the flyweight design pattern in C# minimizes memory consumption -- both patterns are concerned with resource efficiency, but they attack the problem from different angles. The flyweight shares intrinsic state across many objects, while the proxy design pattern delays object creation entirely.

Protection Proxy: Access Control Based on Roles

A protection proxy uses the proxy design pattern to restrict access to an object based on the caller's permissions. Instead of scattering authorization checks throughout your business logic, you centralize them in the proxy. This keeps your real subject clean and focused on its core responsibility.

Here's an example with a sensitive data service that should only be accessible to users with specific roles:

public interface IConfidentialDataService
{
    string GetSalaryData(int employeeId);
    void UpdateSalary(int employeeId, decimal newSalary);
}

public class ConfidentialDataService
    : IConfidentialDataService
{
    public string GetSalaryData(int employeeId)
    {
        return $"Salary for employee {employeeId}: $95,000";
    }

    public void UpdateSalary(
        int employeeId,
        decimal newSalary)
    {
        Console.WriteLine(
            $"Updated employee {employeeId} " +
            $"salary to {newSalary:C}");
    }
}

public sealed class UserContext
{
    public required string Username { get; init; }
    public required IReadOnlyList<string> Roles { get; init; }
}

public class ProtectionProxy : IConfidentialDataService
{
    private readonly IConfidentialDataService _inner;
    private readonly UserContext _userContext;

    public ProtectionProxy(
        IConfidentialDataService inner,
        UserContext userContext)
    {
        _inner = inner
            ?? throw new ArgumentNullException(nameof(inner));
        _userContext = userContext
            ?? throw new ArgumentNullException(
                nameof(userContext));
    }

    public string GetSalaryData(int employeeId)
    {
        EnsureRole("HR", "Admin");
        return _inner.GetSalaryData(employeeId);
    }

    public void UpdateSalary(
        int employeeId,
        decimal newSalary)
    {
        EnsureRole("Admin");
        _inner.UpdateSalary(employeeId, newSalary);
    }

    private void EnsureRole(params string[] requiredRoles)
    {
        bool hasRole = _userContext.Roles
            .Any(r => requiredRoles.Contains(r));

        if (!hasRole)
        {
            throw new UnauthorizedAccessException(
                $"User '{_userContext.Username}' lacks " +
                $"required role: " +
                $"{string.Join(" or ", requiredRoles)}");
        }
    }
}

Client code that resolves IConfidentialDataService from a DI container would get the protection proxy without knowing it:

var user = new UserContext
{
    Username = "jsmith",
    Roles = ["Employee"],
};

IConfidentialDataService service =
    new ProtectionProxy(
        new ConfidentialDataService(),
        user);

// Throws UnauthorizedAccessException
service.GetSalaryData(42);

This approach keeps authorization logic in one place and keeps the core service testable without any security concerns. If you've worked with the strategy design pattern in C#, you'll notice a similar separation of concerns -- the proxy pattern separates access control from business logic just as the strategy pattern separates algorithm selection from execution.

Caching Proxy: Wrapping a Service to Cache Results

A caching proxy applies the proxy design pattern to intercept calls to the real subject and return cached results when available. This avoids redundant work for operations that produce the same output given the same input -- expensive database queries, API calls, or complex calculations.

public interface IWeatherService
{
    WeatherData GetWeather(string city);
}

public sealed record WeatherData(
    string City,
    double TemperatureCelsius,
    string Conditions);

public class WeatherApiService : IWeatherService
{
    public WeatherData GetWeather(string city)
    {
        Console.WriteLine(
            $"Calling external weather API for {city}...");

        // Simulate API call
        Thread.Sleep(1500);

        return new WeatherData(
            city,
            Math.Round(Random.Shared.NextDouble() * 35, 1),
            "Partly Cloudy");
    }
}

public class CachingWeatherProxy : IWeatherService
{
    private readonly IWeatherService _inner;
    private readonly ConcurrentDictionary<string, WeatherData>
        _cache = new();

    public CachingWeatherProxy(IWeatherService inner)
    {
        _inner = inner
            ?? throw new ArgumentNullException(nameof(inner));
    }

    public WeatherData GetWeather(string city)
    {
        string key = city.ToUpperInvariant();

        if (_cache.TryGetValue(key, out var cached))
        {
            Console.WriteLine(
                $"Returning cached weather for {city}");
            return cached;
        }

        WeatherData fresh = _inner.GetWeather(city);
        _cache[key] = fresh;

        return fresh;
    }
}

Usage is transparent to the caller:

IWeatherService weatherService =
    new CachingWeatherProxy(
        new WeatherApiService());

// First call hits the API
var result1 = weatherService.GetWeather("Toronto");

// Second call returns cached result
var result2 = weatherService.GetWeather("Toronto");

Notice the use of ConcurrentDictionary for thread safety. In a web application, multiple requests might hit the caching proxy simultaneously, so thread-safe collections are essential.

Remote Proxy: Representing Distant Objects

A remote proxy represents an object that exists in a different address space -- a different process, a different machine, or behind an API boundary. This variation of the proxy design pattern handles all the network communication details so client code can work with a local interface as if the remote object were right there.

In modern .NET, the remote proxy concept shows up whenever you wrap an HTTP client behind a domain-specific interface:

public interface IProductCatalog
{
    Task<Product?> GetProductAsync(int productId);
}

public sealed record Product(
    int Id,
    string Name,
    decimal Price);

public class ProductCatalogProxy : IProductCatalog
{
    private readonly HttpClient _httpClient;

    public ProductCatalogProxy(HttpClient httpClient)
    {
        _httpClient = httpClient
            ?? throw new ArgumentNullException(
                nameof(httpClient));
    }

    public async Task<Product?> GetProductAsync(
        int productId)
    {
        var response = await _httpClient.GetAsync(
            $"/api/products/{productId}");

        if (!response.IsSuccessStatusCode)
        {
            return null;
        }

        return await response.Content
            .ReadFromJsonAsync<Product>();
    }
}

The consuming code works with IProductCatalog without knowing whether the data comes from a local database or a remote API. This is the same principle behind typed HTTP clients in ASP.NET Core. This approach also pairs well with the adapter design pattern in C# when the remote API's data model doesn't match your domain model.

Proxy Design Pattern with Dependency Injection in .NET

In production .NET applications, you'll want to register proxies through the IServiceCollection container. Applying the proxy design pattern as a decorator wraps the real service transparently, which aligns with inversion of control principles.

Manual Registration with Factory Delegates

You can register a proxy by wrapping the real service in a factory delegate:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddSingleton<WeatherApiService>();
services.AddSingleton<IWeatherService>(sp =>
{
    var inner = sp
        .GetRequiredService<WeatherApiService>();

    return new CachingWeatherProxy(inner);
});

var provider = services.BuildServiceProvider();
var weather = provider
    .GetRequiredService<IWeatherService>();

// Client code gets the caching proxy transparently
var data = weather.GetWeather("Vancouver");

The factory delegate resolves the real WeatherApiService first, then wraps it in a CachingWeatherProxy. Any code that depends on IWeatherService receives the proxy without knowing it. This keeps the proxy design pattern wiring centralized in your composition root.

Stacking Multiple Proxies

You can layer proxies just like decorators. For example, a caching proxy wrapped in a protection proxy:

var services = new ServiceCollection();

services.AddSingleton<WeatherApiService>();

services.AddSingleton<IWeatherService>(sp =>
{
    IWeatherService inner = sp
        .GetRequiredService<WeatherApiService>();

    inner = new CachingWeatherProxy(inner);

    // Additional proxy layers can be added here

    return inner;
});

This manual approach works well for straightforward proxy registrations. For more complex scenarios with many proxied services, libraries like Scrutor provide a cleaner Decorate<TInterface, TDecorator>() API that keeps registrations concise and declarative.

Proxy vs. Decorator vs. Adapter

Because the proxy design pattern, decorator, and adapter patterns all use composition and interface-based wrapping, developers frequently confuse them. The differences come down to intent:

  • Proxy controls access to the real object. It might defer creation, enforce permissions, cache results, or handle remote communication. The proxy and the real subject have the same interface, and the proxy decides when and whether to delegate.

  • Decorator adds new behavior to an existing object. It always delegates to the wrapped component and layers additional responsibilities on top. You can read more about this distinction in the decorator design pattern in C# guide.

While proxy and decorator both wrap a single object behind the same interface, the adapter pattern solves a fundamentally different problem in your codebase.

  • Adapter changes the interface of an existing object so it can work with code that expects a different interface. The adapter design pattern in C# guide covers this in detail.

A useful rule of thumb: if you're controlling when or whether delegation happens, you're building a proxy. If you're adding behavior around delegation, you're building a decorator. If you're translating between interfaces, you're building an adapter. In practice, the line between proxy and decorator can blur -- the intent you're expressing in your design is what matters most.

The facade design pattern in C# is another structural pattern worth contrasting. While a proxy provides an identical interface to a single object, a facade provides a simplified interface to an entire subsystem. They solve fundamentally different problems.

When to Use the Proxy Design Pattern

The proxy design pattern shines in specific scenarios. Use it when:

  • Object creation is expensive and you want to defer it until the client actually needs the object. Virtual proxies are the go-to solution for lazy loading.

  • Access control is required and you want to centralize authorization checks rather than scattering them throughout business logic. Protection proxies keep your services focused on their domain.

  • Caching would improve performance for read-heavy operations. A caching proxy transparently eliminates redundant computations or network calls.

Beyond these core use cases, the proxy design pattern also fits well in infrastructure-level concerns where you want to add cross-cutting behavior without modifying existing service implementations.

  • Remote communication needs abstraction. Remote proxies hide network details behind a clean domain interface, making the rest of your application agnostic to where data lives.

  • You need logging, metrics, or monitoring around service calls without modifying the service itself.

Avoid the proxy design pattern when the added indirection isn't justified. If your object is cheap to create and has no access control requirements, a proxy adds unnecessary complexity.

Frequently Asked Questions

What is the proxy design pattern in C#?

The proxy design pattern is a structural pattern that provides a substitute or placeholder for another object. In C#, you implement it by creating a proxy class that shares the same interface as the real subject. The proxy holds a reference to the real object (or the information needed to create it) and controls access -- whether that means deferring creation, checking permissions, caching results, or handling remote communication. Client code interacts with the proxy through the shared interface without knowing it isn't the real object.

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

Both patterns use composition and shared interfaces, but the proxy design pattern differs from the decorator in intent. A proxy controls access to the wrapped object -- it decides when or whether to delegate. A decorator always delegates and adds behavior around the delegation. In practice, some implementations blur this line, but the primary motivation determines which pattern label fits best.

When should I use a virtual proxy for lazy loading?

Use a virtual proxy when object creation is expensive -- loading large files, establishing database connections, or initializing complex object graphs -- and you can't guarantee the client will use the object. If every instance gets used immediately, the lazy loading overhead adds complexity without benefit. The proxy design pattern for lazy loading is most valuable when you create many objects upfront but only access a subset during runtime.

How does the proxy design pattern work with dependency injection?

You register the proxy as the implementation for the shared interface in your DI container. The proxy's constructor takes the real subject as a dependency. The container resolves the proxy when client code requests the interface, and the proxy handles delegation internally. This approach keeps your service consumers completely unaware of the proxy layer. You can use factory delegates with IServiceCollection or libraries like Scrutor that provide dedicated decorator/proxy registration methods.

Can I combine multiple proxy design pattern types on the same object?

Yes. Because proxies implement the same interface as the real subject, you can stack them just like decorators. For example, you might wrap a WeatherApiService with a CachingWeatherProxy, then wrap that with a ProtectionProxy. Each proxy in the chain handles its own concern and delegates to the next layer. Place access control proxies outermost to reject unauthorized requests before they reach the cache.

What is the difference between a proxy and an adapter pattern?

The proxy design pattern preserves the interface of the real subject -- the proxy and the real object implement the same contract. The adapter pattern changes the interface, translating between one interface and another so that incompatible classes can work together. Use a proxy when you want to control access to an existing object without changing its interface. Use an adapter when you need to make an existing class work with code that expects a different interface.

Is the C# Lazy class a proxy?

Lazy<T> from the .NET base class library implements the virtual proxy concept for lazy initialization. It defers creation of the wrapped object until the Value property is first accessed, then caches the result. While it doesn't implement the same interface as the wrapped object (it wraps via a generic container rather than interface sharing), it embodies the same lazy-loading principle that virtual proxies use. For cases where you need interface-level transparency, a custom virtual proxy is the better choice.

Wrapping Up the Proxy Design Pattern in C#

The proxy design pattern in C# is a versatile structural pattern that gives you fine-grained control over object access. Whether you're deferring expensive initialization with virtual proxies, centralizing access control with protection proxies, eliminating redundant work with caching proxies, or abstracting remote communication, the pattern provides a clean separation between access control logic and core business behavior.

The pattern integrates naturally with .NET's dependency injection system. Register your proxy as the interface implementation, let the container handle the wiring, and your consuming code stays unaware of the proxy layer.

Start by identifying places in your codebase where you're creating expensive objects that might not be used, duplicating authorization checks, or making redundant calls to external systems. Keep each proxy focused on a single concern, use thread-safe collections when state is involved, and leverage the DI container to wire everything up cleanly.

Weekly Recap: GitHub Copilot SDK, C# Source Generators, and Design Patterns [Mar 2026]

This week covers building AI developer tools with the GitHub Copilot SDK in C# -- from CLI tools and coding agents to ASP.NET Core AI APIs -- plus deep dives into C# source generators with incremental pipelines and Roslyn syntax trees, and practical guides on observer, singleton, decorator, and factory method design patterns.

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.

Facade Design Pattern in C#: Complete Guide with Examples

Master the facade design pattern in C# with practical examples showing simplified interfaces, subsystem encapsulation, and clean API design.

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