Proxy pattern in C# - an easy way to extend production code

According to open-close principle, code should be open for extensions but closed for modifications. In other words, you should avoid modifying production code unless it's really necessary. A proxy design pattern is one of the solutions to this problem.

Proxy pattern in C# - an easy way to extend production code

Examining production code

For demo purposes, let's say that we have a web app and one of its modules is responsible for transactions. To clarify, it doesn't really matter what the transaction is. We assume that it's a simple object with Amount property and can be processed somehow. Below you can see ASP.NET Core 6 code.

public class Startup{    public void ConfigureServices(IServiceCollection services)    {        services.AddTransient<ITransactionProccessor, TransactionProccessor>();    }
    public void Configure(IApplicationBuilder app)    {        app.UseMiddleware<ClientCodeMiddleware>();    }}
public class ClientCodeMiddleware{    public ClientCodeMiddleware(RequestDelegate next) {}
    public async Task InvokeAsync(HttpContext context, ITransactionProccessor proccessor)    {        var transactionItem = new TransactionItem { Amount = 100 };        var message = await proccessor.ProcessAsync(transactionItem);        await context.Response.WriteAsync(message);    }}
public interface ITransactionProccessor{    Task<string> ProcessAsync(TransactionItem transactionItem);}
public class TransactionProccessor : ITransactionProccessor{    public async Task<string> ProcessAsync(TransactionItem transactionItem)    {        // complex code responsible for processing...        return "Transaction item has been processed!";    }}
public class TransactionItem{    public decimal Amount { get; set; }}
public enum AcceptanceLevel{    Level1, Level2, Level3}
C#

Then we execute this code, we get successful results in the web browser.

Proxy design pattern - transaction item 1

Using proxy pattern

Now we want to extend this code but we should not modify TransactionProccessor. First of all, in order to achieve this let's create a proxy class. It could also be named wrapper class.

public class UserAwareTransactionProccessor : ITransactionProccessor{    private readonly ITransactionProccessor _transactionProccessor;
    public UserAwareTransactionProccessor(ITransactionProccessor transactionProccessor)    {        _transactionProccessor = transactionProccessor;    }
    public async Task ProcessAsync(TransactionItem transactionItem)    {        await _transactionProccessor.ProcessAsync(transactionItem);    }}
C#

Currently, it does no more than delegate to internal ITransactionProccessor which is wrapped. But at this stage, you might have noticed that it enables us to write some extra logic. We could do it before or after the execution of the wrapped method. Let's say that only users with AcceptanceLevel set to Level2 can invoke this action. We need some kind of user provider to get this data.

public class UserAwareTransactionProccessor : ITransactionProccessor{    private readonly ITransactionProccessor _transactionProccessor;    private readonly IUserContext _userContext;
    public UserAwareTransactionProccessor(        ITransactionProccessor transactionProccessor,        IUserContext userContext)    {        _transactionProccessor = transactionProccessor;        _userContext = userContext;    }
    public async Task<string> ProcessAsync(TransactionItem transactionItem)    {        if (_userContext.CurrentUser.AcceptanceLevel < AcceptanceLevel.Level2)        {            return "Current user has not sufficient privileges";        }
        return await _transactionProccessor.ProcessAsync(transactionItem);    }}
public interface IUserContext{    AppUser CurrentUser { get; }}
public class UserContext : IUserContext{    public AppUser CurrentUser => new() { AcceptanceLevel = AcceptanceLevel.Level1 };}
C#

For simplicity, we hardcoded AppUser which is returned by UserContext. In the real world, that would be, for instance, a user taken from a cookie stored in a request. Then we check if AppUser has sufficient AcceptanceLevel. The last step is to register UserContext and replace implementation for ITransactionProccessor in the dependency container.

Registering proxy class in dependency container

In order to register proxy class for TransactionProccessor, firstly we register TransactionProccessor itself. Then we register UserAwareTransactionProccessor under ITransactionProccessor interface. In the factory method, we resolve needed services and create instances of UserAwareTransactionProccessor class.

public void ConfigureServices(IServiceCollection services){    services.AddTransient<IUserContext, UserContext>();    services.AddTransient<TransactionProccessor>();    services.AddTransient<ITransactionProccessor, UserAwareTransactionProccessor>(serviceProvider =>    {        var userContext = serviceProvider.GetRequiredService<IUserContext>();        var transactionProccessor = serviceProvider.GetRequiredService<TransactionProccessor>();        return new UserAwareTransactionProccessor(transactionProccessor, userContext);    });}
C#

Now we can execute our code and receive the expected result.

And that’s it 🎉! As you can see, we registered UserAwareTransactionProccessor by creating an instance of this class and resolving needed dependencies. In this case that was UserContext and wrapped TransactionProccessor. Depending on the container you use, proxy registration might be different. ASP.NET Core provides a simple dependency container. In comparison to third-party libraries (like Autofac or Ninject), it does not have advanced options. For instance, some libraries enable conditional injection and give us context object with more details about the current request.

You can find the source code used in this post here.

Conclusion

The proxy pattern is quite easy to use and enables us to add some functionality to living code with respect to the open-closed principle. In case of emergency, we can always replace implementation on the container level. We revert this way all our changes.

Unfortunately, the proxy pattern is not always the best option. Imagine that after some time someone adds one or more additional proxy class that wraps UserAwareTransactionProccessor. As a result, you can end up with many layers that depend on one another. Debugging such cases is usually a living nightmare. Depending on the architecture of your code, it’s worth remembering not to overuse this pattern.

Comments

Add comment
The comments system is based on GitHub Issues API. Once you add your comment to the linked issue on my GitHub repository, it will show up below.
© brokul.dev 2024