How to Implement Adapter Pattern in C#: Step-by-Step Guide
Working with incompatible interfaces is one of the most common friction points in real-world C# applications. You have a class that does exactly what you need, but its API doesn't match the interface your code expects. The adapter pattern solves this cleanly by introducing a wrapper that translates one interface into another. If you want to implement adapter pattern in C#, this guide walks you through the entire process from scratch with complete code examples.
Whether you're integrating a third-party library, bridging legacy code into a modern system, or swapping out infrastructure services behind a clean abstraction, knowing how to implement adapter pattern in C# keeps your code loosely coupled and easy to test. It's one of the most practical structural patterns in the big list of design patterns and a pattern you'll reach for regularly once you understand it.
In this step-by-step guide, we'll implement adapter pattern in C# by building a notification system adapter that wraps a third-party email client behind a clean domain interface. By the end, you'll have working examples covering target interfaces, adaptees, object adapters, dependency injection registration, and edge case handling.
Prerequisites
Before getting started, make sure you're familiar with these fundamentals:
- C# interfaces and classes: You'll define target interfaces and implement adapter classes. Understanding how interfaces enforce contracts is essential.
- Composition: When you implement adapter pattern in C#, composition is the preferred approach -- holding a reference to the adaptee rather than inheriting from it. This avoids tight coupling.
- Dependency injection basics: The final steps cover registering adapters with IServiceCollection. Familiarity with service registration will help.
- .NET 8 or later: The code examples use modern C# syntax. Any recent .NET SDK works.
Step 1: Define the Target Interface
The first step to implement adapter pattern in C# is defining the target interface. This is the contract your application code depends on. It represents what your domain needs, not what any specific external library provides. Think of it as the boundary between your application and the outside world.
For our notification example, the application needs to send messages:
public interface INotificationSender
{
NotificationResult Send(NotificationMessage message);
}
public sealed class NotificationMessage
{
public string Recipient { get; }
public string Subject { get; }
public string Body { get; }
public NotificationMessage(
string recipient,
string subject,
string body)
{
Recipient = recipient;
Subject = subject;
Body = body;
}
}
public sealed class NotificationResult
{
public bool Success { get; }
public string MessageId { get; }
public string ErrorDetails { get; }
public NotificationResult(
bool success,
string messageId,
string errorDetails = "")
{
Success = success;
MessageId = messageId;
ErrorDetails = errorDetails;
}
}
The interface is narrow and focused. One method, clear input and output types. This matters because every adapter you write must implement this interface when you implement adapter pattern in C#. A focused contract means less work per adapter and a cleaner abstraction. Your application code only ever depends on INotificationSender -- it never knows or cares which concrete email service is behind it.
Step 2: Identify the Adaptee
The adaptee is the existing class whose interface doesn't match your target. When you implement adapter pattern in C#, the adaptee is the class you're wrapping -- not modifying. This might be a third-party SDK, a legacy internal service, or any class with an API that doesn't align with your domain interface.
Here's a third-party email client we need to adapt:
public class ThirdPartyEmailClient
{
public string SendEmail(
string toAddress,
string emailSubject,
string htmlBody,
bool enableTracking)
{
// Simulates calling an external email API
Console.WriteLine(
$"[ThirdPartyEmailClient] Sending email " +
$"to '{toAddress}' with subject " +
$"'{emailSubject}'...");
string confirmationCode = Guid.NewGuid()
.ToString("N")[..8]
.ToUpperInvariant();
Console.WriteLine(
$"[ThirdPartyEmailClient] Email sent. " +
$"Confirmation: {confirmationCode}");
return confirmationCode;
}
}
Notice the mismatch. The ThirdPartyEmailClient takes four separate parameters instead of a single message object. It returns a string confirmation code instead of a NotificationResult. And it uses method names and parameter names that don't match our domain vocabulary. These are exactly the kinds of incompatibilities the adapter pattern resolves.
You don't modify the ThirdPartyEmailClient. You can't always change it -- maybe it's from a NuGet package, or maybe other parts of the system already depend on its existing API. That's exactly why you implement adapter pattern in C# -- the adapter wraps it instead.
Step 3: Create the Adapter Class Using Composition
This is the core step when you implement adapter pattern in C#. The adapter class implements the target interface and holds a reference to the adaptee. It translates calls from the target interface into calls the adaptee understands. This is called an object adapter because it uses composition rather than inheritance.
public class EmailNotificationAdapter : INotificationSender
{
private readonly ThirdPartyEmailClient _emailClient;
public EmailNotificationAdapter(
ThirdPartyEmailClient emailClient)
{
_emailClient = emailClient
?? throw new ArgumentNullException(
nameof(emailClient));
}
public NotificationResult Send(
NotificationMessage message)
{
string confirmationCode = _emailClient.SendEmail(
toAddress: message.Recipient,
emailSubject: message.Subject,
htmlBody: message.Body,
enableTracking: true);
return new NotificationResult(
success: true,
messageId: confirmationCode);
}
}
A few important things to notice:
- The adapter implements
INotificationSender, so it satisfies the target interface contract. - It holds a reference to
ThirdPartyEmailClientthrough its constructor. This is composition -- the adapter has an email client rather than being one. - The
Sendmethod translates the domainNotificationMessageinto the four parameters the email client expects, then wraps the string response into aNotificationResult. - The adapter is the only class that knows about the adaptee's API. Everything else in your application talks to
INotificationSender.
This composition-based approach aligns with the principle of inversion of control. Your high-level application code depends on the INotificationSender abstraction, not the concrete ThirdPartyEmailClient. The adapter bridges the gap.
Step 4: Register with Dependency Injection
Once you have a working adapter, the next step to implement the adapter pattern in C# for production use is registering it with your DI container. This keeps your composition root clean and lets the rest of your application request INotificationSender without knowing which adapter is behind it.
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<ThirdPartyEmailClient>();
services.AddSingleton<INotificationSender>(sp =>
{
var emailClient = sp
.GetRequiredService<ThirdPartyEmailClient>();
return new EmailNotificationAdapter(emailClient);
});
var provider = services.BuildServiceProvider();
var sender = provider
.GetRequiredService<INotificationSender>();
If your adapter's constructor takes only the adaptee (and no other configuration), you can simplify this to a direct registration:
var services = new ServiceCollection();
services.AddSingleton<ThirdPartyEmailClient>();
services.AddSingleton<INotificationSender,
EmailNotificationAdapter>();
var provider = services.BuildServiceProvider();
var sender = provider
.GetRequiredService<INotificationSender>();
The DI container resolves the ThirdPartyEmailClient automatically and injects it into EmailNotificationAdapter. If you're new to service registration, the IServiceCollection beginner's guide covers the fundamentals.
Step 5: Use in Client Code
Client code should depend only on the target interface. This is the payoff when you implement adapter pattern in C# -- the rest of your application is completely decoupled from the third-party email client. If you swap the email provider later, only the adapter changes. Every class that consumes INotificationSender is unaffected because it never references the adaptee. That decoupling is the core reason developers implement adapter pattern in their applications.
public class OrderService
{
private readonly INotificationSender _notificationSender;
public OrderService(
INotificationSender notificationSender)
{
_notificationSender = notificationSender
?? throw new ArgumentNullException(
nameof(notificationSender));
}
public void ProcessOrder(string orderId, string customerEmail)
{
// ... order processing logic ...
var message = new NotificationMessage(
recipient: customerEmail,
subject: $"Order {orderId} Confirmed",
body: $"<p>Your order {orderId} has been " +
$"processed successfully.</p>");
NotificationResult result = _notificationSender
.Send(message);
if (!result.Success)
{
Console.WriteLine(
$"[OrderService] Failed to send " +
$"notification: {result.ErrorDetails}");
}
Console.WriteLine(
$"[OrderService] Notification sent. " +
$"Message ID: {result.MessageId}");
}
}
The OrderService knows nothing about ThirdPartyEmailClient, SendEmail, or confirmation codes. It speaks purely in domain terms -- NotificationMessage and NotificationResult. If you later switch from email to SMS or push notifications, you write a new adapter and update the DI registration. The OrderService doesn't change at all.
Step 6: Handle Edge Cases
A production-ready adapter needs to handle failures gracefully. When you implement adapter pattern in C#, the adapter is responsible for translating not just the happy path but also exceptions, null responses, and unexpected behavior from the adaptee. Let's revisit our email adapter and implement adapter pattern error handling properly.
Here's an improved adapter with error handling:
public class ResilientEmailNotificationAdapter
: INotificationSender
{
private readonly ThirdPartyEmailClient _emailClient;
public ResilientEmailNotificationAdapter(
ThirdPartyEmailClient emailClient)
{
_emailClient = emailClient
?? throw new ArgumentNullException(
nameof(emailClient));
}
public NotificationResult Send(
NotificationMessage message)
{
if (string.IsNullOrWhiteSpace(message.Recipient))
{
return new NotificationResult(
success: false,
messageId: string.Empty,
errorDetails: "Recipient cannot be empty.");
}
try
{
string confirmationCode = _emailClient.SendEmail(
toAddress: message.Recipient,
emailSubject: message.Subject ?? string.Empty,
htmlBody: message.Body ?? string.Empty,
enableTracking: true);
if (string.IsNullOrWhiteSpace(confirmationCode))
{
return new NotificationResult(
success: false,
messageId: string.Empty,
errorDetails: "Email service returned " +
"an empty confirmation code.");
}
return new NotificationResult(
success: true,
messageId: confirmationCode);
}
catch (Exception ex)
{
return new NotificationResult(
success: false,
messageId: string.Empty,
errorDetails: $"Email delivery failed: " +
$"{ex.Message}");
}
}
}
This version addresses several edge cases:
- Null or empty recipients: Caught before calling the adaptee, avoiding unnecessary network calls or cryptic exceptions from the third-party client.
- Null subject and body: Defaulted to empty strings so the adaptee doesn't throw
NullReferenceException. - Exceptions from the adaptee: Caught and translated into a
NotificationResultwith error details. The client code doesn't need to catch third-party-specific exceptions. - Empty confirmation codes: Treated as a failure rather than returning a misleading success result.
The adapter acts as a shield. It ensures that no matter what the adaptee does, the target interface contract is honored. This is especially important when the adaptee is a third-party library you don't control. Getting error handling right is a critical part of learning to implement adapter pattern in production C# code.
A More Complex Scenario: Multi-Provider Adapter
Simple one-to-one adapters cover most cases, but sometimes you need to implement adapter pattern in C# with more complex translation logic. Consider a scenario where your application must support multiple notification providers, and each provider has a completely different API.
public class SmsGatewayClient
{
public bool TransmitTextMessage(
string phoneNumber,
string messageText)
{
Console.WriteLine(
$"[SmsGateway] Sending SMS to " +
$"'{phoneNumber}': {messageText}");
return true;
}
}
public class SmsNotificationAdapter : INotificationSender
{
private readonly SmsGatewayClient _smsGateway;
public SmsNotificationAdapter(
SmsGatewayClient smsGateway)
{
_smsGateway = smsGateway
?? throw new ArgumentNullException(
nameof(smsGateway));
}
public NotificationResult Send(
NotificationMessage message)
{
string plainText = StripHtml(message.Body);
bool sent = _smsGateway.TransmitTextMessage(
phoneNumber: message.Recipient,
messageText: $"{message.Subject}: {plainText}");
return new NotificationResult(
success: sent,
messageId: sent
? Guid.NewGuid().ToString("N")[..8]
: string.Empty,
errorDetails: sent
? ""
: "SMS transmission failed.");
}
private static string StripHtml(string html)
{
if (string.IsNullOrWhiteSpace(html))
{
return string.Empty;
}
return System.Text.RegularExpressions
.Regex.Replace(html, "<.*?>", string.Empty)
.Trim();
}
}
This SMS adapter does more than simple parameter mapping. It strips HTML from the message body because SMS doesn't support HTML. It combines the subject and body into a single text message because the SMS API expects one string. And it generates its own message ID because the SMS gateway only returns a boolean.
Now you can register both adapters and choose at runtime:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<ThirdPartyEmailClient>();
services.AddSingleton<SmsGatewayClient>();
// Register with a keyed approach or
// choose based on configuration
services.AddKeyedSingleton<INotificationSender>(
"email",
(sp, _) => new EmailNotificationAdapter(
sp.GetRequiredService<ThirdPartyEmailClient>()));
services.AddKeyedSingleton<INotificationSender>(
"sms",
(sp, _) => new SmsNotificationAdapter(
sp.GetRequiredService<SmsGatewayClient>()));
This pattern scales well. Each adapter is independent, testable, and focused on translating one specific adaptee. If you need to add push notifications later, you write a new adapter and implement adapter pattern in C# with the same target interface. The application code never changes.
This multi-provider approach also works well alongside other structural patterns. The facade pattern can wrap multiple adapters behind a single unified interface if client code needs to broadcast across all channels simultaneously.
Common Mistakes to Avoid
Even experienced developers make these mistakes when they first implement the adapter pattern in C#.
Leaking adaptee types through the adapter: The whole point when you implement adapter pattern is to hide the adaptee's API. If your adapter exposes ThirdPartyEmailClient through a public property or lets exceptions from the adaptee propagate uncaught, you've broken the abstraction. Client code should never need to reference the adaptee's namespace.
Making the adapter do too much: The adapter's job is translation, not business logic. If your adapter starts making decisions about retry policies, rate limiting, or message formatting, those concerns belong in separate classes. The adapter should map inputs and outputs -- nothing more. Consider using the decorator pattern to layer cross-cutting behavior around the adapter rather than stuffing it inside.
Using class adapter (inheritance) instead of object adapter (composition): C# doesn't support multiple inheritance, so inheriting from the adaptee limits your adapter to a single base class. When you implement adapter pattern in C#, composition is more flexible because the adapter can wrap any instance, including mocked instances in tests.
Forgetting to adapt error behavior: Translating the happy path is easy. The hard part is handling exceptions, nulls, and unexpected responses from the adaptee. When you implement adapter pattern correctly, your adapter should ensure the target interface contract is honored in all cases, not just the success scenario.
Creating one-off adapters for every integration: If you find yourself writing many nearly identical adapters, step back and look for a pattern. You might benefit from a generic adapter or a pipeline-based approach that applies transformations in sequence. Still, it's better to implement adapter pattern individually at first and refactor toward a generic solution once you see the commonalities.
Frequently Asked Questions
What is the adapter pattern and when should I use it in C#?
The adapter pattern is a structural design pattern that allows classes with incompatible interfaces to work together. You should implement adapter pattern in C# whenever you need to integrate a class -- whether it's a third-party SDK, a legacy service, or a different module -- whose API doesn't match the interface your application expects. It's especially useful when you can't modify the source class and need to implement adapter pattern without touching existing code.
What is the difference between an object adapter and a class adapter?
An object adapter uses composition -- it holds a reference to the adaptee and delegates calls to it. A class adapter uses multiple inheritance to combine the target interface and the adaptee class. Since C# doesn't support multiple inheritance of classes, object adapters are the standard approach when you implement adapter pattern in C#. Object adapters are also more flexible because they can wrap any instance of the adaptee, including test doubles.
How is the adapter pattern different from the facade pattern?
The facade pattern simplifies a complex subsystem by providing a unified higher-level interface. The adapter pattern makes one specific interface compatible with another. A facade typically wraps multiple classes and hides complexity. An adapter wraps a single class and translates its interface. You implement adapter pattern in C# when you have an interface mismatch, and you use a facade when you want to reduce the surface area of a complex API. Both are structural patterns, but their goals differ.
Can I have multiple adapters for the same target interface?
Absolutely. This is one of the adapter pattern's biggest strengths. You can write an EmailNotificationAdapter, an SmsNotificationAdapter, and a PushNotificationAdapter -- all implementing INotificationSender. You implement adapter pattern in C# the same way for each provider. Your DI container can provide the right one based on configuration, and your application code stays completely unaware of which adapter it's using.
How do I test adapter classes in C#?
Test adapters by providing a controlled adaptee instance. If the adaptee is a concrete class without an interface, you can either wrap it in a thin wrapper interface for testing or use the real instance with predictable inputs. Verify that the adapter correctly maps inputs from the target interface to the adaptee's parameters, and that it correctly translates the adaptee's response back into the target's return type. Also test error scenarios -- pass inputs that cause the adaptee to throw and verify the adapter handles them gracefully.
Should I always introduce an adapter when using third-party libraries?
Not always. If a third-party library's API already aligns well with your needs and you only use it in a few places, adding an adapter introduces complexity without much benefit. Implement adapter pattern in C# when the library's interface diverges significantly from your domain model, when you want to isolate your codebase from the library for future swaps, or when you need to mock the dependency in tests. Use your judgment -- the adapter pattern adds value when it solves a real interface mismatch problem.
How does the adapter pattern relate to dependency injection?
The adapter pattern and dependency injection work together naturally. You define a target interface, register the adapter as the implementation in your DI container, and inject the target interface into consuming classes. This follows inversion of control -- your application depends on abstractions, and the adapter provides the concrete bridge to external systems. The consuming code never needs to know an adapter exists.

