
Structural Design Pattern - Adapter
Author - Abdul Rahman (Content Writer)
Design Pattern
12 Articles
Table of Contents
What we gonna do?
Ever tried to fit a square peg into a round hole? That's exactly what the Adapter Design Pattern solves in software development. In this article, let's learn about the Adapter Pattern in C# and see how it elegantly bridges incompatible interfaces to make them work together seamlessly.
The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. Think of it as a translator that helps two parties communicate when they speak different languages.
Why we gonna do?
The Adapter Pattern becomes essential when you need to integrate with third-party libraries, legacy systems, or external APIs that don't match your application's interface expectations. Instead of modifying existing code (which might not even be possible), you create an adapter that acts as a bridge between incompatible interfaces.
Here's why this pattern is crucial for modern .NET development:
Cross-Platform Compatibility: When building applications that need to work across Web API, MAUI, and Blazor WASM, you often encounter platform-specific services that can't be directly shared. The Adapter pattern provides a unified interface that works consistently across all platforms.
Third-Party Integration: External libraries rarely conform to your application's architecture. Adapters let you maintain clean architecture while still leveraging powerful third-party functionality.
Legacy System Integration: When modernizing applications, adapters help you gradually migrate from old systems without breaking existing functionality.
How we gonna do?
Let's implement the Adapter pattern using a real-world scenario where we need to access user context information across different .NET platforms. We'll create an adapter that wraps IHttpContextAccessor to provide a consistent interface for user operations.
ACSII Diagram: Adapter Pattern
The following ASCII class diagram illustrates the structure of the Adapter pattern as implemented in this article:
+------------------------+ +--------------------------+
| IUserContextAdapter |<----------| UserContextAdapter |
+------------------------+ +--------------------------+
| +GetCurrentUserId() | | -_httpContextAccessor |
| +GetCurrentUserRole() | | +GetCurrentUserId() |
+------------------------+ | +GetCurrentUserRole() |
+--------------------------+
|
v
+-----------------------------+
| IHttpContextAccessor |
+-----------------------------+
+------------------------+
| IUserContextAdapter |<----------+--------------------------+
+------------------------+ | MockUserContextAdapter |
| +GetCurrentUserId() | +--------------------------+
| +GetCurrentUserRole() | | +GetCurrentUserId() |
+------------------------+ | +GetCurrentUserRole() |
+--------------------------+
Step 1: Define the Target Interface
First, let's define the interface that our application expects to work with:
public interface IUserContextAdapter
{
string GetCurrentUserId();
string GetCurrentUserRole();
}
This interface defines all the user context operations we need across our application, regardless of the underlying platform.
Step 2: Create the Adapter Implementation
Now, let's implement the adapter that wraps IHttpContextAccessor and adapts it to our expected interface:
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
public class UserContextAdapter : IUserContextAdapter
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserContextAdapter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor ??
throw new ArgumentNullException(nameof(httpContextAccessor));
}
public string GetCurrentUserId()
{
return _httpContextAccessor.HttpContext?.User
.FindFirst("sub")?.Value ?? string.Empty;
}
public string GetCurrentUserRole()
{
return _httpContextAccessor.HttpContext?.User
.FindFirst("role")?.Value ?? string.Empty;
}
}
This adapter encapsulates all the complexity of working with HttpContext and provides a clean, testable interface for accessing user information.
Step 3: Register the Adapter with Dependency Injection
Register the adapter in your DI container so it can be injected throughout your application:
// In Program.cs or Startup.cs
var builder = WebApplication.CreateBuilder(args);
// Register the adaptee (what we're adapting)
builder.Services.AddHttpContextAccessor();
// Register the adapter
builder.Services.AddScoped<IUserContextAdapter, UserContextAdapter>();
var app = builder.Build();
Step 4: Use the Adapter in Your Services
Now you can inject and use the adapter in your services, controllers, or other components:
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IUserContextAdapter _userContext;
public UserController(IUserContextAdapter userContext)
{
_userContext = userContext;
}
[HttpGet("profile")]
public async Task<IActionResult> GetUserProfile()
{
var userId = _userContext.GetCurrentUserId();
var userRole = _userContext.GetCurrentUserRole();
var profile = new UserProfile
{
Id = userId,
Name = userName,
Role = userRole
};
return Ok(profile);
}
[HttpGet("admin-only")]
public async Task<IActionResult> GetAdminData()
{
if (!_userContext.HasRole("Admin"))
{
return Forbid("Insufficient permissions");
}
// Admin-only logic here
return Ok("Admin data");
}
}
Step 5: Create Platform-Specific Adapters
For platforms like MAUI or Blazor WASM where IHttpContextAccessor isn't available, you can create alternative adapters:
// For MAUI applications
public class MauiUserContextAdapter : IUserContextAdapter
{
private readonly ISecureStorage _secureStorage;
public MauiUserContextAdapter(ISecureStorage secureStorage)
{
_secureStorage = secureStorage;
}
public string GetCurrentUserId()
{
return _secureStorage.GetAsync("user_id").GetAwaiter().GetResult() ?? string.Empty;
}
public string GetCurrentUserRole()
{
return _secureStorage.GetAsync("user_role").GetAwaiter().GetResult() ?? string.Empty;
}
// Implement other methods based on platform-specific storage
// ...
}
// For Blazor WASM
public class BlazorWasmUserContextAdapter : IUserContextAdapter
{
private readonly IJSRuntime _jsRuntime;
public BlazorWasmUserContextAdapter(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public string GetCurrentUserId()
{
return _jsRuntime.InvokeAsync<string>("localStorage.getItem", "user_id")
.GetAwaiter().GetResult() ?? string.Empty;
}
// Implement other methods using localStorage or sessionStorage
// ...
}
Step 6: Testing the Adapter
The adapter pattern makes testing much easier by providing a clear interface to mock:
[Test]
public async Task GetUserProfile_ReturnsCorrectProfile()
{
// Arrange
var mockUserContext = new Mock<IUserContextAdapter>();
mockUserContext.Setup(x => x.GetCurrentUserId()).Returns("123");
mockUserContext.Setup(x => x.GetCurrentUserRole()).Returns("Admin");
var controller = new UserController(mockUserContext.Object);
// Act
var result = await controller.GetUserProfile();
// Assert
var okResult = result as OkObjectResult;
var profile = okResult?.Value as UserProfile;
Assert.That(profile?.Id, Is.EqualTo("123"));
Assert.That(profile?.Role, Is.EqualTo("Admin"));
}
Interactive Demo
Here's a working demonstration of the Adapter pattern in action. Try the different buttons to see how multiple adapters can provide the same interface while working with different underlying implementations:
Pattern Benefits
- Both adapters implement the same interface
- Client code works with either adapter without changes
- Easy to swap implementations for different platforms
- Clean separation between interface and implementation
- Simplified testing with mock implementations
Summary
The Adapter Design Pattern is a powerful tool for creating flexible and maintainable applications. By wrapping incompatible interfaces like IHttpContextAccessor with adapters, we achieve several key benefits:
Cross-Platform Consistency: The same IUserContextAdapter interface works across Web API, MAUI, and Blazor applications, with platform-specific implementations handling the details.
Improved Testability: Adapters provide clean interfaces that are easy to mock and test, leading to more reliable unit tests.
Separation of Concerns: Business logic remains focused on domain operations while adapters handle the technical details of accessing platform-specific services.
Remember: the Adapter pattern shines when you need to integrate with external systems or create unified interfaces across different platforms. It's the bridge that connects incompatible worlds, making your code more modular and maintainable.