C# Proxy

Summary: in this tutorial, you’ll learn about the C# Proxy design pattern and how to use a proxy object to control access to a real object.

Introduction to the C# Proxy design pattern

The Proxy pattern is a structural design pattern that allows you to use a proxy object to control access to an actual object and to provide additional functionality without modifying the actual object.

For example, the proxy object may add caching, logging, or authorization service to the actual object. In some cases, the actual object creation is expensive, the proxy object can help improve the performance by delaying the creation of the actual object until it is actually needed.

The following UML diagram illustrates the Proxy design pattern:

C# Proxy

Here are the participants in the Proxy pattern:

  • Subject defines the common interface for both RealSubject and Proxy so that you can substitute the RealSubject by the Proxy.
  • RealSubject defines the real object that the Proxy substitutes.
  • Proxy maintains a RealSubject object so that it can call the methods of the RealSubject when needed. Before or after calling the methods of the RealSubject object, the Proxy may add more functionality such as caching or logging.

C# Proxy pattern example

This C# program demonstrates how to use the Proxy pattern to cache API requests using HttpClient:

using System.Text.Json;

namespace ProxyPattern;

public class Post
{
    public int UserId
    {
        get; set;
    }
    public int Id
    {
        get; set;
    }
    public string? Title
    {
        get; set;
    }
    public string? Body
    {
        get; set;
    }

    public override string ToString() => $"<{Id}> - {Title}";
}

public interface IPostAPI
{
    Task<Post?> GetPost(int postId);
}

public class JSONPlaceholderAPI : IPostAPI
{
    private readonly HttpClient _httpClient;

    public JSONPlaceholderAPI()
    {
        _httpClient = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var response = await _httpClient.GetAsync($"https://jsonplaceholder.typicode.com/posts/{postId}");

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"Failed to retrieve post {postId} from API: {response.ReasonPhrase}");
        }

        var json = await response.Content.ReadAsStringAsync();

        var options = new JsonSerializerOptions()
        {
            PropertyNameCaseInsensitive = true
        };

        var post = JsonSerializer.Deserialize<Post?>(json, options);
        return post;
    }
}

public class JSONPlaceholderAPIProxy : IPostAPI
{
    private readonly IPostAPI _postAPI;
    private readonly Dictionary<int, Post> _postCache;

    public JSONPlaceholderAPIProxy(IPostAPI postAPI)
    {
        _postAPI = postAPI;
        _postCache = new Dictionary<int, Post>();
    }

    public async Task<Post?> GetPost(int postId)
    {
        if (_postCache.ContainsKey(postId))
        {
            Console.WriteLine($"Retrieving post {postId} from cache.");
            return _postCache[postId];
        }

        var post = await _postAPI.GetPost(postId);
        _postCache[postId] = post;
        return post;
    }
}


public static class Program
{
    public static async Task Main()
    {
        var api = new JSONPlaceholderAPI();
        var apiProxy = new JSONPlaceholderAPIProxy(api);

        // Get post 1
        var post1 = await apiProxy.GetPost(1);
        Console.WriteLine($"Post 1: {post1}");

        // Get post 1 again - should retrieve from cache
        var post1Cached = await apiProxy.GetPost(1);
        Console.WriteLine($"Post 1 (cached): {post1Cached}");
    }
}Code language: C# (cs)

Output:

Post 1: <1> - sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Retrieving post 1 from cache.
Post 1 (cached): <1> - sunt aut facere repellat provident occaecati excepturi optio reprehenderitCode language: plaintext (plaintext)

How it works.

First, define the Post class that has the UserId, Id, Title, and Body properties. The ToString() method returns the string representation of a post:

public class Post
{
    public int UserId
    {
        get; set;
    }
    public int Id
    {
        get; set;
    }
    public string? Title
    {
        get; set;
    }
    public string? Body
    {
        get; set;
    }

    public override string ToString() => $"<{Id}> - {Title}";
}Code language: C# (cs)

Next, define the IPostAPI interface that has a GetPost() method which returns a Post by PostId:

public interface IPostAPI
{
    Task<Post?> GetPost(int postId);
}Code language: C# (cs)

The IPostAPI serves as the Subject in the above UML diagram.

Then, define the JSONPlaceholderAPI that implements the IPostAPI interface. The JSONPlaceholderAPI uses the HttpClient to call API from https://jsonplaceholder.typicode.com/posts/1/, where 1 is the post id:

public class JSONPlaceholderAPI : IPostAPI
{
    private readonly HttpClient _httpClient;

    public JSONPlaceholderAPI()
    {
        _httpClient = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var response = await _httpClient.GetAsync($"https://jsonplaceholder.typicode.com/posts/{postId}");

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"Failed to retrieve post {postId} from API: {response.ReasonPhrase}");
        }

        var json = await response.Content.ReadAsStringAsync();

        var options = new JsonSerializerOptions()
        {
            PropertyNameCaseInsensitive = true
        };

        var post = JsonSerializer.Deserialize<Post?>(json, options);
        return post;
    }
}Code language: C# (cs)

The JSONPlaceholderAPI class serves as the RealSubject.

After that, define the JSONPlaceholderAPIProxy class that implements the IPostAPI. The JSONPlaceholderAPIProxy acts as a Proxy object of the JSONPlaceholderAPI. It caches a Post by postId so that if you request the same postId again, it’ll return the Post from the cache instead of calling the API:

public class JSONPlaceholderAPIProxy : IPostAPI
{
    private readonly IPostAPI _postAPI;
    private readonly Dictionary<int, Post> _postCache;

    public JSONPlaceholderAPIProxy(IPostAPI postAPI)
    {
        _postAPI = postAPI;
        _postCache = new Dictionary<int, Post>();
    }

    public async Task<Post?> GetPost(int postId)
    {
        if (_postCache.ContainsKey(postId))
        {
            Console.WriteLine($"Retrieving post {postId} from cache.");
            return _postCache[postId];
        }

        var post = await _postAPI.GetPost(postId);
        _postCache[postId] = post;
        return post;
    }
}Code language: C# (cs)

Finally, create the JSONPlaceholderAPI object and pass it to the constructor of the JSONPlaceholderAPIProxy object. The program requests the post with id 1 twice from the JSONPlaceholderAPIProxy object. Because of the caching, the second request does not call the API but returns the post from the cache.

public static class Program
{
    public static async Task Main()
    {
        var api = new JSONPlaceholderAPI();
        var apiProxy = new JSONPlaceholderAPIProxy(api);

        // Get post 1
        var post1 = await apiProxy.GetPost(1);
        Console.WriteLine($"Post 1: {post1}");

        // Get post 1 again - should retrieve from cache
        var post1Cached = await apiProxy.GetPost(1);
        Console.WriteLine($"Post 1 (cached): {post1Cached}");
    }
}Code language: C# (cs)

Summary

  • Use the Proxy design pattern to control access to an actual object and to extend its functionality without modifying the actual object directly.
Was this tutorial helpful ?