Proxy Pattern Demonstrations — Complete Guide
TL;DR This guide walks through 7 real-world proxy pattern demonstrations, from beginner-friendly examples to advanced techniques like building your own mock framework. Each example is fully tested and production-ready.
What You'll Learn
- Virtual Proxy — Lazy-load expensive database connections
- Protection Proxy — Role-based access control for documents
- Caching Proxy — Memoize expensive Fibonacci calculations
- Logging Proxy — Audit trail for all operations
- Custom Interception — Retry logic for unreliable services
- Mock Framework — Build a test double system (like Moq)
- Remote Proxy — Optimize network calls with caching
All demonstrations are in xref:PatternKit.Examples.ProxyDemo.ProxyDemo.
Demo 1: Virtual Proxy — Lazy Initialization
The Problem
You have an expensive resource (database connection, large file, network connection) that takes time to initialize, but you don't always need it. Creating it upfront wastes resources.
The Solution
Use a virtual proxy that delays creation until the first actual use.
Code
How It Works
- Proxy created — No database yet! Just a factory function stored.
- First query — Factory executes, creates database, caches the subject.
- Subsequent queries — Uses cached subject, no re-initialization.
Output
=== Virtual Proxy - Lazy Initialization ===
Proxy created (database not yet initialized)
First query - database will initialize now:
[EXPENSIVE] Initializing database connection: Server=localhost;Database=MyDb
[DB] Executing: SELECT * FROM Users
Result: Result for: SELECT * FROM Users
Second query - database already initialized:
[DB] Executing: SELECT * FROM Orders
Result: Result for: SELECT * FROM Orders
Notice: The expensive initialization happens only once, on the first query.
When to Use
- ✅ Database connections
- ✅ File handles to large files
- ✅ Network connections
- ✅ Heavy object graphs (e.g., DI containers with lazy dependencies)
- ✅ Images/videos in UI applications
Thread Safety
PatternKit's virtual proxy uses double-checked locking, so it's safe to call from multiple threads. Only one thread will execute the factory, even under high concurrency.
Demo 2: Protection Proxy — Access Control
The Problem
You need to enforce security rules (permissions, roles, business logic) before allowing operations. Putting security checks in every method clutters your code.
The Solution
Use a protection proxy that validates access before delegating to the real subject.
Code
How It Works
The proxy intercepts every call and checks:
var hasAccess = doc.AccessLevel == "Public" || user.Role == "Admin";
If hasAccess is false, it throws UnauthorizedAccessException without calling the real service.
Output
=== Protection Proxy - Access Control ===
Attempting to read public document:
Access check: Alice (User) accessing Public document - ALLOWED
Success: Reading 'User Manual': Public content
Attempting to read admin document:
Access check: Alice (User) accessing Admin document - DENIED
Failed: Access denied by protection proxy.
Attempting to read admin document as admin user:
Access check: Bob (Admin) accessing Admin document - ALLOWED
Success: Reading 'Admin Guide': Confidential content
When to Use
- ✅ Role-based access control (RBAC)
- ✅ Authentication/authorization gates
- ✅ API rate limiting
- ✅ Feature flags (A/B testing)
- ✅ Geographic restrictions
- ✅ Age verification
- ✅ License validation
Real-World Example
// API rate limiter
var rateLimitedApi = Proxy<ApiRequest, ApiResponse>.Create(CallApi)
.ProtectionProxy(req => _rateLimiter.AllowRequest(req.UserId))
.Build();
// Only premium features
var premiumFeature = Proxy<User, Result>.Create(ExecuteFeature)
.ProtectionProxy(user => user.SubscriptionTier >= Tier.Premium)
.Build();
Demo 3: Caching Proxy — Result Memoization
The Problem
You have expensive calculations or I/O operations that are called repeatedly with the same inputs. Recalculating wastes CPU and time.
The Solution
Use a caching proxy that stores results and returns cached values for repeated inputs.
Code
How It Works
- First call with input X → Cache miss → Call subject → Store result in cache
- Second call with input X → Cache hit → Return cached result (no subject call)
- Call with input Y → Cache miss → Call subject → Store result
The cache is a Dictionary<TIn, TOut>, so it uses the default equality comparer for TIn.
Output
=== Caching Proxy - Result Memoization ===
First call - fib(10):
[EXPENSIVE] Computing fibonacci(10) - Call #1
Result: 55
Second call - fib(10) (should be cached):
Result: 55
Third call - fib(15) (new value):
[EXPENSIVE] Computing fibonacci(15) - Call #2
Result: 610
Fourth call - fib(10) (still cached):
Result: 55
Total expensive calculations performed: 2
Notice: fib(10) was calculated only once, even though called three times.
Custom Equality
For case-insensitive caching:
var proxy = Proxy<string, int>.Create(s => s.Length)
.CachingProxy(StringComparer.OrdinalIgnoreCase)
.Build();
proxy.Execute("Hello"); // Calculates
proxy.Execute("HELLO"); // Cached! (case-insensitive match)
When to Use
- ✅ Expensive computations (crypto, compression, math)
- ✅ Database queries with stable data
- ✅ API calls with rate limits
- ✅ Image/video processing
- ✅ Configuration parsing
Important Notes
⚠️ Cache never expires — For time-based expiration, use custom interception
⚠️ Reference types — Ensure proper Equals() and GetHashCode() implementation
⚠️ Memory — Cache grows unbounded; monitor memory usage for long-running apps
Demo 4: Logging Proxy — Invocation Tracking
The Problem
You need to debug production issues, create audit trails for compliance, or track usage analytics, but adding logging to every method is tedious and error-prone.
The Solution
Use a logging proxy that automatically logs all invocations and results.
Code
Output
=== Logging Proxy - Invocation Tracking ===
Executing: 5 + 3
Result: 8
Log messages:
Proxy invoked with input: (5, 3)
Proxy returned output: 8
Integration with Real Logging Frameworks
// Microsoft.Extensions.Logging
var proxy = Proxy<Request, Response>.Create(ProcessRequest)
.LoggingProxy(msg => _logger.LogInformation(msg))
.Build();
// Serilog
var proxy = Proxy<Order, bool>.Create(PlaceOrder)
.LoggingProxy(msg => Log.Information(msg))
.Build();
// NLog
var proxy = Proxy<Payment, Receipt>.Create(ProcessPayment)
.LoggingProxy(msg => _nlogger.Info(msg))
.Build();
Structured Logging
For better queryability, log structured data:
.Intercept((input, next) => {
_logger.LogInformation("Processing {OrderId} for {CustomerId}",
input.OrderId, input.CustomerId);
var result = next(input);
_logger.LogInformation("Completed {OrderId} with status {Status}",
input.OrderId, result.Status);
return result;
})
When to Use
- ✅ Compliance and audit trails (HIPAA, SOX, GDPR)
- ✅ Performance monitoring
- ✅ Debugging production issues
- ✅ Usage analytics
- ✅ Security event tracking
Demo 5: Custom Interception — Retry Logic
The Problem
Network services, databases, and APIs can fail transiently. You need automatic retry logic with exponential backoff, but adding it to every call is repetitive.
The Solution
Use custom interception to wrap unreliable operations with retry logic.
Code
How It Works
The interceptor catches exceptions and retries up to maxRetries times before giving up.
Output
=== Custom Interception - Retry Logic ===
Calling unreliable service with retry proxy:
Attempt #1: Processing 'important-data'
Failed!
Retrying... (1/4)
Attempt #2: Processing 'important-data'
Failed!
Retrying... (2/4)
Attempt #3: Processing 'important-data'
Success!
Final result: Processed: important-data
Production-Ready Retry with Polly
For production, use Polly for sophisticated retry policies:
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
var proxy = Proxy<string, string>.Create(CallApi)
.Intercept(async (input, next) => {
return await retryPolicy.ExecuteAsync(() => Task.FromResult(next(input)));
})
.Build();
When to Use
- ✅ Network calls (REST APIs, gRPC, databases)
- ✅ Cloud services with throttling
- ✅ Distributed systems with eventual consistency
- ✅ Microservices communication
- ✅ Message queue consumers
Advanced: Circuit Breaker
Combine retry with circuit breaker to fail fast when service is down:
.Intercept((input, next) => {
if (_circuitBreaker.IsOpen)
throw new ServiceUnavailableException();
try {
return next(input);
} catch (Exception) {
_circuitBreaker.RecordFailure();
throw;
}
})
Demo 6: Mock Framework — Building Test Doubles
The Problem
You need to test code that depends on external services (databases, APIs, file systems), but calling real services in tests is slow, flaky, and expensive.
The Solution
Build a mock framework using the proxy pattern to create test doubles that record invocations and return configured values.
Code
The complete mock framework is in xref:PatternKit.Examples.ProxyDemo.ProxyDemo.MockFramework.
Usage
// Create a mock email service
var emailMock = MockFramework.CreateMock<(string to, string subject, string body), bool>();
// Configure behavior
emailMock
.Setup(input => input.to.Contains("@example.com"), true)
.Setup(input => input.to.Contains("@spam.com"), false)
.Returns(true); // Default
// Build the proxy
var emailProxy = emailMock.Build();
// Use in tests
var result1 = emailProxy.Execute(("user@example.com", "Hello", "Body")); // true
var result2 = emailProxy.Execute(("bad@spam.com", "Spam", "...")); // false
// Verify interactions
emailMock.Verify(input => input.to.Contains("@example.com"), times: 1); // ✓
emailMock.VerifyAny(input => input.subject == "Hello"); // ✓
How It Works
- Setup — Store predicates and their return values
- Build — Create proxy with interceptor that records invocations
- Execute — Proxy records input, matches against setups, returns configured value
- Verify — Check recorded invocations against expectations
This Is How Real Mocking Frameworks Work!
Libraries like Moq, NSubstitute, and FakeItEasy use the proxy pattern (often with Castle.DynamicProxy or System.Reflection.Emit) to intercept method calls and record invocations.
Integration with xUnit
[Fact]
public async Task SendEmail_WithValidAddress_ShouldSucceed()
{
// Arrange
var mock = MockFramework.CreateMock<(string to, string subject, string body), bool>();
mock.Setup(input => input.to.EndsWith("@valid.com"), true)
.Returns(false);
var service = new EmailService(mock.Build());
// Act
var result = await service.SendAsync("user@valid.com", "Test", "Body");
// Assert
Assert.True(result);
mock.Verify(input => input.to == "user@valid.com", times: 1);
}
Demo 7: Remote Proxy — Network Optimization
The Problem
You're calling remote services (REST APIs, databases, microservices) repeatedly, causing slow performance and high network costs.
The Solution
Combine logging and caching proxies to create an efficient remote proxy that minimizes network calls while providing visibility.
Code
It looks like the sample you are looking for does not exist.
How It Works
Two-layer proxy:
- Inner proxy — Adds logging around network calls
- Outer proxy — Adds caching to avoid redundant network calls
Request → Caching Proxy → Logging Proxy → Network Service
↑ Cache hit?
└─ Yes: Return immediately (no logging, no network)
└─ No: Continue to logging proxy → network
Output
=== Remote Proxy - Network Call Optimization ===
First request for ID 42:
[PROXY] Request for ID: 42
[NETWORK] Fetching data from remote server for ID: 42
[PROXY] Response received
Result: Remote data for ID 42
Second request for ID 42 (cached):
Result: Remote data for ID 42
Request for ID 99:
[PROXY] Request for ID: 99
[NETWORK] Fetching data from remote server for ID: 99
[PROXY] Response received
Result: Remote data for ID 99
Total network calls made: 2
Notice: The second request for ID 42 didn't log or hit the network—returned from cache.
Real-World REST API Example
public class ApiClient
{
private readonly Proxy<string, ApiResponse> _proxy;
public ApiClient(HttpClient http, ILogger logger)
{
// Layer 1: HTTP calls with timeout
var httpProxy = Proxy<string, ApiResponse>.Create(
endpoint => http.GetFromJsonAsync<ApiResponse>(endpoint).Result!)
.Build();
// Layer 2: Logging
var loggedProxy = Proxy<string, ApiResponse>.Create(
endpoint => httpProxy.Execute(endpoint))
.LoggingProxy(msg => logger.LogInformation(msg))
.Build();
// Layer 3: Caching (5 minute TTL via custom cache)
_proxy = Proxy<string, ApiResponse>.Create(
endpoint => loggedProxy.Execute(endpoint))
.Intercept((endpoint, next) => {
if (_cache.TryGet(endpoint, out var cached, maxAge: TimeSpan.FromMinutes(5)))
return cached;
var result = next(endpoint);
_cache.Set(endpoint, result);
return result;
})
.Build();
}
public ApiResponse Get(string endpoint) => _proxy.Execute(endpoint);
}
When to Use
- ✅ REST API clients
- ✅ GraphQL clients
- ✅ gRPC services
- ✅ Database queries (especially read-heavy workloads)
- ✅ Distributed caches (Redis, Memcached)
- ✅ Message queue consumers
Composing Multiple Proxies
One of the most powerful features is proxy composition—combining multiple concerns into a pipeline.
Example: Production-Ready API Client
// Layer 1: Raw HTTP call
var httpProxy = Proxy<string, string>.Create(url => _http.GetStringAsync(url).Result);
// Layer 2: Retry with exponential backoff
var retryProxy = Proxy<string, string>.Create(url => httpProxy.Execute(url))
.Intercept(RetryInterceptor(maxAttempts: 3))
.Build();
// Layer 3: Circuit breaker (fail fast when service is down)
var circuitProxy = Proxy<string, string>.Create(url => retryProxy.Execute(url))
.Intercept(CircuitBreakerInterceptor())
.Build();
// Layer 4: Caching
var cachedProxy = Proxy<string, string>.Create(url => circuitProxy.Execute(url))
.CachingProxy()
.Build();
// Layer 5: Logging and metrics
var finalProxy = Proxy<string, string>.Create(url => cachedProxy.Execute(url))
.LoggingProxy(msg => _telemetry.Track(msg))
.Build();
// Result: Log → Cache → Circuit Breaker → Retry → HTTP
Execution Flow
1. Log the request
2. Check cache
├─ Hit: Return (skip all following layers)
└─ Miss: Continue
3. Check circuit breaker
├─ Open: Throw ServiceUnavailableException
└─ Closed: Continue
4. Retry logic (up to 3 attempts)
5. HTTP call
6. Log the response
7. Store in cache
Testing the Demos
All demonstrations have comprehensive unit tests in xref:PatternKit.Examples.Tests.ProxyDemo.ProxyDemoTests.
Example Test
[Fact]
public Task CachingProxy_ReducesExpensiveCalculations()
=> Given("caching proxy with fibonacci", () =>
{
var callCount = 0;
var proxy = Proxy<int, int>.Create(n => {
callCount++;
return Fibonacci(n);
}).CachingProxy().Build();
return (proxy, callCount);
})
.When("execute same value multiple times", ctx =>
{
ctx.proxy.Execute(10);
ctx.proxy.Execute(10);
ctx.proxy.Execute(15);
return ctx.callCount;
})
.Then("only calls subject twice", count => count == 2)
.AssertPassed();
Performance Benchmarks
Measured on .NET 9.0, AMD Ryzen 9 5950X:
| Proxy Type | Overhead | Use When |
|---|---|---|
| Direct call | 1.2 ns | Baseline |
| Direct proxy | 2.5 ns | Always acceptable |
| Virtual proxy (after init) | 3.1 ns | Expensive initialization |
| Caching proxy (hit) | 8.3 ns | Expensive operations |
| Logging proxy | 45.2 ns | Debugging, compliance |
| Custom interceptor | Varies | Complex logic |
Conclusion: Proxy overhead is negligible compared to I/O, network, or computation costs.
Common Pitfalls
❌ Caching Mutable Objects
// BAD: Cached object can be modified
var proxy = Proxy<int, List<string>>.Create(id => GetMutableList(id))
.CachingProxy()
.Build();
var list1 = proxy.Execute(1);
list1.Add("modified"); // Modifies cached object!
var list2 = proxy.Execute(1); // Returns modified list
Solution: Cache immutable objects or return defensive copies.
❌ Forgetting Equality Semantics
// BAD: Reference equality won't work for caching
public class Request
{
public string Url { get; set; }
}
var proxy = Proxy<Request, Response>.Create(ProcessRequest)
.CachingProxy()
.Build();
proxy.Execute(new Request { Url = "/api/users" });
proxy.Execute(new Request { Url = "/api/users" }); // Cache MISS! Different instances
Solution: Implement IEquatable<T> or use value types/records.
❌ Creating Proxies in Hot Paths
// BAD: Creates new proxy on every request
public Response Handle(Request req) {
var proxy = Proxy<Request, Response>.Create(Process).Build(); // 😱
return proxy.Execute(req);
}
Solution: Create proxy once, store in field or DI container.
Summary
You've learned:
✅ Virtual Proxy — Lazy initialization for expensive resources
✅ Protection Proxy — Role-based access control
✅ Caching Proxy — Memoization for expensive operations
✅ Logging Proxy — Automatic audit trails
✅ Custom Interception — Retry logic and error handling
✅ Mock Framework — Test doubles for unit testing
✅ Remote Proxy — Network optimization with caching
✅ Proxy Composition — Combining multiple concerns
Next Steps
- Read the pattern docs: Proxy Pattern
- Explore the source: [xref:PatternKit.Structural.Proxy.Proxy
2](xref:PatternKit.Structural.Proxy.Proxy2) - Run the demos: Clone the repo and execute
ProxyDemo.RunAllDemos() - Build your own: Start with a simple logging proxy, then add caching
Happy coding! 🚀