.NET & C# Development · Lesson 40 of 229
Proxy — Control Access to Another Object
Proxy — Control Access to Another Object
A Proxy wraps a real object and controls access to it, adding behaviour before or after forwarding to the real object. It implements the same interface as the real subject.
Types of Proxy
Virtual Proxy: delays expensive creation until first access (lazy initialisation)
Protection Proxy: controls access based on permissions
Caching Proxy: caches results to avoid repeated expensive calls
Remote Proxy: represents an object in another address space (gRPC client stubs)
Logging Proxy: records all interactions for debugging/auditingCaching Proxy (Most Common in Web APIs)
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IReadOnlyList<Product>> GetAllAsync();
}
// Real implementation — hits the database
public class ProductRepository(AppDbContext db) : IProductRepository
{
public Task<Product?> GetByIdAsync(int id)
=> db.Products.FindAsync(id).AsTask();
public Task<IReadOnlyList<Product>> GetAllAsync()
=> db.Products.ToListAsync()
.ContinueWith(t => (IReadOnlyList<Product>)t.Result);
}
// Caching proxy — transparent to callers
public class CachedProductRepository(
IProductRepository inner,
IMemoryCache cache) : IProductRepository
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
public async Task<Product?> GetByIdAsync(int id)
{
string key = $"product:{id}";
if (cache.TryGetValue(key, out Product? cached))
return cached;
var product = await inner.GetByIdAsync(id);
if (product is not null)
cache.Set(key, product, CacheDuration);
return product;
}
public async Task<IReadOnlyList<Product>> GetAllAsync()
{
const string key = "products:all";
if (cache.TryGetValue(key, out IReadOnlyList<Product>? cached))
return cached!;
var products = await inner.GetAllAsync();
cache.Set(key, products, CacheDuration);
return products;
}
}
// Register proxy in DI — callers get the proxy, not the real class
builder.Services.AddScoped<ProductRepository>();
builder.Services.AddScoped<IProductRepository>(sp =>
new CachedProductRepository(
sp.GetRequiredService<ProductRepository>(),
sp.GetRequiredService<IMemoryCache>()
));Protection Proxy
public interface IDocumentService
{
Task<Document> GetAsync(int documentId);
Task DeleteAsync(int documentId);
}
public class DocumentService(AppDbContext db) : IDocumentService
{
public Task<Document> GetAsync(int id) => db.Documents.FindAsync(id).AsTask()!;
public async Task DeleteAsync(int id)
{
var doc = await db.Documents.FindAsync(id);
if (doc is not null) { db.Documents.Remove(doc); await db.SaveChangesAsync(); }
}
}
// Protection proxy — enforces access control
public class SecureDocumentService(
IDocumentService inner,
ICurrentUserService currentUser) : IDocumentService
{
public async Task<Document> GetAsync(int documentId)
{
var doc = await inner.GetAsync(documentId);
if (doc.OwnerId != currentUser.UserId && !currentUser.IsAdmin)
throw new UnauthorizedAccessException("You do not have access to this document");
return doc;
}
public async Task DeleteAsync(int documentId)
{
if (!currentUser.IsAdmin)
throw new UnauthorizedAccessException("Only admins can delete documents");
await inner.DeleteAsync(documentId);
}
}Virtual Proxy (Lazy Loading)
public interface IExpensiveService
{
string GetReport();
}
// Real service — expensive to initialise (e.g., connects to a slow external system)
public class ExpensiveReportService : IExpensiveService
{
public ExpensiveReportService()
{
// Slow initialisation: loading config, warming up connections
Thread.Sleep(2000);
}
public string GetReport() => "Report data...";
}
// Virtual proxy — delays creation until GetReport() is first called
public class LazyReportServiceProxy : IExpensiveService
{
private readonly Lazy<IExpensiveService> _inner =
new(() => new ExpensiveReportService());
public string GetReport() => _inner.Value.GetReport();
}Proxy via Scrutor (Decorator Registration)
// Scrutor makes wrapping for DI much cleaner:
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.Decorate<IProductRepository, CachedProductRepository>();
// Now IProductRepository resolves to CachedProductRepository wrapping ProductRepositoryInterview Answer
"The Proxy pattern wraps a real object, implementing the same interface, to add behaviour without the caller knowing. Most common in .NET backends: the Caching Proxy wraps a repository and returns cached results, the Protection Proxy enforces authorisation, and the Remote Proxy represents a gRPC or HTTP service. Proxy is structurally identical to Decorator — the difference is intent: Proxy controls access (the real object may not be created yet, may require permission, or results may be cached); Decorator adds behaviour (logging, validation) without restricting access. In .NET, Scrutor's
Decoratemethod registers proxies/decorators cleanly without changing the DI consumer.Lazy<T>is the built-in virtual proxy for expensive initialisation."