Advanced

C-Sharp Design Patterns Proxy

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.

GitHub repo: DesignPatternsInCSharp (ardalis)


Table of Contents

  1. Course Overview
  2. Introduction to Proxy Pattern
  3. Problem solved by Proxy Pattern
  4. Structure and design of the Proxy Pattern
  5. Proxy Pattern Variants
  6. Virtual Proxy — Virtual Proxy
  1. Remote Proxy — Remote Proxy
  1. Smart Proxy — Intelligent Proxy
  1. Protective Proxy — Protection Proxy
  1. Proxy Pattern Design Principles
  2. Related Patterns
  3. Key Takeaways
  4. Resources and references

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

  1. What problems does the Proxy Pattern solve?
  2. How is it structured and how does it fit into your existing code base?
  3. How to apply the pattern in real code through several concrete examples
  4. 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

ContextDescription
User interface (UI)Loading content on demand (infinite scroll): a proxy serves as a placeholder for content not yet retrieved from the server
Security / AuthorizationCheck a user’s access rights before allowing them to access a resource
CachingCache expensive call results to avoid repeating them
Lazy loadingOnly load expensive data when it is actually needed
Network proxiesCentralize 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

  1. 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.

  2. Transparency for the customer. The client must be completely unaware of the presence of the proxy. It thinks it works directly with the RealService.

  3. 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 RealService at all (e.g.: access denied, cached result)
  • Execute logic after receiving response from RealService (e.g.: caching, logging)
  1. 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:

VariantMain objective
Virtual ProxyReplacement for objects that are expensive to create (performance optimization)
Remote ProxyHide connection details to a remote resource on the network
Smart ProxyPerform additional actions when accessing a resource
Protective ProxyControl 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

  1. Placeholders in UI: allow the screen to be displayed quickly even if the real data is not yet available
  2. 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:

  • virtual properties: HomeEntities and AwayEntities are marked virtual → they can be overloaded by the proxy to implement lazy loading.
  • protected constructor: 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 of VirtualExpensiveToFullyLoad, not ExpensiveToFullyLoad directly. 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:

  • VirtualExpensiveToFullyLoad inherits from ExpensiveToFullyLoad
  • It overrides the two virtual properties
  • Pattern lazy: when first accessing the property, if it is null, the proxy loads the data from ExpensiveDataSource, 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, mode LazyThreadSafetyMode.ExecutionAndPublication)
  • Simpler code: no need to overload virtual methods or manually handle null checking
  • 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

CriterionInheritance + virtualLazy<T>
Required inheritanceYesNo
Thread safetyManualAutomatic
ComplexityAverageLow
ORM usage (EF)CurrentLess common
ExpressivenessGoodVery 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:

  1. LogsConstructionToHistory: After creation, the history does contain "Constructor called." — proves that the base class constructor was executed.
  2. 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

AdvantageDescription
TransparencyThe client does not need to know the address, protocol or serialization format
EncapsulationAll network communication details are in the proxy
Automatic generationThe proxy code can be generated from the service contract
AbstractionThe 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 GreeterClient is the Automatically generated Remote Proxy: its code is located in the /obj/Debug folder of the project.
  • This proxy was created by adding a Service Reference (connected reference) in Visual Studio, pointing to the .proto file.
  • 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 GreeterService service is running on https://localhost:5001.

Remote Proxy creation process in Visual Studio

  1. Right click on the project → Add → Connected Service
  2. Select Service Reference
  3. Point to the service .proto file
  4. Visual Studio generates the proxy code (GreeterClient, HelloRequest, HelloReply)
  5. 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 FileStream for 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:

  1. Open stream dictionary: _openStreams maintains a register of open files (path → FileStream)
  2. First time opening: attempts to open the file normally. If successful, saves the stream to the dictionary.
  3. Second opening (same file): File.OpenWrite() throws an IOException. 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.
  4. 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:

  1. RaisesExceptionWithDirectFileAccess: With DefaultFile, opening the same file twice does raise an IOException. This test validates the problematic behavior without the proxy.

  2. ManageReferences: With FileSmartProxy, both calls to OpenWrite() 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:

RoleAbilities
AuthorCreate document, edit name and content, assign initial tags, submit for review
EditorEdit tags, mark review as complete (DateReviewed)
RuleAn 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:

  • protected constructor: Prevents direct instantiation from the outside → control via the factory
  • CreateDocument (factory method): Actually returns a ProtectedDocument — the client always gets the protected version
  • DateReviewed with setter private: Only internal methods of the class can modify this value
  • virtual methods: CompleteReview and UpdateName are virtual → proxy can override them to add checks
  • internal: Methods are internal to 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:

  • ProtectedDocument inherits from Document and overrides both methods
  • UpdateName: checks that the user is Author. If not → UnauthorizedAccessException. If yes → delegate to base.UpdateName()
  • CompleteReview: checks that the user is Editor. If not → UnauthorizedAccessException. If yes → delegate to base.CompleteReview()
  • Centralized authorization logic: all security logic is in ProtectedDocument, not in Document or 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:

  1. An Author user can modify the name → test passes
  2. An Editor user attempting to change the name → UnauthorizedAccessException thrown

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 Author attempting to complete a review → UnauthorizedAccessException
  • Property DateReviewed remains null (no partial state change)

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 documents
  • ProtectedDocument: responsible for authorization rules
  • VirtualExpensiveToFullyLoad: responsible for lazy loading
  • FileSmartProxy: 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

PrincipleHow the Proxy Pattern applies it
PRSEach class has a single responsibility (business vs. access control)
Separation of ConcernsAccess, caching, security separated from business logic
Loose CouplingClient coupled only to the interface, not to the details
DRYAccess logic defined only once in proxy
Open/ClosedThe resource is not modified to add access control

The Proxy Pattern is structurally similar to several other patterns. It is important to understand the differences.

11.1 Decorator Pattern

CriterionProxyDecorator
StructureVery similarVery similar
IntentControl access to a resourceAdd functionality to an object
Similar caseSmart 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

CriterionVirtual ProxyPrototype
Common problemExpensive items to createExpensive items to create
SolutionStub/placeholder, load actual object on demandKeeps a copy and can clone it
Relationship to real objectDelegate to real objectBecomes the real object (via clone)

11.3 Adapter Pattern

CriterionProxyAdapt
StructureSimilarSimilar
IntentControl accessConvert one incompatible interface to another
InterfaceSame interface as the resourceInterface 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

CriterionProxyFlyweight
StructureSimilarSimilar
Instance managementEncapsulates a single specific instanceHandles 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:

  1. 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.

  2. Four main variants for four different contexts:

VariantRoleUse cases
Virtual ProxyPlaceholder for expensive itemsLazy loading, UI placeholders
Remote ProxySeamless connection to remote resourcesgRPC, REST, WCF
Smart ProxyCounting, locking, other operationsConcurrent access management, caching
Protective ProxyAuthorization rulesRole-based access control
  1. Proxies can be generated automatically: particularly common for Remote Proxies (from .proto, Swagger, WSDL).

  2. Favored Design Principles:

  • Separation of Concerns
  • Single Responsibility Principle (SRP)
  • Loose coupling (loose coupling)
  • DRY
  1. 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.

  2. Factory Method often combined: to force the use of the proxy (protected constructor + static Create() 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.