StructuralC#verifiedVerified
Bridge Pattern in C#
Decouples an abstraction from its implementation so that the two can vary independently.
How to Implement the Bridge Pattern in C#
1Step 1: Implementation interface (the "bridge")
public interface IRenderer
{
string RenderCircle(double radius);
string RenderSquare(double side);
}2Step 2: Concrete implementations
public class SvgRenderer : IRenderer
{
public string RenderCircle(double radius) =>
$"<circle r=\"{radius}\"/>";
public string RenderSquare(double side) =>
$"<rect width=\"{side}\" height=\"{side}\"/>";
}
public class CanvasRenderer : IRenderer
{
public string RenderCircle(double radius) =>
$"ctx.arc(0, 0, {radius})";
public string RenderSquare(double side) =>
$"ctx.fillRect(0, 0, {side}, {side})";
}3Step 3: Abstraction uses the implementation via the bridge
public abstract class Shape(IRenderer renderer)
{
protected IRenderer Renderer => renderer;
public abstract string Draw();
}
public class Circle(IRenderer renderer, double radius)
: Shape(renderer)
{
public override string Draw() => Renderer.RenderCircle(radius);
}
public class Square(IRenderer renderer, double side)
: Shape(renderer)
{
public override string Draw() => Renderer.RenderSquare(side);
}
// Usage:
// var circle = new Circle(new SvgRenderer(), 10);
// Console.WriteLine(circle.Draw()); // <circle r="10"/>using Microsoft.Extensions.Logging;
// [step] Implementation: message transport (the "bridge")
public interface IMessageTransport : IAsyncDisposable
{
string TransportName { get; }
Task ConnectAsync(CancellationToken ct = default);
Task SendAsync(byte[] payload, string routingKey,
CancellationToken ct = default);
Task<byte[]?> ReceiveAsync(string queueName,
CancellationToken ct = default);
}
// [step] Concrete transports
public class InMemoryTransport(
ILogger<InMemoryTransport> logger) : IMessageTransport
{
private readonly Dictionary<string, Queue<byte[]>> _queues = [];
public string TransportName => "InMemory";
public Task ConnectAsync(CancellationToken ct = default)
{
logger.LogInformation("InMemory transport connected");
return Task.CompletedTask;
}
public Task SendAsync(byte[] payload, string routingKey,
CancellationToken ct = default)
{
if (!_queues.TryGetValue(routingKey, out var queue))
{
queue = new Queue<byte[]>();
_queues[routingKey] = queue;
}
queue.Enqueue(payload);
return Task.CompletedTask;
}
public Task<byte[]?> ReceiveAsync(string queueName,
CancellationToken ct = default)
{
if (_queues.TryGetValue(queueName, out var queue)
&& queue.Count > 0)
return Task.FromResult<byte[]?>(queue.Dequeue());
return Task.FromResult<byte[]?>(null);
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
public class TcpTransport(
string host, int port,
ILogger<TcpTransport> logger) : IMessageTransport
{
public string TransportName => $"TCP({host}:{port})";
public async Task ConnectAsync(CancellationToken ct = default)
{
logger.LogInformation("Connecting to {Host}:{Port}", host, port);
await Task.Delay(50, ct);
}
public async Task SendAsync(byte[] payload, string routingKey,
CancellationToken ct = default)
{
logger.LogDebug("TCP send {Bytes} bytes to {Key}",
payload.Length, routingKey);
await Task.Delay(5, ct);
}
public async Task<byte[]?> ReceiveAsync(string queueName,
CancellationToken ct = default)
{
await Task.Delay(5, ct);
return null;
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
// [step] Abstraction: messaging patterns built on any transport
public abstract class MessageBroker(
IMessageTransport transport, ILogger logger)
{
protected IMessageTransport Transport => transport;
protected ILogger Logger => logger;
public async Task InitializeAsync(CancellationToken ct = default)
{
await transport.ConnectAsync(ct);
Logger.LogInformation("Broker initialized on {Transport}",
transport.TransportName);
}
public abstract Task PublishAsync<T>(
string topic, T message, CancellationToken ct = default);
public abstract Task<T?> ConsumeAsync<T>(
string topic, CancellationToken ct = default);
}
// [step] Refined abstraction: pub-sub broker
public class PubSubBroker(
IMessageTransport transport,
ILogger<PubSubBroker> logger) : MessageBroker(transport, logger)
{
public override async Task PublishAsync<T>(
string topic, T message, CancellationToken ct = default)
{
var payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(message);
await Transport.SendAsync(payload, topic, ct);
Logger.LogDebug("Published to {Topic}", topic);
}
public override async Task<T?> ConsumeAsync<T>(
string topic, CancellationToken ct = default) where T : default
{
var payload = await Transport.ReceiveAsync(topic, ct);
if (payload is null) return default;
return System.Text.Json.JsonSerializer.Deserialize<T>(payload);
}
}Bridge Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Bridge Pattern in the Real World
“A TV remote (abstraction) works with any brand of television (implementation) because both sides communicate through an agreed IR protocol (the bridge). Samsung and LG can redesign their TVs, and universal remote makers can add new button layouts—neither side needs to know about the other’s internal design, only the shared protocol.”