Understanding the Proxy Pattern in C

The Proxy pattern is a structural design pattern that provides a surrogate or placeholder object to control access to another target object. This article breaks down the Proxy pattern, explains the mechanisms it uses to manage and restrict access to real objects, and provides a clear, practical implementation in C#.

What is the Proxy Pattern?

The Proxy pattern creates an intermediary object that mirrors the interface of a target object, known as the Real Subject. By intercepting calls made by the client before they reach the Real Subject, the proxy can perform actions such as access validation, lazy initialization, caching, logging, or remote communication without modifying the underlying object's code.

The pattern consists of three primary components:

  • Subject Interface: Defines the common contract shared by both the Real Subject and the Proxy.
  • Real Subject: The concrete class that contains the core business logic and performs the actual work.
  • Proxy: The wrapper class implementing the same interface, holding a reference to the Real Subject to delegate requests while enforcing access rules.

How the Proxy Pattern Controls Access

A proxy controls access to the Real Subject primarily through three mechanisms:

  1. Protection (Access Control): Checks permissions or authentication criteria before allowing the client to execute methods on the Real Subject.
  2. Lazy Loading (Virtual Proxy): Delays the creation and resource-heavy initialization of the Real Subject until it is strictly needed by a client call.
  3. Caching and Throttling: Intercepts repeated requests to serve cached results or limits execution frequency, preventing unnecessary load on the Real Subject.

Implementing the Proxy Pattern in C

The following example demonstrates a Protection Proxy that controls access to a shared corporate database based on user roles.

1. The Subject Interface

public interface IDatabaseService
{
    void ExecuteQuery(string query);
}

2. The Real Subject

The RealDatabaseService performs the actual expensive operations and executes database queries.

public class RealDatabaseService : IDatabaseService
{
    public RealDatabaseService()
    {
        // Simulating heavy initialization, such as opening connections
        Console.WriteLine("Connecting to the database server...");
    }

    public void ExecuteQuery(string query)
    {
        Console.WriteLine($"Executing query: {query}");
    }
}

3. The Proxy

The DatabaseProxy enforces access restrictions. It ensures that only users with the Admin role can execute write or drop queries, while simultaneously applying lazy loading to the RealDatabaseService.

public class DatabaseProxy : IDatabaseService
{
    private RealDatabaseService? _realService;
    private readonly string _userRole;

    public DatabaseProxy(string userRole)
    {
        _userRole = userRole;
    }

    public void ExecuteQuery(string query)
    {
        if (query.StartsWith("DROP", StringComparison.OrdinalIgnoreCase) && _userRole != "Admin")
        {
            Console.WriteLine($"Access Denied: Role '{_userRole}' is not allowed to drop tables.");
            return;
        }

        // Lazy initialization: Instantiate only when authorized and needed
        _realService ??= new RealDatabaseService();

        _realService.ExecuteQuery(query);
    }
}

4. Client Usage

class Program
{
    static void Main()
    {
        // A standard user attempting a destructive query
        IDatabaseService userProxy = new DatabaseProxy("User");
        userProxy.ExecuteQuery("DROP TABLE Users;");

        Console.WriteLine();

        // An administrator executing the same query
        IDatabaseService adminProxy = new DatabaseProxy("Admin");
        adminProxy.ExecuteQuery("DROP TABLE Users;");
    }
}

Output:

Access Denied: Role 'User' is not allowed to drop tables.

Connecting to the database server...
Executing query: DROP TABLE Users;

When to Use the Proxy Pattern

The Proxy pattern is most effective when:

  • Direct access is unsafe or inefficient: You need to protect sensitive operations behind permission checks.
  • Resource allocation must be optimized: An object consumes significant memory or network bandwidth, making on-demand instantiation (lazy loading) essential.
  • Cross-cutting concerns must be decoupled: You want to add auditing, logging, or caching without altering the core business logic of the service.