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:
Here are the participants in the Proxy pattern:
Subject
defines the common interface for both
andRealSubject
Proxy
so that you can substitute the
by theRealSubject
Proxy
.RealSubject
defines the real object that theProxy
substitutes.Proxy
maintains a
object so that it can call the methods of theRealSubject
when needed. Before or after calling the methods of theRealSubject
object, theRealSubject
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 reprehenderit
Code 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
that implements the JSONPlaceholderAPI
IPostAPI
interface. The
uses the JSONPlaceholderAPI
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.