BrandGhost
Proxy Pattern Real-World Example in C#: Complete Implementation

Proxy Pattern Real-World Example in C#: Complete Implementation

Proxy Pattern Real-World Example in C#: Complete Implementation

Most proxy pattern tutorials show you a class that lazily loads an image or guards access with a permission check. That's fine for textbook understanding, but it falls apart the moment you need to layer caching, rate limiting, and structured logging onto an HTTP API client without turning your codebase into a tangled mess. This article builds a complete proxy pattern real-world example in C# -- a weather service API client where each cross-cutting concern lives in its own proxy class that can be composed through dependency injection.

By the end, you'll have a fully compilable implementation covering every layer: the subject interface, the real HTTP service, a caching proxy with configurable TTL, a rate-limiting proxy backed by SemaphoreSlim and a token bucket, a logging proxy with timing diagnostics, and a complete Program.cs that composes them all through dependency injection. Every proxy wraps the next without modifying existing code, and adding a new concern means writing one class and updating one DI registration.

The Problem: Cross-Cutting Concerns in API Clients

You're building a service that consumes a third-party weather API. The initial implementation works, but production demands pile up fast. You need caching to avoid hammering the API on repeated requests. You need rate limiting because the provider enforces quotas. You need structured logging with timing to diagnose performance issues. And you need all of this without turning your HTTP client class into a 500-line monster that violates every separation-of-concern principle you've ever learned.

Here's what happens when you shove everything into a single class:

public sealed class WeatherService
{
    private readonly HttpClient _httpClient;
    private readonly IMemoryCache _cache;
    private readonly ILogger<WeatherService> _logger;
    private readonly SemaphoreSlim _rateLimiter;

    public async Task<WeatherForecast> GetForecastAsync(
        string city)
    {
        var cacheKey = $"forecast_{city}";
        if (_cache.TryGetValue(cacheKey, out WeatherForecast? cached))
        {
            _logger.LogInformation(
                "Cache hit for {City}", city);
            return cached!;
        }

        await _rateLimiter.WaitAsync();
        try
        {
            _logger.LogInformation(
                "Fetching forecast for {City}", city);
            var stopwatch = Stopwatch.StartNew();

            var response = await _httpClient
                .GetFromJsonAsync<WeatherForecast>(
                    $"/api/forecast/{city}");

            stopwatch.Stop();
            _logger.LogInformation(
                "Forecast retrieved in {Elapsed}ms",
                stopwatch.ElapsedMilliseconds);

            _cache.Set(
                cacheKey,
                response,
                TimeSpan.FromMinutes(10));

            return response!;
        }
        finally
        {
            _rateLimiter.Release();
        }
    }
}

Every concern is woven together. Testing caching means also dealing with the rate limiter and logger. Adjusting the rate-limiting strategy requires touching the same class that handles HTTP calls. Adding a circuit breaker means yet another field, more conditional logic, and a class that grows in every direction simultaneously.

The proxy pattern solves this by giving each concern its own class that implements the same interface. Each proxy wraps the next, forming a chain where every layer handles exactly one responsibility. This is structurally similar to how the decorator pattern layers behavior, and the two patterns share the wrapping mechanic. The distinction is in intent -- proxies control access to the real subject, while decorators add behavior. In practice, the implementation looks nearly identical, and this proxy pattern example in C# demonstrates that overlap.

Defining the Subject Interface

The subject interface defines the contract that both the real service and all proxies implement. Keep it focused on what consumers of the weather API actually need:

public sealed record WeatherForecast(
    string City,
    double TemperatureCelsius,
    string Summary,
    DateTimeOffset RetrievedAt);

public sealed record HistoricalWeatherData(
    string City,
    DateTimeOffset Date,
    double HighCelsius,
    double LowCelsius,
    string Conditions);

public interface IWeatherService
{
    Task<WeatherForecast> GetForecastAsync(
        string city);

    Task<IReadOnlyList<HistoricalWeatherData>>
        GetHistoricalDataAsync(
            string city,
            DateTimeOffset from,
            DateTimeOffset to);
}

Two methods, two record types, one interface. The WeatherForecast and HistoricalWeatherData records are immutable and carry only the data your system needs. Both methods are async because every proxy and the real service will involve asynchronous operations. This interface becomes the single dependency that calling code references -- they never know whether they're talking to the real HTTP client or a stack of proxies. This aligns with the core idea behind inversion of control -- depend on abstractions rather than concrete implementations.

Building the Real Subject: HttpWeatherService

The real subject handles the actual HTTP communication. It's the only class in the chain that talks to the network:

public sealed class HttpWeatherService : IWeatherService
{
    private readonly HttpClient _httpClient;

    public HttpWeatherService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<WeatherForecast> GetForecastAsync(
        string city)
    {
        var response = await _httpClient
            .GetFromJsonAsync<WeatherApiResponse>(
                $"/api/forecast/{city}");

        if (response is null)
        {
            throw new InvalidOperationException(
                $"No forecast data returned for {city}");
        }

        return new WeatherForecast(
            City: city,
            TemperatureCelsius: response.Temperature,
            Summary: response.Description,
            RetrievedAt: DateTimeOffset.UtcNow);
    }

    public async Task<IReadOnlyList<HistoricalWeatherData>>
        GetHistoricalDataAsync(
            string city,
            DateTimeOffset from,
            DateTimeOffset to)
    {
        var url = $"/api/history/{city}" +
            $"?from={from:yyyy-MM-dd}" +
            $"&to={to:yyyy-MM-dd}";

        var response = await _httpClient
            .GetFromJsonAsync<List<HistoricalApiResponse>>(
                url);

        if (response is null)
        {
            return Array.Empty<HistoricalWeatherData>();
        }

        return response
            .Select(r => new HistoricalWeatherData(
                City: city,
                Date: r.Date,
                HighCelsius: r.High,
                LowCelsius: r.Low,
                Conditions: r.Conditions))
            .ToList()
            .AsReadOnly();
    }
}

public sealed record WeatherApiResponse
{
    public double Temperature { get; init; }
    public string Description { get; init; } = "";
}

public sealed record HistoricalApiResponse
{
    public DateTimeOffset Date { get; init; }
    public double High { get; init; }
    public double Low { get; init; }
    public string Conditions { get; init; } = "";
}

Notice that HttpWeatherService does exactly one thing -- it makes HTTP calls and maps responses to domain records. No caching, no rate limiting, no logging. The DTO records (WeatherApiResponse and HistoricalApiResponse) are mutable enough for JSON deserialization but stay contained within this class's scope. The proxy pattern real-world example starts here with a clean, single-responsibility real subject.

Caching Proxy: CachingWeatherProxy

The caching proxy intercepts calls and checks IMemoryCache before forwarding to the inner service. This proxy pattern example uses configurable TTL values so different operations can cache at different durations:

public sealed class CachingWeatherProxy : IWeatherService
{
    private readonly IWeatherService _inner;
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _forecastTtl;
    private readonly TimeSpan _historicalTtl;

    public CachingWeatherProxy(
        IWeatherService inner,
        IMemoryCache cache,
        TimeSpan forecastTtl,
        TimeSpan historicalTtl)
    {
        _inner = inner;
        _cache = cache;
        _forecastTtl = forecastTtl;
        _historicalTtl = historicalTtl;
    }

    public async Task<WeatherForecast> GetForecastAsync(
        string city)
    {
        var cacheKey = $"forecast:{city.ToLowerInvariant()}";

        if (_cache.TryGetValue(
            cacheKey, out WeatherForecast? cached))
        {
            return cached!;
        }

        var result = await _inner.GetForecastAsync(city);

        _cache.Set(cacheKey, result, _forecastTtl);

        return result;
    }

    public async Task<IReadOnlyList<HistoricalWeatherData>>
        GetHistoricalDataAsync(
            string city,
            DateTimeOffset from,
            DateTimeOffset to)
    {
        var cacheKey =
            $"history:{city.ToLowerInvariant()}" +
            $":{from:yyyyMMdd}:{to:yyyyMMdd}";

        if (_cache.TryGetValue(
            cacheKey,
            out IReadOnlyList<HistoricalWeatherData>? cached))
        {
            return cached!;
        }

        var result = await _inner
            .GetHistoricalDataAsync(city, from, to);

        _cache.Set(cacheKey, result, _historicalTtl);

        return result;
    }
}

The caching proxy stores _inner -- the next IWeatherService in the chain. It builds deterministic cache keys using normalized city names and date ranges. The forecast TTL and historical TTL are injected, making the caching behavior fully configurable without modifying the proxy code itself.

Rate Limiting Proxy: RateLimitingWeatherProxy

The rate-limiting proxy controls how many concurrent requests reach the downstream service. This implementation uses SemaphoreSlim for concurrency control combined with a simple token bucket for request-per-second throttling:

public sealed class RateLimitingWeatherProxy
    : IWeatherService, IDisposable
{
    private readonly IWeatherService _inner;
    private readonly SemaphoreSlim _concurrencyLimiter;
    private readonly SemaphoreSlim _tokenBucket;
    private readonly Timer _tokenRefillTimer;

    public RateLimitingWeatherProxy(
        IWeatherService inner,
        int maxConcurrentRequests,
        int maxRequestsPerSecond)
    {
        _inner = inner;
        _concurrencyLimiter = new SemaphoreSlim(
            maxConcurrentRequests,
            maxConcurrentRequests);
        _tokenBucket = new SemaphoreSlim(
            maxRequestsPerSecond,
            maxRequestsPerSecond);

        _tokenRefillTimer = new Timer(
            _ => RefillTokens(maxRequestsPerSecond),
            null,
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(1));
    }

    public async Task<WeatherForecast> GetForecastAsync(
        string city)
    {
        await AcquirePermissionAsync();

        try
        {
            return await _inner.GetForecastAsync(city);
        }
        finally
        {
            _concurrencyLimiter.Release();
        }
    }

    public async Task<IReadOnlyList<HistoricalWeatherData>>
        GetHistoricalDataAsync(
            string city,
            DateTimeOffset from,
            DateTimeOffset to)
    {
        await AcquirePermissionAsync();

        try
        {
            return await _inner
                .GetHistoricalDataAsync(city, from, to);
        }
        finally
        {
            _concurrencyLimiter.Release();
        }
    }

    private async Task AcquirePermissionAsync()
    {
        await _tokenBucket.WaitAsync();
        await _concurrencyLimiter.WaitAsync();
    }

    private void RefillTokens(int maxTokens)
    {
        var tokensToAdd =
            maxTokens - _tokenBucket.CurrentCount;

        for (var i = 0; i < tokensToAdd; i++)
        {
            try
            {
                _tokenBucket.Release();
            }
            catch (SemaphoreFullException)
            {
                break;
            }
        }
    }

    public void Dispose()
    {
        _tokenRefillTimer.Dispose();
        _concurrencyLimiter.Dispose();
        _tokenBucket.Dispose();
    }
}

The token bucket refills every second, capping the number of requests that can flow through per interval. The SemaphoreSlim for concurrency ensures that even if tokens are available, only a fixed number of requests execute simultaneously. The AcquirePermissionAsync method gates both checks before forwarding to _inner. If you've worked with the command pattern, you'll recognize this separation of "deciding whether to execute" from "actually executing" -- it's the same principle applied at the infrastructure level.

Logging Proxy: LoggingWeatherProxy

The logging proxy adds structured logging with elapsed time measurements to every call:

public sealed class LoggingWeatherProxy : IWeatherService
{
    private readonly IWeatherService _inner;
    private readonly ILogger<LoggingWeatherProxy> _logger;

    public LoggingWeatherProxy(
        IWeatherService inner,
        ILogger<LoggingWeatherProxy> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task<WeatherForecast> GetForecastAsync(
        string city)
    {
        _logger.LogInformation(
            "Requesting forecast for {City}", city);

        var stopwatch = Stopwatch.StartNew();

        try
        {
            var result = await _inner
                .GetForecastAsync(city);

            stopwatch.Stop();
            _logger.LogInformation(
                "Forecast for {City} retrieved in " +
                "{ElapsedMs}ms",
                city,
                stopwatch.ElapsedMilliseconds);

            return result;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(
                ex,
                "Forecast request for {City} failed " +
                "after {ElapsedMs}ms",
                city,
                stopwatch.ElapsedMilliseconds);

            throw;
        }
    }

    public async Task<IReadOnlyList<HistoricalWeatherData>>
        GetHistoricalDataAsync(
            string city,
            DateTimeOffset from,
            DateTimeOffset to)
    {
        _logger.LogInformation(
            "Requesting historical data for {City} " +
            "from {From} to {To}",
            city, from, to);

        var stopwatch = Stopwatch.StartNew();

        try
        {
            var result = await _inner
                .GetHistoricalDataAsync(city, from, to);

            stopwatch.Stop();
            _logger.LogInformation(
                "Historical data for {City} retrieved " +
                "({Count} records) in {ElapsedMs}ms",
                city,
                result.Count,
                stopwatch.ElapsedMilliseconds);

            return result;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(
                ex,
                "Historical data request for {City} " +
                "failed after {ElapsedMs}ms",
                city,
                stopwatch.ElapsedMilliseconds);

            throw;
        }
    }
}

The logging proxy captures both success and failure paths with timing. Notice it uses structured logging with named placeholders rather than string interpolation -- this ensures log aggregation tools can index by City, ElapsedMs, and Count fields. The proxy doesn't suppress exceptions; it logs them and rethrows. Each concern remains isolated. If you later need to swap your logging framework or add distributed tracing, you modify only this proxy. This isolation of behavior per class is the same principle that drives the observer pattern and the strategy pattern -- each behavioral variation gets its own class.

Composing Proxies with Dependency Injection

The real power of this proxy pattern example in C# emerges when you compose the proxies into a layered chain through DI. The outermost proxy is what consumers resolve. Each layer wraps the next:

using System.Diagnostics;

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMemoryCache();

builder.Services.AddHttpClient<HttpWeatherService>(
    client =>
    {
        client.BaseAddress = new Uri(
            "https://api.weather-provider.example");
        client.Timeout = TimeSpan.FromSeconds(10);
    });

builder.Services.AddSingleton<IWeatherService>(sp =>
{
    var httpService = sp
        .GetRequiredService<HttpWeatherService>();

    var logger = sp
        .GetRequiredService<
            ILogger<LoggingWeatherProxy>>();

    var cache = sp
        .GetRequiredService<IMemoryCache>();

    IWeatherService chain = httpService;

    chain = new LoggingWeatherProxy(chain, logger);

    chain = new RateLimitingWeatherProxy(
        chain,
        maxConcurrentRequests: 5,
        maxRequestsPerSecond: 10);

    chain = new CachingWeatherProxy(
        chain,
        cache,
        forecastTtl: TimeSpan.FromMinutes(10),
        historicalTtl: TimeSpan.FromHours(1));

    return chain;
});

var app = builder.Build();

app.MapGet("/forecast/{city}", async (
    string city,
    IWeatherService weatherService) =>
{
    var forecast = await weatherService
        .GetForecastAsync(city);
    return Results.Ok(forecast);
});

app.MapGet("/history/{city}", async (
    string city,
    DateTimeOffset from,
    DateTimeOffset to,
    IWeatherService weatherService) =>
{
    var data = await weatherService
        .GetHistoricalDataAsync(city, from, to);
    return Results.Ok(data);
});

app.Run();

The chain builds from inside out. HttpWeatherService is the innermost layer -- it talks to the network. LoggingWeatherProxy wraps it, recording every call with timing. RateLimitingWeatherProxy wraps the logging layer, throttling requests before they even reach the logger. CachingWeatherProxy sits on the outside, short-circuiting the entire chain on cache hits. When a consumer resolves IWeatherService, they get the caching proxy, which delegates to rate limiting, which delegates to logging, which delegates to the real HTTP service.

This composition strategy means the endpoints know nothing about caching, rate limiting, or logging. They depend on IWeatherService and get whatever proxy chain the DI container provides. Swapping the order, removing a layer, or adding a new proxy type requires changing only the registration block.

Adding a New Proxy Without Modifying Existing Code

Suppose you need a circuit breaker proxy that stops forwarding requests after a threshold of consecutive failures. You write one new class:

public sealed class CircuitBreakerWeatherProxy
    : IWeatherService
{
    private readonly IWeatherService _inner;
    private readonly int _failureThreshold;
    private readonly TimeSpan _openDuration;
    private int _consecutiveFailures;
    private DateTimeOffset _circuitOpenedAt;
    private bool _isOpen;
    private readonly object _lock = new();

    public CircuitBreakerWeatherProxy(
        IWeatherService inner,
        int failureThreshold,
        TimeSpan openDuration)
    {
        _inner = inner;
        _failureThreshold = failureThreshold;
        _openDuration = openDuration;
    }

    public async Task<WeatherForecast> GetForecastAsync(
        string city)
    {
        EnsureCircuitAllowsRequest();

        try
        {
            var result = await _inner
                .GetForecastAsync(city);
            RecordSuccess();
            return result;
        }
        catch
        {
            RecordFailure();
            throw;
        }
    }

    public async Task<IReadOnlyList<HistoricalWeatherData>>
        GetHistoricalDataAsync(
            string city,
            DateTimeOffset from,
            DateTimeOffset to)
    {
        EnsureCircuitAllowsRequest();

        try
        {
            var result = await _inner
                .GetHistoricalDataAsync(city, from, to);
            RecordSuccess();
            return result;
        }
        catch
        {
            RecordFailure();
            throw;
        }
    }

    private void EnsureCircuitAllowsRequest()
    {
        lock (_lock)
        {
            if (!_isOpen)
            {
                return;
            }

            if (DateTimeOffset.UtcNow - _circuitOpenedAt
                >= _openDuration)
            {
                _isOpen = false;
                _consecutiveFailures = 0;
                return;
            }

            throw new InvalidOperationException(
                "Circuit breaker is open. " +
                "Requests are temporarily blocked.");
        }
    }

    private void RecordSuccess()
    {
        lock (_lock)
        {
            _consecutiveFailures = 0;
        }
    }

    private void RecordFailure()
    {
        lock (_lock)
        {
            _consecutiveFailures++;

            if (_consecutiveFailures >= _failureThreshold)
            {
                _isOpen = true;
                _circuitOpenedAt = DateTimeOffset.UtcNow;
            }
        }
    }
}

No existing proxy was touched. No interface changed. You slot it into the DI chain wherever it makes sense -- typically between the rate limiter and the logging proxy:

chain = new LoggingWeatherProxy(chain, logger);

chain = new CircuitBreakerWeatherProxy(
    chain,
    failureThreshold: 3,
    openDuration: TimeSpan.FromSeconds(30));

chain = new RateLimitingWeatherProxy(
    chain,
    maxConcurrentRequests: 5,
    maxRequestsPerSecond: 10);

chain = new CachingWeatherProxy(
    chain,
    cache,
    forecastTtl: TimeSpan.FromMinutes(10),
    historicalTtl: TimeSpan.FromHours(1));

This is the open/closed principle in action. The system is open for extension -- add new proxy types freely -- and closed for modification -- existing proxies, the interface, and the real subject never change. The proxy pattern gives you this extensibility at the architectural level while the adapter pattern provides similar interface-boundary benefits when integrating with external systems.

Error Handling and Fallback Behavior

Each proxy in the chain can handle errors differently depending on its responsibility. The logging proxy catches exceptions to record them and rethrows. The circuit breaker proxy counts failures and eventually blocks requests entirely. But what about returning fallback data when the real service is unavailable?

You could add fallback logic into the caching proxy by returning stale cached data when the inner service throws:

public async Task<WeatherForecast> GetForecastAsync(
    string city)
{
    var cacheKey = $"forecast:{city.ToLowerInvariant()}";

    try
    {
        if (_cache.TryGetValue(
            cacheKey, out WeatherForecast? cached))
        {
            return cached!;
        }

        var result = await _inner.GetForecastAsync(city);
        _cache.Set(cacheKey, result, _forecastTtl);
        return result;
    }
    catch (Exception) when (
        _cache.TryGetValue(
            cacheKey, out WeatherForecast? stale))
    {
        return stale!;
    }
}

The when clause in the catch block only triggers if stale data exists in the cache. If there's no stale data, the exception propagates up the chain. This gives you graceful degradation -- callers get slightly outdated data instead of an error page when the weather API is temporarily unreachable.

Frequently Asked Questions

What is the difference between the proxy pattern and the decorator pattern in C#?

Both patterns wrap an object implementing the same interface, and the implementation structure is nearly identical. The difference is intent. The proxy pattern controls access to the real subject -- think caching, rate limiting, lazy loading, or access control. The decorator pattern adds new behavior or responsibilities. In practice, especially in C#, the line blurs significantly. The caching proxy in this article could reasonably be called a caching decorator. Focus on the problem you're solving rather than arguing over which label applies. For a deep dive on decorators specifically, see this guide on the decorator design pattern in C#.

How do I decide the order of proxies in the chain?

Put the proxy that short-circuits most often on the outside. In this proxy pattern example, caching sits outermost because cache hits skip the entire chain -- no rate-limiting check, no logging, no HTTP call. Rate limiting goes next to prevent overloading the downstream service. Logging sits closest to the real subject so it captures actual HTTP call timing. Circuit breaking fits between rate limiting and logging to prevent cascading failures.

Can I use the proxy pattern with IHttpClientFactory?

Yes. Register HttpWeatherService with AddHttpClient<HttpWeatherService>() so it benefits from IHttpClientFactory's connection pooling and handler pipeline. The proxy chain wraps the fully-configured HttpWeatherService instance that the DI container creates. The proxies themselves don't interact with HttpClient directly -- they only see IWeatherService.

Is the proxy pattern the right choice for cross-cutting concerns?

The proxy pattern works well for cross-cutting concerns that need to be composed and reordered at the DI level. For concerns that apply uniformly across all HTTP calls regardless of the service interface, middleware or delegating handlers might be a better fit. Use proxies when the concern is specific to your service contract -- like caching responses for particular methods or rate limiting by operation type.

How do I test individual proxies in this chain?

Test each proxy in isolation by passing a mock or stub IWeatherService as the inner service. For the caching proxy, verify that the second call for the same city doesn't invoke the inner service. For the rate-limiting proxy, verify that exceeding the token bucket blocks subsequent calls. For the logging proxy, verify that the logger receives the expected structured messages. Each proxy is independently testable because each depends only on IWeatherService and its own specific dependencies.

Does adding proxies affect performance?

Each proxy adds a layer of method dispatch, but the overhead is negligible compared to the HTTP calls they wrap. The caching proxy actually improves performance dramatically by eliminating network round-trips. The rate-limiting proxy introduces intentional latency to protect the downstream service -- that's a feature, not a cost. Profile before optimizing, and focus on the real bottleneck, which is almost always the network call in the real subject.

How does this proxy pattern example relate to middleware in ASP.NET Core?

ASP.NET Core middleware and the proxy pattern share the same structural concept -- a chain of components where each can short-circuit or pass through to the next. Middleware operates at the HTTP request level across all endpoints. Proxies operate at the service interface level for a specific contract. Use middleware for concerns like authentication and CORS. Use the proxy pattern for concerns tied to a specific service's behavior, like caching weather forecasts with domain-aware cache keys.

Wrapping Up This Proxy Pattern Real-World Example

This proxy pattern real-world example in C# demonstrates the pattern solving a genuine architectural problem -- layering caching, rate limiting, logging, and circuit breaking onto an API client without any single class knowing about the others. Each proxy implements IWeatherService, wraps an inner IWeatherService, and handles exactly one concern. The DI registration composes them into a chain, and consumers resolve a single interface without caring how many proxies sit between them and the real HTTP service.

The proxy pattern scales cleanly because adding a new cross-cutting concern means writing one new class and inserting it into the chain. No existing code changes. No interface modifications. No conditional branches growing inside a single mega-class. Take this weather service example, swap in your actual API client, and you've got a production-ready infrastructure layer that's testable, composable, and easy to reason about.

When to Use Proxy Pattern in C#: Decision Guide with Examples

Discover when to use proxy pattern in C# with decision criteria for virtual, protection, and caching proxies plus guidance on simpler alternatives.

Proxy Design Pattern in C#: Complete Guide with Examples

Master the proxy design pattern in C# with practical examples showing virtual proxies, protection proxies, caching proxies, and lazy loading implementations.

How to Implement Proxy Pattern in C#: Step-by-Step Guide

Learn how to implement the proxy pattern in C# with step-by-step examples covering virtual proxies, protection proxies, and DI registration.

An error has occurred. This application may no longer respond until reloaded. Reload