GitHub repo: DesignPatternsInCSharp (ardalis)
Table of Contents
- Course Overview
- Introduction to Proxy Pattern
- Problem solved by Proxy Pattern
- Structure and design of the Proxy Pattern
- Proxy Pattern Variants
- Virtual Proxy — Virtual Proxy
- 6.1 Concept and objective
- 6.2 Implementation with inheritance (VirtualExpensiveToFullyLoad)
- 6.3 Implementation with
Lazy<T> - 6.4 Virtual Proxy unit tests
- 8.1 Concept and objective
- 8.2 Default interface and implementation
- 8.3 FileSmartProxy implementation
- 8.4 Smart Proxy Unit Tests
- 9.1 Concept and objective
- 9.2 Domain model (Document, User, Roles)
- 9.3 ProtectedDocument implementation
- 9.4 Protective Proxy unit tests
1. Course Overview
This course, presented by Steve Smith (aka Ardalis), an experienced .NET developer, architect, and trainer, covers the Proxy Design Pattern — one of the most useful and frequently used design patterns in software development.
Topics covered
- What problem is the Proxy Pattern designed to solve?
- What software design principles apply to this pattern?
- How to apply the Proxy Pattern in four specific variations in your applications:
- Virtual Proxy
- Remote Proxy
- Smart Proxy
- Protective Proxy
- What other design patterns are similar to the Proxy Pattern?
Educational objective
At the end of this course, you will be able to:
- Recognize situations where the Proxy Pattern is appropriate
- Apply this pattern with confidence in your own applications
- Distinguish the Proxy Pattern from related patterns (Decorator, Adapter, Prototype, Flyweight)
2. Introduction to Proxy Pattern
The Proxy Pattern is one of the most versatile and commonly used design patterns in a wide variety of contexts. Once mastered, you will be ready to use it appropriately in your applications.
Learning Plan
- What problems does the Proxy Pattern solve?
- How is it structured and how does it fit into your existing code base?
- How to apply the pattern in real code through several concrete examples
- How to recognize related patterns and underlying principles
3. Problem solved by the Proxy Pattern
Problem definition
You need to control access to a type for performance, security, or other reasons.
The purpose of the Proxy Design Pattern is to control access to another instance. This is the heart of the pattern: interposing between a client and a resource to manage this access.
Real World Analogy: The Beekeeper
Steve Smith uses his hobby — beekeeping — to illustrate the concept:
- Scenario: someone wants to buy honey.
- Direct access: go directly to the hive, full of bees protecting their honey. This is dangerous for the buyer and for the bees.
- Via a proxy: use a beekeeper (beekeeper), trained and equipped to access honey safely.
In this example, the beekeeper is the proxy (or surrogate) for the hive: he controls access and protects both parties.
Second analogy: The bank check
When you pay the beekeeper:
- You do not have cash → you write a check.
- The check is itself a proxy: it acts as an intermediary between your bank and the beekeeper’s bank.
- It controls access to a certain amount of funds in your account.
Money itself is a proxy for value — but that is beyond the scope of this course.
Common Software Examples
| Context | Description |
|---|---|
| User interface (UI) | Loading content on demand (infinite scroll): a proxy serves as a placeholder for content not yet retrieved from the server |
| Security / Authorization | Check a user’s access rights before allowing them to access a resource |
| Caching | Cache expensive call results to avoid repeating them |
| Lazy loading | Only load expensive data when it is actually needed |
| Network proxies | Centralize connection details to remote services |
4. Structure and design of the Proxy Pattern
General structure
The Proxy Pattern involves three main actors:
Client → Proxy → RealService
- Client: calls the resource via the proxy, without knowing that it is a proxy
- Proxy: implements the same interface as
RealService, interposes between the client and the real resource - RealService: the real, expensive, remote or sensitive resource
Design rules
-
The proxy implements the same interface (or base type) as the
RealService. If the client needs a different interface, use the Adapter pattern rather than the Proxy. -
Transparency for the customer. The client must be completely unaware of the presence of the proxy. It thinks it works directly with the
RealService. -
Pre/post call logic When the client makes a request to the proxy, it can:
- Execute logic before passing request to
RealService - Deciding to not call the
RealServiceat all (e.g.: access denied, cached result) - Execute logic after receiving response from
RealService(e.g.: caching, logging)
- Response still required.
Even if the proxy restricts access to the
RealService, it must still return a response to the client (e.g. an exception, a default value, etc.).
Structure diagram (textual)
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Client │────────▶│ IService │◀────────│ RealService │
└──────────────┘ └──────────────────┘ └──────────────┘
▲
┌────────┴──────────┐
│ Proxy │
│ (same interface) │
│ + extra logic │
└───────────────────┘
│
delegates to RealService
Remote Proxy case
In the case of a Remote Proxy, the call may cross process or network boundaries. In other scenarios, it is often a simple in-process method call.
Automatic generation
One of the great strengths of the Proxy Pattern: proxies can often be generated automatically from service definitions:
- WSDL (SOAP services)
- .proto (gRPC)
- Swagger / OpenAPI (REST)
5. Proxy Pattern Variants
There are four main variations of the Proxy Pattern:
| Variant | Main objective |
|---|---|
| Virtual Proxy | Replacement for objects that are expensive to create (performance optimization) |
| Remote Proxy | Hide connection details to a remote resource on the network |
| Smart Proxy | Perform additional actions when accessing a resource |
| Protective Proxy | Control access to a sensitive resource according to authorization rules |
6. Virtual Proxy — Virtual proxy
6.1 Concept and objective
A Virtual Proxy is a replacement for an object that is expensive to actually create. Its general goal is to optimize performance.
Rather than being the actual object, the proxy:
- Knows how to get the actual item when it is needed
- Delegate all calls to this real object once it gets it
Common Examples
- Placeholders in UI: allow the screen to be displayed quickly even if the real data is not yet available
- Lazy-loaded entity properties: are only populated when the property is accessed (often used in ORMs like Entity Framework)
Lazy Loading Concept
Lazy Loading consists of deferring the loading of expensive data until the moment it is actually needed, rather than loading it as soon as the object is constructed.
6.2 Implementation with inheritance (VirtualExpensiveToFullyLoad)
Base class with history
// BaseClassWithHistory.cs
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Proxy.VirtualProxy
{
public abstract class BaseClassWithHistory
{
public List<string> History { get; } = new List<string>();
}
}
This abstract class provides a History list allowing you to trace the operations performed (construction, data loading, etc.). It is useful for demonstrations and tests.
Expensive entity to load
// ExpensiveEntity.cs
namespace DesignPatternsInCSharp.Proxy.VirtualProxy
{
public class ExpensiveEntity
{
public int Id { get; set; }
}
}
Represents an entity that is expensive to load (database query, network call, etc.).
Simulated data source
// ExpensiveDataSource.cs
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Proxy.VirtualProxy
{
public static class ExpensiveDataSource
{
public static List<ExpensiveEntity> GetEntities(BaseClassWithHistory owner)
{
var list = new List<ExpensiveEntity>();
for (int i = 0; i < 10; i++)
{
list.Add(new ExpensiveEntity { Id = i });
}
owner.History.Add("Got expensive entities from source.");
return list;
}
}
}
ExpensiveDataSource simulates an expensive data source (database, API, etc.). It also traces its call in the history of the owning object.
Main class: ExpensiveToFullyLoad
// ExpensiveToFullyLoad.cs
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Proxy.VirtualProxy
{
public class ExpensiveToFullyLoad : BaseClassWithHistory
{
public static ExpensiveToFullyLoad Create()
{
return new VirtualExpensiveToFullyLoad();
}
public virtual IEnumerable<ExpensiveEntity> HomeEntities { get; protected set; }
public virtual IEnumerable<ExpensiveEntity> AwayEntities { get; protected set; }
protected ExpensiveToFullyLoad()
{
History.Add("Constructor called.");
}
}
}
Important points:
virtualproperties:HomeEntitiesandAwayEntitiesare markedvirtual→ they can be overloaded by the proxy to implement lazy loading.protectedconstructor: Only the class itself (or its children) can instantiate this class. This allows controlling instantiation and injecting the proxy type.- Factory method
Create(): Actually returns an instance ofVirtualExpensiveToFullyLoad, notExpensiveToFullyLoaddirectly. It is a Factory Method Pattern combined with the Proxy Pattern.
The Virtual Proxy: VirtualExpensiveToFullyLoad
// VirtualExpensiveToFullyLoad.cs
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Proxy.VirtualProxy
{
public class VirtualExpensiveToFullyLoad : ExpensiveToFullyLoad
{
public override IEnumerable<ExpensiveEntity> AwayEntities
{
get
{
if(base.AwayEntities == null)
{
base.AwayEntities = ExpensiveDataSource.GetEntities(this);
}
return base.AwayEntities;
}
protected set => base.AwayEntities = value;
}
public override IEnumerable<ExpensiveEntity> HomeEntities
{
get
{
if (base.HomeEntities == null)
{
base.HomeEntities = ExpensiveDataSource.GetEntities(this);
}
return base.HomeEntities;
}
protected set => base.HomeEntities = value;
}
}
}
Proxy analysis:
VirtualExpensiveToFullyLoadinherits fromExpensiveToFullyLoad- It overrides the two
virtualproperties - Pattern lazy: when first accessing the property, if it is
null, the proxy loads the data fromExpensiveDataSource, then returns the value. On subsequent accesses, the value is already loaded → no reloading. - This pattern is sometimes called “null check lazy loading”
Execution flow:
client.HomeEntities
→ VirtualExpensiveToFullyLoad.HomeEntities.get
→ base.HomeEntities == null ?
→ OUI: ExpensiveDataSource.GetEntities(this) → charge et stocke
→ NON: retourne directement la valeur en cache
6.3 Implementation with Lazy<T>
C# provides the generic Lazy<T> type which natively encapsulates lazy loading, without requiring inheritance or manual null checking.
// LazyExpensiveToFullyLoad.cs
using System;
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Proxy.VirtualProxy
{
public class LazyExpensiveToFullyLoad : BaseClassWithHistory
{
private Lazy<IEnumerable<ExpensiveEntity>> _homeEntities;
public IEnumerable<ExpensiveEntity> HomeEntities { get { return _homeEntities.Value; } }
private Lazy<IEnumerable<ExpensiveEntity>> _awayEntities;
public IEnumerable<ExpensiveEntity> AwayEntities { get { return _awayEntities.Value; } }
public LazyExpensiveToFullyLoad()
{
History.Add("Constructor called.");
_homeEntities = new Lazy<IEnumerable<ExpensiveEntity>>(
() => ExpensiveDataSource.GetEntities(this));
_awayEntities = new Lazy<IEnumerable<ExpensiveEntity>>(
() => ExpensiveDataSource.GetEntities(this));
}
}
}
Advantages of Lazy<T>:
- Thread-safe by default:
Lazy<T>handles concurrency automatically (by default, modeLazyThreadSafetyMode.ExecutionAndPublication) - Simpler code: no need to overload virtual methods or manually handle
nullchecking - Identical behavior: loading only takes place on first access to
.Value - Expressive: the code clearly expresses the intention of lazy loading
Note: In this case, LazyExpensiveToFullyLoad does not inherit from ExpensiveToFullyLoad — it is an independent implementation that uses Lazy<T> as an internal proxy mechanism.
Comparison of the two approaches
| Criterion | Inheritance + virtual | Lazy<T> |
|---|---|---|
| Required inheritance | Yes | No |
| Thread safety | Manual | Automatic |
| Complexity | Average | Low |
| ORM usage (EF) | Current | Less common |
| Expressiveness | Good | Very good |
6.4 Virtual Proxy unit tests
Inheritance Variant Tests
// Tests/ExpensiveToFullyLoadCreate.cs
using System.Linq;
using Xunit;
namespace DesignPatternsInCSharp.Proxy.VirtualProxy.Tests
{
public class ExpensiveToFullyLoadCreate
{
[Fact]
public void LogsConstructionToHistory()
{
var obj = ExpensiveToFullyLoad.Create();
Assert.Equal("Constructor called.", obj.History.Last());
}
[Fact]
public void LogsCollectionLoadingToHistory()
{
var obj = ExpensiveToFullyLoad.Create();
var list = obj.HomeEntities;
Assert.Equal(2, obj.History.Count());
var anotherList = obj.AwayEntities;
Assert.Equal(3, obj.History.Count());
}
}
}
What these tests check:
LogsConstructionToHistory: After creation, the history does contain"Constructor called."— proves that the base class constructor was executed.LogsCollectionLoadingToHistory:
- After creation only → 1 entry in the history (constructor)
- After accessing
HomeEntities→ 2 entries (constructor + loading) - After access to
AwayEntities→ 3 entries (+ a second load) - This proves that data is only loaded when each property is first accessed
Tests of the Lazy<T> variant
// Tests/LazyExpensiveToFullyLoadConstructor.cs
using System.Linq;
using Xunit;
using Xunit.Abstractions;
namespace DesignPatternsInCSharp.Proxy.VirtualProxy.Tests
{
public class LazyExpensiveToFullyLoadConstructor
{
private readonly ITestOutputHelper _output;
public LazyExpensiveToFullyLoadConstructor(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void LogsConstructionToHistory()
{
var obj = new LazyExpensiveToFullyLoad();
Assert.Equal("Constructor called.", obj.History.Last());
WriteHistory(obj);
}
[Fact]
public void LogsCollectionLoadingToHistory()
{
var obj = new LazyExpensiveToFullyLoad();
_output.WriteLine("Initial object created history:");
WriteHistory(obj);
var list = obj.HomeEntities;
Assert.Equal(2, obj.History.Count());
_output.WriteLine("Access HomeEntities. Now history:");
WriteHistory(obj);
var anotherList = obj.AwayEntities;
Assert.Equal(3, obj.History.Count());
_output.WriteLine("Access AwayEntities. Now history:");
WriteHistory(obj);
}
private void WriteHistory(LazyExpensiveToFullyLoad obj)
{
obj.History.ForEach(h => _output.WriteLine(h));
}
}
}
Expected behavior (test output):
Initial object created history:
Constructor called.
Access HomeEntities. Now history:
Constructor called.
Got expensive entities from source.
Access AwayEntities. Now history:
Constructor called.
Got expensive entities from source.
Got expensive entities from source.
7. Remote Proxy — Remote proxy
7.1 Concept and objective
A Remote Proxy is one of the most common uses of the Proxy Pattern. Its objective is to:
Act as a local resource while hiding connection details to a remote resource on the network.
The Remote Proxy centralizes all knowledge of network details, and these proxies can often be automatically generated from a service definition file:
- WSDL for SOAP services
- .proto for gRPC
- Swagger / OpenAPI for REST APIs
Advantages of Remote Proxy
| Advantage | Description |
|---|---|
| Transparency | The client does not need to know the address, protocol or serialization format |
| Encapsulation | All network communication details are in the proxy |
| Automatic generation | The proxy code can be generated from the service contract |
| Abstraction | The client interacts with a simple interface, as if calling a local method |
7.2 gRPC implementation
The demonstration uses a Google Remote Procedure Call (gRPC) service to demonstrate the Remote Proxy.
Service definition (.proto file)
// greet.proto
syntax = "proto3";
option csharp_namespace = "GreeterService";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
}
The .proto file is the service contract: it defines the available methods, the types of input and output messages. From this file, the gRPC framework automatically generates the basic client (proxy) code and server code.
Server-side gRPC service implementation
// Services/GreeterService.cs
using Grpc.Core;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
namespace GreeterService
{
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
}
Configuring the gRPC server
// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace GreeterService
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync(
"Communication with gRPC endpoints must be made through a gRPC client.");
});
});
}
}
}
Server entry point
// Program.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace GreeterService
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
7.3 Remote Proxy Test
// RemoteProxy/GreeterTests.cs
using Grpc.Net.Client;
using System.Threading.Tasks;
using Xunit;
namespace DesignPatternsInCSharp.Proxy.RemoteProxy
{
// NOTE: GreeterService must be running for these to pass
// The remote proxy code is generated at build time and found in the /obj/Debug folder
public class GreeterTests
{
[Fact]
public async Task GreetReturnsResponseAsync()
{
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new GreeterService.Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new GreeterService.HelloRequest { Name = "GreeterClient" });
Assert.Equal("Hello GreeterClient", reply.Message);
}
}
}
Important points:
- The
GreeterClientis the Automatically generated Remote Proxy: its code is located in the/obj/Debugfolder of the project. - This proxy was created by adding a Service Reference (connected reference) in Visual Studio, pointing to the
.protofile. - The client has no knowledge of HTTP/2 communication details, Protobuf serialization, or network connection management — all of which is encapsulated in the generated proxy.
- Note: this test only passes if the
GreeterServiceservice is running onhttps://localhost:5001.
Remote Proxy creation process in Visual Studio
- Right click on the project → Add → Connected Service
- Select Service Reference
- Point to the service
.protofile - Visual Studio generates the proxy code (
GreeterClient,HelloRequest,HelloReply) - The process is similar for an OpenAPI reference (REST API)
8. Smart Proxy
8.1 Concept and objective
A Smart Proxy adds additional logic around access to a resource. It is useful for:
- Reference counting (resource counting): know how many clients are using the resource
- Cache management (caching): memorize the result so as not to repeat a costly call
- Access locking (locking): control concurrent access to a shared resource
Note: The Smart Proxy is the closest to the Decorator pattern among all the Proxy variants — the difference is subtle (access control vs. adding functionality).
Illustrated issue: Concurrent access to a file
The demonstration shows a default file system provider attempting to write to the same file from multiple concurrent instances.
Behavior without Smart Proxy:
- Opening a
FileStreamfor writing locks the file - Any attempt to open the same file again throws an
IOException - The two streams cannot coexist
8.2 Default interface and implementation
Interface IFile
// IFile.cs
using System.IO;
namespace DesignPatternsInCSharp.Proxy.SmartProxy
{
public interface IFile
{
FileStream OpenWrite(string path);
}
}
This interface defines the contract: open a file for writing and return a FileStream.
Default implementation: DefaultFile
// DefaultFile.cs
using System.IO;
namespace DesignPatternsInCSharp.Proxy.SmartProxy
{
public class DefaultFile : IFile
{
public FileStream OpenWrite(string path)
{
return File.OpenWrite(path);
}
}
}
DefaultFile is a thin layer around the standard System.IO.File library. It does not handle concurrency — if two calls attempt to open the same file, the second throws an IOException.
8.3 Implementing FileSmartProxy
// FileSmartProxy.cs
using System.Collections.Generic;
using System.IO;
namespace DesignPatternsInCSharp.Proxy.SmartProxy
{
public class FileSmartProxy : IFile
{
Dictionary<string, FileStream> _openStreams = new Dictionary<string, FileStream>();
public FileStream OpenWrite(string path)
{
try
{
var stream = File.OpenWrite(path);
_openStreams.Add(path, stream);
return stream;
}
catch (IOException)
{
if(_openStreams.ContainsKey(path))
{
var stream = _openStreams[path];
if(stream != null && stream.CanWrite)
{
return stream;
}
}
throw;
}
}
}
}
Smart Proxy Analysis:
- Open stream dictionary:
_openStreamsmaintains a register of open files (path → FileStream) - First time opening: attempts to open the file normally. If successful, saves the stream to the dictionary.
- Second opening (same file):
File.OpenWrite()throws anIOException. The proxy catches this exception and checks whether an existing stream is still valid and writable. If so, it returns the existing flow rather than propagating the exception. - If no valid flow: re-raises the original exception.
Behavior: Multiple calls to OpenWrite() on the same path return the same FileStream, thus avoiding the concurrency conflict.
8.4 Smart Proxy unit tests
// Tests/FileConcurrentWrites.cs
using System.IO;
using System.Text;
using Xunit;
namespace DesignPatternsInCSharp.Proxy.SmartProxy.Tests
{
public class FileConcurrentWrites
{
private readonly string _testFile = "output.txt";
[Fact]
public void RaisesExceptionWithDirectFileAccess()
{
var fs = new DefaultFile();
byte[] outputBytes1 = Encoding.ASCII.GetBytes("1. ardalis.com\n");
byte[] outputBytes2 = Encoding.ASCII.GetBytes("2. weeklydevtips.com\n");
using var file = fs.OpenWrite(_testFile);
Assert.Throws<IOException>(() =>
fs.OpenWrite(_testFile)); // Lance IOException : fichier déjà verrouillé
file.Write(outputBytes1);
// file2 n'existe jamais : on n'atteint jamais ce code
file.Close();
}
[Fact]
public void ManageReferences()
{
var fs = new FileSmartProxy();
byte[] outputBytes1 = Encoding.ASCII.GetBytes("1. ardalis.com\n");
byte[] outputBytes2 = Encoding.ASCII.GetBytes("2. weeklydevtips.com\n");
using var file = fs.OpenWrite(_testFile);
using var file2 = fs.OpenWrite(_testFile); // Retourne le même flux : pas d'exception
file.Write(outputBytes1);
file2.Write(outputBytes2); // Les deux écritures fonctionnent
file.Close();
file2.Close();
}
}
}
What these tests demonstrate:
-
RaisesExceptionWithDirectFileAccess: WithDefaultFile, opening the same file twice does raise anIOException. This test validates the problematic behavior without the proxy. -
ManageReferences: WithFileSmartProxy, both calls toOpenWrite()on the same file return references to the same stream, allowing both writes without error.
9. Protective Proxy
9.1 Concept and objective
A Protective Proxy (or Protection Proxy) controls access to a resource by checking certain rules — typically authorization rules.
Advantages
- Separation of Concerns: authorization rules do not pollute the business logic of the resource
- DRY principle (Don’t Repeat Yourself): access checks are centralized in the proxy, not duplicated in each client
- SRP Principle (Single Responsibility Principle): the resource only manages its business domain, the proxy manages access
- Gatekeeper: the proxy acts as guardian of the resource
The Protective Proxy can be seen as a form of guardian around the resource.
Domain context
The demonstration uses a document management system with roles:
| Role | Abilities |
|---|---|
| Author | Create document, edit name and content, assign initial tags, submit for review |
| Editor | Edit tags, mark review as complete (DateReviewed) |
| Rule | An Editor can NOT edit the name or content directly |
9.2 Domain model (Document, User, Roles)
Role enumeration
// Roles.cs
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy
{
public enum Roles
{
Author,
Editor
}
// Author can create a new document
// Author can update the name of the document, modify its content, and assign initial tags
// Author can submit document for review by an Editor
// Editor can edit tags
// Editor can mark the document reviewed (sets DateReviewed)
// Editor cannot modify Name or Content directly
// Maybe add Admin who can do anything (later)
}
Class User
// User.cs
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy
{
public class User
{
public string Name { get; set; }
public Roles Role { get; set; }
public List<Document> AuthoredDocuments { get; } = new List<Document>();
public void AddDocument(string documentName, string documentContent)
{
var document = Document.CreateDocument(documentName, documentContent);
AuthoredDocuments.Add(document);
}
}
}
User has a role (Roles) and a list of documents of which he is the author. The AddDocument method creates a document via the factory and adds it to its list.
Document class (base class)
// Document.cs
using System;
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy
{
public class Document
{
public static Document CreateDocument(string name, string content)
{
return new ProtectedDocument(name, content);
}
protected Document(string name, string content)
{
Name = name;
Content = content;
}
public int Id { get; private set; }
public string Name { get; private set; }
public IEnumerable<string> Tags { get; private set; }
public string Content { get; private set; }
public DateTime DateCreated { get; private set; } = DateTime.UtcNow;
public DateTime? DateReviewed { get; private set; }
internal virtual void CompleteReview(User editor)
{
DateReviewed = DateTime.UtcNow;
}
internal virtual void UpdateName(string newName, User user)
{
Name = newName;
}
}
}
Important points:
protectedconstructor: Prevents direct instantiation from the outside → control via the factoryCreateDocument(factory method): Actually returns aProtectedDocument— the client always gets the protected versionDateReviewedwith setterprivate: Only internal methods of the class can modify this valuevirtualmethods:CompleteReviewandUpdateNamearevirtual→ proxy can override them to add checksinternal: Methods areinternalto limit their visibility to the assembly
9.3 Implementation of ProtectedDocument
// ProtectedDocument.cs
using System;
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy
{
public class ProtectedDocument : Document
{
public ProtectedDocument(string name, string content) : base(name, content)
{
}
internal override void UpdateName(string newName, User user)
{
if(user.Role != Roles.Author)
{
throw new UnauthorizedAccessException(
"Cannot update name unless in Author role.");
}
base.UpdateName(newName, user);
}
internal override void CompleteReview(User editor)
{
if (editor.Role != Roles.Editor)
{
throw new UnauthorizedAccessException(
"Cannot review documents unless you are an Editor.");
}
base.CompleteReview(editor);
}
}
}
Protective Proxy analysis:
ProtectedDocumentinherits fromDocumentand overrides both methodsUpdateName: checks that the user isAuthor. If not →UnauthorizedAccessException. If yes → delegate tobase.UpdateName()CompleteReview: checks that the user isEditor. If not →UnauthorizedAccessException. If yes → delegate tobase.CompleteReview()- Centralized authorization logic: all security logic is in
ProtectedDocument, not inDocumentor in clients
Flow diagram:
client.UpdateName(newName, user)
→ ProtectedDocument.UpdateName(newName, user)
→ user.Role == Author ?
→ NON: throw UnauthorizedAccessException
→ OUI: base.UpdateName(newName, user) → Document.UpdateName()
9.4 Protective Proxy unit tests
Test constants
// Tests/TestConstants.cs
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy.Tests
{
public class TestConstants
{
public const string TEST_DOCUMENT_NAME = "test name";
public const string TEST_DOCUMENT_CONTENT = "test content";
}
}
Test: An author can add a document
// Tests/AuthorAddDocument.cs
using Xunit;
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy.Tests
{
public class AuthorAddDocument
{
[Fact]
public void AddsDocumentToAuthoredDocuments()
{
var author = new User { Role = Roles.Author };
author.AddDocument(
TestConstants.TEST_DOCUMENT_NAME,
TestConstants.TEST_DOCUMENT_CONTENT);
Assert.Contains(
author.AuthoredDocuments,
doc => doc.Name == TestConstants.TEST_DOCUMENT_NAME);
}
}
}
Test: An author can modify the name of a document
// Tests/AuthorUpdateDocumentName.cs
using System;
using Xunit;
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy.Tests
{
public class AuthorUpdateDocumentName
{
[Fact]
public void UpdatesNameGivenUserInAuthorRole()
{
var author = new User { Role = Roles.Author };
var document = Document.CreateDocument(
TestConstants.TEST_DOCUMENT_NAME,
TestConstants.TEST_DOCUMENT_CONTENT);
string newName = "new name";
document.UpdateName(newName, author);
Assert.Equal(newName, document.Name);
}
[Fact]
public void ThrowsUnauthorizedExceptionGivenUserNotInAuthorRole()
{
var editor = new User { Role = Roles.Editor };
var document = new ProtectedDocument(
TestConstants.TEST_DOCUMENT_NAME,
TestConstants.TEST_DOCUMENT_CONTENT);
Assert.Throws<UnauthorizedAccessException>(
() => document.UpdateName(TestConstants.TEST_DOCUMENT_NAME, editor));
}
}
}
What these tests demonstrate:
- An
Authoruser can modify the name → test passes - An
Editoruser attempting to change the name →UnauthorizedAccessExceptionthrown
Test: An editor can complete a review
// Tests/EditorReviewDocument.cs
using System;
using Xunit;
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy.Tests
{
public class EditorReviewDocument
{
[Fact]
public void SetsDateReviewedToCurrentDateTime()
{
var editor = new User { Role = Roles.Editor };
var document = Document.CreateDocument(
TestConstants.TEST_DOCUMENT_NAME,
TestConstants.TEST_DOCUMENT_CONTENT);
document.CompleteReview(editor);
Assert.True(DateTime.UtcNow - document.DateReviewed < TimeSpan.FromMilliseconds(500));
}
}
}
Test: An author cannot complete a review
// Tests/AuthorReviewDocument.cs
using System;
using Xunit;
namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy.Tests
{
public class AuthorReviewDocument
{
[Fact]
public void ThrowsUnauthorizedExceptionAndDoesNotSetDateReviewed()
{
var author = new User { Role = Roles.Author };
var document = Document.CreateDocument(
TestConstants.TEST_DOCUMENT_NAME,
TestConstants.TEST_DOCUMENT_CONTENT);
Assert.Throws<UnauthorizedAccessException>(
() => document.CompleteReview(author));
Assert.Null(document.DateReviewed);
}
}
}
What this test demonstrates:
- An
Authorattempting to complete a review →UnauthorizedAccessException - Property
DateReviewedremainsnull(no partial state change)
10. Design principles related to the Proxy Pattern
The Proxy Pattern, when used correctly, promotes several fundamental principles of software design:
10.1 Separation of Concerns
Without the Proxy Pattern, the concerns are mixed:
- Access control logic is found in the resource itself
- Or worse, each client that consumes the resource must perform these checks
With the Proxy Pattern:
- Access control logic is isolated in proxy
- The resource remains focused on its business area
- Keeping Customers Simple
10.2 Single Responsibility Principle (SRP)
Each class must have only one reason to change.
Document: responsible for the business logic of documentsProtectedDocument: responsible for authorization rulesVirtualExpensiveToFullyLoad: responsible for lazy loadingFileSmartProxy: responsible for managing concurrent access to files
10.3 Loose Coupling
Without proxy:
- Client is coupled to details: network logic, security rules, cache mechanisms
- Hard to test, hard to modify
With the Proxy Pattern:
- Strong coupling is concentrated in proxy
- Client is only coupled to the interface
- Easy to substitute, test and evolve
10.4 Cross-Cutting Concerns
Proxies are particularly suited to cross-cutting concerns:
- Authorization: check the rights before each operation
- Caching: memorize results
- Logging: log access
- Performance: measure execution times
These concerns can be encapsulated in reusable proxies throughout the application.
10.5 DRY principle (Don’t Repeat Yourself)
Proxy logic is defined once in the proxy class and applies uniformly, instead of being repeated in each client that accesses the resource.
Summary of principles
| Principle | How the Proxy Pattern applies it |
|---|---|
| PRS | Each class has a single responsibility (business vs. access control) |
| Separation of Concerns | Access, caching, security separated from business logic |
| Loose Coupling | Client coupled only to the interface, not to the details |
| DRY | Access logic defined only once in proxy |
| Open/Closed | The resource is not modified to add access control |
11. Related Patterns
The Proxy Pattern is structurally similar to several other patterns. It is important to understand the differences.
11.1 Decorator Pattern
| Criterion | Proxy | Decorator |
|---|---|---|
| Structure | Very similar | Very similar |
| Intent | Control access to a resource | Add functionality to an object |
| Similar case | Smart Proxy (nearest) | — |
The difference between a Smart Proxy and a Decorator can be very small. The main distinction is intent: access control vs. behavior enrichment.
11.2 Prototype Pattern
| Criterion | Virtual Proxy | Prototype |
|---|---|---|
| Common problem | Expensive items to create | Expensive items to create |
| Solution | Stub/placeholder, load actual object on demand | Keeps a copy and can clone it |
| Relationship to real object | Delegate to real object | Becomes the real object (via clone) |
11.3 Adapter Pattern
| Criterion | Proxy | Adapt |
|---|---|---|
| Structure | Similar | Similar |
| Intent | Control access | Convert one incompatible interface to another |
| Interface | Same interface as the resource | Interface different from that of the resource |
If the client needs a different interface to the resource, use the Adapter, not the Proxy.
11.4 Flyweight Pattern
| Criterion | Proxy | Flyweight |
|---|---|---|
| Structure | Similar | Similar |
| Instance management | Encapsulates a single specific instance | Handles many references to a shared instance |
Visual Summary
Pattern | Structure | Intention principale
------------|------------|------------------------------------------
Proxy | Interface | Contrôler l'accès
Decorator | Interface | Ajouter des fonctionnalités
Adapter | Interface | Convertir une interface incompatible
Prototype | — | Cloner un objet coûteux
Flyweight | — | Partager de nombreuses références légères
12. Key Takeaways
Full summary
The Proxy Pattern is one of the most common and useful design patterns. Here are the essential points:
-
Intent is everything: Although the Proxy is structurally similar to several other patterns (Decorator, Adapter, Flyweight), it is its intention to control access to another class that sets it apart.
-
Four main variants for four different contexts:
| Variant | Role | Use cases |
|---|---|---|
| Virtual Proxy | Placeholder for expensive items | Lazy loading, UI placeholders |
| Remote Proxy | Seamless connection to remote resources | gRPC, REST, WCF |
| Smart Proxy | Counting, locking, other operations | Concurrent access management, caching |
| Protective Proxy | Authorization rules | Role-based access control |
-
Proxies can be generated automatically: particularly common for Remote Proxies (from
.proto, Swagger, WSDL). -
Favored Design Principles:
- Separation of Concerns
- Single Responsibility Principle (SRP)
- Loose coupling (loose coupling)
- DRY
-
The client must remain ignorant: one of the fundamental points of the pattern is that the client does not know (and should not know) whether it is working with the real service or with a proxy.
-
Factory Method often combined: to force the use of the proxy (
protectedconstructor + staticCreate()method)
Patterns to explore in addition
- SOLID Principles: to deepen SRP and Separation of Concerns
- Decorator Pattern: for behavior enrichment
- Adapter Pattern: for interface conversion
- Factory Method Pattern: often used in conjunction with the Proxy
13. Resources and references
- GitHub repo: DesignPatternsInCSharp — ardalis
- Author’s website: ardalis.com
- Podcast: weeklydevtips.com
- Social networks: @ardalis
- Related Pluralsight courses:
- SOLID Principles (Steve Smith)
- Factory Method Pattern (Steve Smith)
- Other Design Patterns C# courses (Steve Smith)
14. Appendix: Structure of the demonstration project
DesignPatternsInCSharp/
├── DesignPatternsInCSharp.sln
├── DesignPatternsInCSharp/
│ └── Proxy/
│ ├── VirtualProxy/
│ │ ├── BaseClassWithHistory.cs
│ │ ├── ExpensiveDataSource.cs
│ │ ├── ExpensiveEntity.cs
│ │ ├── ExpensiveToFullyLoad.cs ← Classe principale (factory)
│ │ ├── VirtualExpensiveToFullyLoad.cs ← Virtual Proxy (héritage)
│ │ ├── LazyExpensiveToFullyLoad.cs ← Virtual Proxy (Lazy<T>)
│ │ └── Tests/
│ │ ├── ExpensiveToFullyLoadCreate.cs
│ │ └── LazyExpensiveToFullyLoadConstructor.cs
│ ├── RemoteProxy/
│ │ └── GreeterTests.cs ← Test du proxy gRPC généré
│ ├── SmartProxy/
│ │ ├── IFile.cs ← Interface commune
│ │ ├── DefaultFile.cs ← Implémentation de base
│ │ ├── FileSmartProxy.cs ← Smart Proxy
│ │ └── Tests/
│ │ └── FileConcurrentWrites.cs
│ └── ProtectiveProxy/
│ ├── Roles.cs ← Enum des rôles
│ ├── User.cs ← Entité utilisateur
│ ├── Document.cs ← Classe de base (factory)
│ ├── ProtectedDocument.cs ← Protective Proxy
│ └── Tests/
│ ├── TestConstants.cs
│ ├── AuthorAddDocument.cs
│ ├── AuthorUpdateDocumentName.cs
│ ├── AuthorReviewDocument.cs
│ └── EditorReviewDocument.cs
└── GreeterService/ ← Service gRPC (Remote Proxy)
├── Program.cs
├── Startup.cs
├── Protos/
│ └── greet.proto ← Contrat du service
└── Services/
└── GreeterService.cs ← Implémentation du service
Search Terms
c-sharp · design · patterns · proxy · testing · architecture · c# · .net · development · pattern · test · concept · objective · remote · tests · class · document · author · grpc · lazy · unit · virtual · advantages · analogy