StructuralC#verifiedVerified
Adapter Pattern in C#
Converts the interface of a class into another interface that clients expect, allowing incompatible interfaces to work together.
How to Implement the Adapter Pattern in C#
1Step 1: Target interface the client expects
public interface IJsonLogger
{
void Log(string json);
}2Step 2: Adaptee with an incompatible interface
public class XmlLogger
{
public void WriteXml(string xml) =>
Console.WriteLine($"XML: {xml}");
}3Step 3: Adapter wraps the adaptee to match the target interface
public class XmlToJsonAdapter(XmlLogger xmlLogger) : IJsonLogger
{
public void Log(string json)
{
// Convert JSON to XML (simplified)
var xml = $"<log>{json}</log>";
xmlLogger.WriteXml(xml);
}
}4Step 4: Client works with the target interface
public class Application(IJsonLogger logger)
{
public void DoWork() =>
logger.Log("{\"action\": \"doWork\", \"status\": \"ok\"}");
}
// Usage:
// var adapted = new XmlToJsonAdapter(new XmlLogger());
// var app = new Application(adapted);
// app.DoWork();using Microsoft.Extensions.Logging;
// [step] Target interface for a unified cache
public interface ICacheService
{
Task<T?> GetAsync<T>(string key, CancellationToken ct = default);
Task SetAsync<T>(string key, T value, TimeSpan? ttl = null,
CancellationToken ct = default);
Task RemoveAsync(string key, CancellationToken ct = default);
Task<bool> ExistsAsync(string key, CancellationToken ct = default);
}
// [step] Legacy adaptee: in-memory dictionary cache
public class LegacyMemoryCache
{
private readonly Dictionary<string, (object Value, DateTime? Expiry)> _store = [];
public object? Retrieve(string key)
{
if (!_store.TryGetValue(key, out var entry)) return null;
if (entry.Expiry.HasValue && entry.Expiry < DateTime.UtcNow)
{
_store.Remove(key);
return null;
}
return entry.Value;
}
public void Store(string key, object value, int? ttlSeconds = null) =>
_store[key] = (value, ttlSeconds.HasValue
? DateTime.UtcNow.AddSeconds(ttlSeconds.Value) : null);
public void Evict(string key) => _store.Remove(key);
public bool Contains(string key) => _store.ContainsKey(key);
}
// [step] Adapter with async wrapping, serialization, and logging
public sealed class LegacyCacheAdapter(
LegacyMemoryCache legacyCache,
ILogger<LegacyCacheAdapter> logger) : ICacheService
{
public Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var value = legacyCache.Retrieve(key);
if (value is null)
{
logger.LogDebug("Cache miss: {Key}", key);
return Task.FromResult<T?>(default);
}
logger.LogDebug("Cache hit: {Key}", key);
return Task.FromResult((T?)value);
}
public Task SetAsync<T>(string key, T value, TimeSpan? ttl = null,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var ttlSeconds = ttl.HasValue ? (int?)ttl.Value.TotalSeconds : null;
legacyCache.Store(key, value!, ttlSeconds);
logger.LogDebug("Cache set: {Key} (TTL: {TTL})", key, ttl);
return Task.CompletedTask;
}
public Task RemoveAsync(string key, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
legacyCache.Evict(key);
logger.LogDebug("Cache remove: {Key}", key);
return Task.CompletedTask;
}
public Task<bool> ExistsAsync(string key, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(legacyCache.Contains(key));
}
}Adapter Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Adapter Pattern in the Real World
“A travel power adapter lets your American laptop plug (client) work in a European wall socket (adaptee) without modifying either. The adapter speaks both “languages”, translating the two-pin plug to the two-round-pin socket, making them interoperable without any changes on either side.”