Showing posts with label Proxy Design Pattern. Show all posts
Showing posts with label Proxy Design Pattern. Show all posts

Monday, July 27, 2026

Proxy Design Pattern


 


Mastering Design Patterns in C# and ASP.NET Core

Part 3.7 – Proxy Design Pattern

Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Structural Design Pattern
Difficulty: ⭐⭐⭐⭐☆ (Intermediate to Advanced)
Prerequisites: OOP, Interfaces, Dependency Injection, SOLID Principles, ASP.NET Core Web API


Table of Contents

  1. Introduction

  2. What is the Proxy Design Pattern?

  3. Why Do We Need a Proxy?

  4. How the Proxy Pattern Works

  5. Types of Proxy

    • Virtual Proxy

    • Protection Proxy

    • Remote Proxy

    • Smart Proxy

  6. UML Class Diagram

  7. Components of the Proxy Pattern

  8. Complete C# Console Application

  9. ASP.NET Core Implementation

  10. Lazy Loading

  11. Caching

  12. Authorization and Access Control

  13. Logging with Proxy

  14. Real-World Examples

  15. Advantages

  16. Disadvantages

  17. Best Practices

  18. Common Mistakes

  19. Proxy vs Decorator vs Adapter

  20. Interview Questions

  21. Summary

  22. Structural Design Patterns Complete


1. Introduction

In enterprise applications, we often don't want a client to communicate directly with an actual object.

There may be several reasons:

  • The object is expensive to create.

  • Access needs to be restricted.

  • Data should be cached.

  • Requests need to be logged.

  • Authorization must be checked.

  • The actual object is located on another server.

  • Additional processing is required before or after the operation.

For example, imagine an application that retrieves a very large report.

Creating the report object might involve:

Database Query
      ↓
Large Data Processing
      ↓
Report Generation
      ↓
Memory Allocation

But what if the user never actually requests the report?

Creating it immediately wastes resources.

A Proxy can delay creation until the object is actually needed.

Client
   ↓
Proxy
   ↓
Create Real Object only when required
   ↓
Real Object

The Proxy Design Pattern provides a controlled intermediary between the client and the real object.


2. What is the Proxy Design Pattern?

Definition

The Proxy Design Pattern is a Structural Design Pattern that provides a substitute or placeholder for another object to control access to it.

The Proxy generally implements the same interface as the real object.

Client
   ↓
Proxy
   ↓
Real Object

The client doesn't need to know whether it is communicating with the real object or the proxy.

Simple Definition

A Proxy is an intermediary that controls access to another object.


3. Why Do We Need a Proxy?

A Proxy can be used for several purposes.

1. Lazy Loading

Create an expensive object only when required.

2. Caching

Return previously retrieved data instead of repeatedly calling the actual service.

3. Authorization

Check whether the user is allowed to access a resource.

4. Logging

Record requests before forwarding them to the real object.

5. Remote Communication

Represent an object that exists on another server or service.

6. Validation

Validate requests before allowing them to reach the actual object.

7. Resource Management

Control access to expensive resources.


4. How the Proxy Pattern Works

Suppose we have:

public interface IReportService
{
    void GenerateReport();
}

The actual implementation:

public class ReportService : IReportService
{
    public void GenerateReport()
    {
        Console.WriteLine("Generating report...");
    }
}

The Proxy:

public class ReportProxy : IReportService
{
    private readonly ReportService _realService;

    public void GenerateReport()
    {
        // Additional logic

        _realService.GenerateReport();
    }
}

The client uses:

IReportService service = new ReportProxy();

service.GenerateReport();

The client doesn't need to know whether it is calling:

ReportProxy

or:

ReportService

because both implement:

IReportService

5. Types of Proxy

There are several common types of Proxy.


5.1 Virtual Proxy

A Virtual Proxy delays the creation of an expensive object until it is actually required.

Example

Imagine loading a large image.

Without Proxy:

Application Starts
      ↓
Load Large Image
      ↓
Consume Memory

With Virtual Proxy:

Application Starts
      ↓
Create Proxy
      ↓
User requests image
      ↓
Load actual image

This is called lazy loading.


5.2 Protection Proxy

A Protection Proxy controls access to the real object.

For example:

User
 ↓
Protection Proxy
 ↓
Is user authorized?
 ├── Yes → Real Service
 └── No  → Access Denied

This is particularly useful for:

  • Authorization

  • Role-based access

  • Permission checks

  • Resource protection


5.3 Remote Proxy

A Remote Proxy represents an object located somewhere else.

For example:

Client Application
       ↓
Remote Proxy
       ↓
HTTP/gRPC
       ↓
Remote Server
       ↓
Actual Service

The client interacts with the proxy as if it were interacting with a local object.

Examples include:

  • Remote APIs

  • gRPC services

  • Microservices

  • Distributed systems


5.4 Smart Proxy

A Smart Proxy performs additional operations whenever an object is accessed.

Examples:

  • Logging

  • Metrics

  • Caching

  • Reference counting

  • Validation

  • Auditing

Example:

Client
  ↓
Smart Proxy
  ↓
Logging
  ↓
Authorization
  ↓
Caching
  ↓
Real Service

6. UML Class Diagram

                    +------------------+
                    |     <<interface>>|
                    |      Subject     |
                    +------------------+
                    | + Request()      |
                    +--------^---------+
                             |
                +------------+------------+
                |                         |
        +---------------+         +---------------+
        |  RealSubject  |         |     Proxy     |
        +---------------+         +---------------+
        | + Request()   |         | - realSubject |
        +---------------+         | + Request()   |
                                  +-------+-------+
                                          |
                                          |
                                          v
                                  +---------------+
                                  |  RealSubject  |
                                  +---------------+

                        Client
                           |
                           v
                         Proxy

7. Components of the Proxy Pattern

The pattern generally consists of four participants.

1. Subject

Defines the common interface.

public interface IImage
{
    void Display();
}

2. Real Subject

The actual object.

public class RealImage : IImage
{
    public void Display()
    {
        Console.WriteLine("Displaying image");
    }
}

3. Proxy

Controls access to the Real Subject.

public class ImageProxy : IImage
{
    private RealImage? _realImage;

    public void Display()
    {
        if (_realImage == null)
        {
            _realImage = new RealImage();
        }

        _realImage.Display();
    }
}

4. Client

Uses the interface.

IImage image = new ImageProxy();

image.Display();

8. Complete C# Console Application

Let's create a complete Virtual Proxy example.

Step 1 – Interface

public interface IImage
{
    void Display();
}

Step 2 – Real Subject

public class RealImage : IImage
{
    private readonly string _fileName;

    public RealImage(string fileName)
    {
        _fileName = fileName;

        LoadFromDisk();
    }

    private void LoadFromDisk()
    {
        Console.WriteLine($"Loading {_fileName} from disk...");
    }

    public void Display()
    {
        Console.WriteLine($"Displaying {_fileName}");
    }
}

The important point is that creating RealImage immediately loads the image.


Step 3 – Proxy

public class ImageProxy : IImage
{
    private readonly string _fileName;

    private RealImage? _realImage;

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

    public void Display()
    {
        if (_realImage == null)
        {
            _realImage = new RealImage(_fileName);
        }

        _realImage.Display();
    }
}

Step 4 – Client

class Program
{
    static void Main()
    {
        IImage image = new ImageProxy("large-image.jpg");

        Console.WriteLine("Proxy created.");

        image.Display();

        Console.WriteLine();

        image.Display();
    }
}

Output

Proxy created.

Loading large-image.jpg from disk...
Displaying large-image.jpg

Displaying large-image.jpg

Notice something important:

Loading large-image.jpg from disk...

appears only once.

The second call reuses the existing object.

That's the benefit of the Virtual Proxy.


9. ASP.NET Core Implementation

Now let's implement a practical Proxy Pattern using ASP.NET Core.

Suppose we have:

Controller
    ↓
Product Service Proxy
    ↓
Real Product Service
    ↓
Database

The Proxy will:

  1. Check cache.

  2. If data exists, return cached data.

  3. Otherwise call the real service.

  4. Store the result in cache.

  5. Return the result.


Step 1 – Interface

public interface IProductService
{
    Task<Product?> GetProductAsync(int id);
}

Step 2 – Model

public class Product
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public decimal Price { get; set; }
}

Step 3 – Real Service

public class ProductService : IProductService
{
    public async Task<Product?> GetProductAsync(int id)
    {
        Console.WriteLine("Fetching product from database...");

        await Task.Delay(500);

        return new Product
        {
            Id = id,
            Name = "Laptop",
            Price = 999.99m
        };
    }
}

Step 4 – Proxy

public class CachedProductService : IProductService
{
    private readonly IProductService _realService;

    private readonly IMemoryCache _cache;

    public CachedProductService(
        IProductService realService,
        IMemoryCache cache)
    {
        _realService = realService;
        _cache = cache;
    }

    public async Task<Product?> GetProductAsync(int id)
    {
        string cacheKey = $"product_{id}";

        if (_cache.TryGetValue(cacheKey, out Product? product))
        {
            Console.WriteLine("Returning product from cache.");

            return product;
        }

        Console.WriteLine("Product not found in cache.");

        product = await _realService.GetProductAsync(id);

        if (product != null)
        {
            _cache.Set(
                cacheKey,
                product,
                TimeSpan.FromMinutes(10));
        }

        return product;
    }
}

10. Dependency Injection

Register the real service and Proxy.

builder.Services.AddMemoryCache();

builder.Services.AddScoped<ProductService>();

builder.Services.AddScoped<IProductService>(provider =>
{
    var realService =
        provider.GetRequiredService<ProductService>();

    var cache =
        provider.GetRequiredService<IMemoryCache>();

    return new CachedProductService(
        realService,
        cache);
});

Now when the controller requests:

IProductService

ASP.NET Core returns:

CachedProductService

instead of directly returning:

ProductService

The Proxy controls access to the actual service.


11. Controller

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(
        IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        var product =
            await _productService.GetProductAsync(id);

        if (product == null)
        {
            return NotFound();
        }

        return Ok(product);
    }
}

The controller doesn't know that caching is happening.

That's an important benefit.

Controller
    ↓
IProductService
    ↓
CachedProductService (Proxy)
    ↓
ProductService (Real Subject)

12. Lazy Loading

Lazy loading means:

Don't create or load something until it is actually needed.

Example:

public class ReportProxy : IReportService
{
    private ReportService? _realService;

    public void Generate()
    {
        _realService ??= new ReportService();

        _realService.Generate();
    }
}

The actual service is created only when:

Generate();

is called.

Benefits

  • Lower startup cost

  • Lower initial memory consumption

  • Faster application initialization

  • Avoids unnecessary work


13. Caching

Caching Proxy is extremely useful in Web APIs.

Request 1
   ↓
Proxy
   ↓
Cache Miss
   ↓
Database
   ↓
Store Result
   ↓
Client


Request 2
   ↓
Proxy
   ↓
Cache Hit
   ↓
Client

This can reduce:

  • Database calls

  • API calls

  • Network traffic

  • CPU usage

  • Response time


14. Authorization and Access Control

A Protection Proxy can check permissions before allowing access.

Example:

public interface IAdminService
{
    void DeleteUser(int userId);
}

Real service:

public class AdminService : IAdminService
{
    public void DeleteUser(int userId)
    {
        Console.WriteLine($"User {userId} deleted.");
    }
}

Protection Proxy:

public class AdminServiceProxy : IAdminService
{
    private readonly IAdminService _realService;

    private readonly string _role;

    public AdminServiceProxy(
        IAdminService realService,
        string role)
    {
        _realService = realService;
        _role = role;
    }

    public void DeleteUser(int userId)
    {
        if (_role != "Admin")
        {
            throw new UnauthorizedAccessException(
                "Only administrators can delete users.");
        }

        _realService.DeleteUser(userId);
    }
}

Flow:

Client
   ↓
AdminServiceProxy
   ↓
Check Role
   ↓
Admin?
 ┌───────┴───────┐
 Yes             No
  ↓               ↓
Real Service    Access Denied

15. Logging with Proxy

A Smart Proxy can add logging.

public class LoggingProductService : IProductService
{
    private readonly IProductService _service;

    private readonly ILogger<LoggingProductService> _logger;

    public LoggingProductService(
        IProductService service,
        ILogger<LoggingProductService> logger)
    {
        _service = service;
        _logger = logger;
    }

    public async Task<Product?> GetProductAsync(int id)
    {
        _logger.LogInformation(
            "Getting product {ProductId}",
            id);

        var result =
            await _service.GetProductAsync(id);

        _logger.LogInformation(
            "Finished getting product {ProductId}",
            id);

        return result;
    }
}

This adds logging without modifying the original ProductService.


16. Real-World Examples

1. Lazy Loading

Large images, documents, reports, or expensive objects can be created only when needed.


2. Caching

Client
 ↓
Cache Proxy
 ↓
Cache Hit → Return
 ↓
Cache Miss
 ↓
Database/API

3. Authorization

User
 ↓
Authorization Proxy
 ↓
Permission Check
 ↓
Real Service

4. Remote Services

Web API
 ↓
Remote Proxy
 ↓
HTTP/gRPC
 ↓
Microservice

5. Logging

Client
 ↓
Logging Proxy
 ↓
Real Service

6. Database Access

A proxy can be placed around expensive database operations to provide:

  • Caching

  • Logging

  • Retry

  • Authorization

  • Metrics


17. Advantages

1. Controls Access

The Proxy can determine whether an operation should be allowed.

2. Supports Lazy Loading

Expensive resources can be created only when required.

3. Enables Caching

Repeated operations can return cached results.

4. Adds Logging

Requests can be logged without modifying the Real Subject.

5. Improves Performance

Caching and lazy loading can reduce unnecessary work.

6. Supports Separation of Concerns

Cross-cutting functionality can be separated from business logic.

7. Supports Remote Communication

A proxy can hide the complexity of communicating with remote services.


18. Disadvantages

Additional Complexity

The architecture now contains another abstraction.

Performance Overhead

Every request passes through an additional layer.

Debugging Can Become Difficult

Multiple proxies may create long call chains.

Controller
 ↓
Logging Proxy
 ↓
Caching Proxy
 ↓
Authorization Proxy
 ↓
Retry Proxy
 ↓
Real Service

Incorrect Caching Can Cause Problems

Stale data or inappropriate cache policies can cause incorrect behavior.

Proxy Can Become Too Powerful

A badly designed Proxy may accumulate too many responsibilities.


19. Best Practices

1. Keep the Proxy Focused

A Proxy should have a clear purpose.

For example:

Caching Proxy

should primarily handle caching.

Don't combine:

Caching
Logging
Authorization
Payment
Database Logic

into one huge Proxy.


2. Program Against Interfaces

Use:

IProductService

rather than:

ProductService

This makes the Proxy interchangeable with the Real Subject.


3. Keep Real Services Focused

The Real Subject should focus on its primary business responsibility.


4. Be Careful with Caching

Define:

  • Expiration

  • Invalidation

  • Cache key

  • Maximum size

  • Failure behavior


5. Consider Thread Safety

ASP.NET Core applications process concurrent requests.

Shared state inside proxies must be thread-safe.


6. Don't Use a Proxy for Everything

If there is no meaningful access-control, lazy-loading, caching, remote-access, or cross-cutting requirement, a Proxy may be unnecessary.


20. Common Mistakes

Mistake 1 – Proxy Does Too Much

Avoid creating a giant proxy.


Mistake 2 – Proxy Changes the Contract

The Proxy should generally honor the same interface and expected behavior as the Real Subject.


Mistake 3 – Poor Cache Invalidation

Caching incorrect or outdated information can cause serious application bugs.


Mistake 4 – Exposing the Real Subject

If clients can directly access the Real Subject, they may bypass the Proxy's protections.


Mistake 5 – Ignoring Concurrency

A proxy with shared state must account for concurrent requests.


21. Proxy vs Decorator vs Adapter

These three patterns can look very similar because all three can wrap another object.

However, their intent is different.

FeatureProxyDecoratorAdapter
Primary PurposeControl accessAdd behaviorConvert interface
Main GoalAccess controlExtend functionalityCompatibility
InterfaceUsually sameUsually sameDifferent
Lazy LoadingSometimes
CachingSometimes
AuthorizationSometimes
Converts Interface
Remote Object
ExampleCached ServiceLogging ServiceLegacy API Adapter

Easy Way to Remember

Proxy:

"Can I access this object?"

Decorator:

"How can I add more behavior?"

Adapter:

"How can I make these incompatible interfaces work together?"


22. Interview Questions

1. What is the Proxy Design Pattern?

The Proxy Pattern provides a substitute object that controls access to another object.


2. Why does Proxy implement the same interface?

So the client can interact with the Proxy and Real Subject interchangeably.


3. What is a Virtual Proxy?

A Virtual Proxy delays the creation of an expensive object until it is actually needed.


4. What is a Protection Proxy?

A Protection Proxy controls access to the Real Subject based on permissions or authorization.


5. What is a Remote Proxy?

A Remote Proxy represents an object located in another process, server, or service.


6. What is a Smart Proxy?

A Smart Proxy adds additional behavior such as:

  • Logging

  • Caching

  • Validation

  • Metrics

  • Resource management


7. What is the difference between Proxy and Decorator?

The main difference is intent.

Proxy primarily controls access.

Decorator primarily adds responsibilities or behavior.


8. What is the difference between Proxy and Adapter?

Proxy generally maintains the same interface.

Adapter converts one interface into another.


9. Can Proxy be used with Dependency Injection?

Absolutely.

In ASP.NET Core, Dependency Injection makes it easy to substitute a Proxy implementation for the Real Subject.


10. Can Proxy be used for caching?

Yes.

A caching Proxy can check a cache before forwarding the request to the actual service.


11. Can Proxy implement authorization?

Yes.

A Protection Proxy can validate the user's role or permissions before calling the Real Subject.


12. What is lazy loading in Proxy Pattern?

Lazy loading means delaying expensive object creation until the client actually needs the object.


13. Where can Proxy be used in enterprise applications?

Common scenarios include:

  • API caching

  • Authorization

  • Lazy loading

  • Logging

  • Remote service communication

  • Database access

  • Monitoring

  • Auditing

  • Resource management


23. Summary

The Proxy Design Pattern provides an intermediary between a client and a real object.

The basic structure is:

Client
   ↓
Proxy
   ↓
Real Subject

The Proxy can provide additional capabilities such as:

┌──────────────────────┐
│      Proxy           │
├──────────────────────┤
│ Lazy Loading         │
│ Caching              │
│ Authorization        │
│ Logging              │
│ Validation           │
│ Remote Communication │
└──────────────────────┘

The most important types to remember are:

Proxy TypePurpose
Virtual ProxyLazy loading
Protection ProxyAuthorization/access control
Remote ProxyAccess remote objects
Smart ProxyAdditional processing such as caching/logging

For ASP.NET Core, the Proxy Pattern is particularly useful for service decorators that implement caching, authorization, logging, and other cross-cutting concerns.

The key idea is:

The client communicates with the Proxy, while the Proxy controls and manages access to the Real Subject.

When used appropriately, the Proxy Pattern can improve performance, security, maintainability, and flexibility without requiring changes to the underlying business service.


🎉 Structural Design Patterns – Complete

With Part 3.7, we have now completed the major Structural Design Patterns in our series:

PartPatternMain Purpose
3.1AdapterMake incompatible interfaces work together
3.2BridgeSeparate abstraction from implementation
3.3CompositeBuild part-whole tree structures
3.4DecoratorAdd behavior dynamically
3.5FacadeSimplify complex subsystems
3.6FlyweightShare objects and reduce memory usage
3.7ProxyControl access to objects

🚀 Coming Up Next: Part 4 – Behavioral Design Patterns

The next major section of the Design Patterns in C# and ASP.NET Core series will cover Behavioral Design Patterns.

These patterns focus on how objects communicate, collaborate, and distribute responsibilities.

We will explore:

Part 4.1 – Chain of Responsibility

Part 4.2 – Command

Part 4.3 – Interpreter

Part 4.4 – Iterator

Part 4.5 – Mediator

Part 4.6 – Memento

Part 4.7 – Observer

Part 4.8 – State

Part 4.9 – Strategy

Part 4.10 – Template Method

Part 4.11 – Visitor

For each pattern, we'll continue the same practical approach:

Definition → Problem → UML → C# Console Application → ASP.NET Core → Real-World Example → Advantages → Disadvantages → Best Practices → Common Mistakes → Interview Questions.

Don't Copy

Protected by Copyscape Online Plagiarism Checker