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
Introduction
What is the Proxy Design Pattern?
Why Do We Need a Proxy?
How the Proxy Pattern Works
Types of Proxy
Virtual Proxy
Protection Proxy
Remote Proxy
Smart Proxy
UML Class Diagram
Components of the Proxy Pattern
Complete C# Console Application
ASP.NET Core Implementation
Lazy Loading
Caching
Authorization and Access Control
Logging with Proxy
Real-World Examples
Advantages
Disadvantages
Best Practices
Common Mistakes
Proxy vs Decorator vs Adapter
Interview Questions
Summary
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:
Check cache.
If data exists, return cached data.
Otherwise call the real service.
Store the result in cache.
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.
| Feature | Proxy | Decorator | Adapter |
|---|---|---|---|
| Primary Purpose | Control access | Add behavior | Convert interface |
| Main Goal | Access control | Extend functionality | Compatibility |
| Interface | Usually same | Usually same | Different |
| Lazy Loading | ✅ | Sometimes | ❌ |
| Caching | ✅ | Sometimes | ❌ |
| Authorization | ✅ | Sometimes | ❌ |
| Converts Interface | ❌ | ❌ | ✅ |
| Remote Object | ✅ | ❌ | ❌ |
| Example | Cached Service | Logging Service | Legacy 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 Type | Purpose |
|---|---|
| Virtual Proxy | Lazy loading |
| Protection Proxy | Authorization/access control |
| Remote Proxy | Access remote objects |
| Smart Proxy | Additional 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:
| Part | Pattern | Main Purpose |
|---|---|---|
| 3.1 | Adapter | Make incompatible interfaces work together |
| 3.2 | Bridge | Separate abstraction from implementation |
| 3.3 | Composite | Build part-whole tree structures |
| 3.4 | Decorator | Add behavior dynamically |
| 3.5 | Facade | Simplify complex subsystems |
| 3.6 | Flyweight | Share objects and reduce memory usage |
| 3.7 | Proxy | Control 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.
