BrandGhost
Adapter Design Pattern in C#: Complete Guide with Examples

Adapter Design Pattern in C#: Complete Guide with Examples

Adapter Design Pattern in C#: Complete Guide with Examples

When you need to make two incompatible interfaces work together, the adapter design pattern in C# is the structural pattern designed for exactly that job. It acts as a translator between classes that couldn't otherwise collaborate -- converting one interface into another that a client expects. Whether you're integrating a legacy system, wrapping a third-party library, or bridging two subsystems with different contracts, the adapter pattern lets you do it without modifying existing code.

In this complete guide, we'll walk through everything you need to know about the adapter design pattern -- from the core concepts and key participants to practical C# implementations, dependency injection integration, and common mistakes. By the end, you'll have working code examples you can adapt to your own projects and a clear understanding of when the adapter pattern is the right tool for the job.

What Is the Adapter Design Pattern?

The adapter design pattern is a structural design pattern from the Gang of Four (GoF) catalog that converts the interface of a class into another interface that clients expect. It lets classes work together that otherwise couldn't because of incompatible interfaces. Think of it like a power adapter you'd use when traveling internationally -- the outlet shape is different, but the adapter bridges the gap so your device gets the electricity it needs.

The adapter design pattern solves a common problem in software development. You have an existing class that does exactly what you need, but its interface doesn't match what your client code expects. Maybe you're consuming a third-party library whose API differs from your application's abstractions. Maybe you're integrating with a legacy system whose method signatures don't align with your modern interface contracts. The adapter design pattern addresses this by wrapping the incompatible class and exposing the expected interface.

This pattern is closely related to other structural patterns. The facade pattern simplifies a complex subsystem behind a single interface, while the adapter design pattern converts one interface into another. The composite pattern composes objects into tree structures, which is a different structural concern entirely. Understanding where the adapter design pattern fits relative to other structural patterns helps you choose the right tool for each situation.

Core Components of the Adapter Design Pattern

The adapter design pattern involves four key participants. Understanding each role clearly is essential before diving into implementation.

The Target is the interface that the client code expects. This is the contract your application is built around -- typically an interface or abstract class in C#.

The Adaptee is the existing class with a useful implementation but an incompatible interface. You want to reuse it but can't because its method signatures don't match the target interface. The adaptee is not modified -- the whole point of the adapter design pattern is to bridge the gap without changing existing code.

The Adapter bridges the target and the adaptee. It implements the target interface and holds a reference to an adaptee instance. When the client calls a method on the target interface, the adapter translates that call into one or more calls on the adaptee, handling any data transformation or parameter mapping between the two interfaces.

The Client works exclusively with the target interface. It doesn't know or care that an adapter is translating its calls behind the scenes -- the client code remains clean and decoupled from the adaptee's implementation details.

Object Adapter vs. Class Adapter

There are two classic variations of the adapter design pattern. The difference comes down to how the adapter connects to the adaptee.

Object Adapter (Composition)

The object adapter uses composition. The adapter holds a reference to an instance of the adaptee and delegates calls to it. This is the preferred approach in C# for several reasons. It follows the principle of composition over inheritance, it works with interfaces (not just classes), and it allows the adapter to work with any subclass of the adaptee without modification.

The object adapter is more flexible because the adaptee is injected as a dependency. You can swap in different adaptee implementations at runtime, which plays nicely with dependency injection and unit testing.

Class Adapter (Inheritance)

The class adapter uses multiple inheritance -- the adapter inherits from both the target and the adaptee simultaneously. In languages like C++, this works directly. In C#, true multiple class inheritance isn't supported. You can approximate it by inheriting from the adaptee class and implementing the target interface, but this creates tighter coupling.

Because the class adapter inherits the adaptee's implementation, it can override adaptee methods directly. However, it's locked to one specific adaptee class and can't adapt different implementations at runtime. For modern C# development, the object adapter approach is almost always the better choice.

Implementing the Adapter Design Pattern in C#

Let's build a practical example of the adapter design pattern. Imagine you're developing an application that needs to send notifications, and your codebase works with a clean INotificationSender interface.You need to integrate with a third-party email library that has a completely different API. The adapter pattern lets you bridge this gap cleanly.

Defining the Target Interface

First, here's the interface your application expects:

public interface INotificationSender
{
    void Send(string recipient, string subject, string body);
}

Your client code is built around this contract. Every notification sender in your system implements this interface.

The Adaptee: A Third-Party Email Library

Now suppose the third-party library has a class with a completely different method signature:

public sealed class ThirdPartyEmailService
{
    public void SendEmail(
        EmailMessage message)
    {
        // Third-party implementation that
        // actually delivers the email
        Console.WriteLine(
            $"[ThirdPartyEmail] To: {message.To}, " +
            $"Subject: {message.Subject}");
    }
}

public sealed class EmailMessage
{
    public string To { get; init; } = string.Empty;

    public string Subject { get; init; } = string.Empty;

    public string Body { get; init; } = string.Empty;

    public string From { get; init; } = "[email protected]";
}

This class does exactly what you need -- it sends emails. But its interface doesn't match INotificationSender. It takes an EmailMessage object instead of separate string parameters, and the method name is SendEmail instead of Send.

Creating the Adapter

The adapter implements the target interface and wraps the adaptee, translating calls between the two:

public sealed class ThirdPartyEmailAdapter : INotificationSender
{
    private readonly ThirdPartyEmailService _emailService;

    public ThirdPartyEmailAdapter(
        ThirdPartyEmailService emailService)
    {
        _emailService = emailService;
    }

    public void Send(
        string recipient,
        string subject,
        string body)
    {
        var message = new EmailMessage
        {
            To = recipient,
            Subject = subject,
            Body = body,
        };

        _emailService.SendEmail(message);
    }
}

The adapter class is straightforward. It implements INotificationSender, takes a ThirdPartyEmailService through its constructor, and translates the Send call into the format the adaptee expects. The parameter mapping happens inside the adapter -- the client never needs to know about EmailMessage or SendEmail. This is a textbook implementation of the adapter design pattern using composition.

Notice that the adapter uses composition (the object adapter approach). It holds a reference to the adaptee rather than inheriting from it. This keeps the coupling loose and makes testing easy.

Client Code

The client works exclusively with the INotificationSender interface:

public sealed class NotificationManager
{
    private readonly INotificationSender _sender;

    public NotificationManager(
        INotificationSender sender)
    {
        _sender = sender;
    }

    public void NotifyUser(
        string userEmail,
        string message)
    {
        _sender.Send(
            userEmail,
            "Notification",
            message);
    }
}

The NotificationManager doesn't know whether it's talking to a direct implementation, an adapter wrapping a third-party library, or any other implementation of INotificationSender. This is the adapter design pattern doing its job -- bridging incompatible interfaces while keeping the client code clean and decoupled.

Registering with Dependency Injection

In a real .NET application, you'd register the adapter with the DI container using IServiceCollection:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddSingleton<ThirdPartyEmailService>();
services.AddSingleton<INotificationSender, ThirdPartyEmailAdapter>();
services.AddTransient<NotificationManager>();

var provider = services.BuildServiceProvider();
var manager = provider
    .GetRequiredService<NotificationManager>();

manager.NotifyUser(
    "[email protected]",
    "Your order has been shipped.");

This is where the adapter design pattern integrates naturally with inversion of control. The DI container constructs the ThirdPartyEmailService, injects it into the ThirdPartyEmailAdapter, and registers the adapter as the implementation of INotificationSender. The client code -- NotificationManager -- receives the adapter through constructor injection without any awareness of the third-party library.

Adapting Multiple Implementations

A powerful application of the adapter design pattern is wrapping multiple incompatible services behind a single target interface. Suppose you also need to integrate with an SMS service that has yet another different API:

public sealed class LegacySmsGateway
{
    public int Transmit(
        string phoneNumber,
        string textContent)
    {
        Console.WriteLine(
            $"[SMS] To: {phoneNumber}, " +
            $"Text: {textContent}");
        return 0; // status code
    }
}

You can create a second adapter:

public sealed class SmsNotificationAdapter : INotificationSender
{
    private readonly LegacySmsGateway _gateway;

    public SmsNotificationAdapter(
        LegacySmsGateway gateway)
    {
        _gateway = gateway;
    }

    public void Send(
        string recipient,
        string subject,
        string body)
    {
        string combinedMessage = $"{subject}: {body}";
        _gateway.Transmit(recipient, combinedMessage);
    }
}

Now both the third-party email service and the legacy SMS gateway can be used anywhere your application expects an INotificationSender. The email adapter maps parameters into an EmailMessage object, while the SMS adapter combines the subject and body into a single text string. Each adapter encapsulates the translation logic specific to its adaptee.

This approach works well when you want to provide consumers with a choice of implementations. Register both adapters in the DI container and resolve the appropriate one based on configuration or context.

When to Use the Adapter Design Pattern

The adapter design pattern shines in specific situations. Recognizing these scenarios helps you reach for the pattern at the right time instead of forcing it where it doesn't belong.

Integrating third-party libraries is the most common use case. When a library's API doesn't match your application's abstractions, an adapter wraps the library and exposes the interface your code expects. This also protects your application from library changes -- if the third-party API evolves, you only update the adapter, not every piece of client code.

Working with legacy systems is another natural fit. Legacy code often has method names, parameter orders, or data formats that don't align with modern design. Instead of rewriting the legacy code (risky and expensive), you write an adapter that translates between the old and new interfaces.

Enabling testability is an underappreciated benefit. When you depend directly on a concrete third-party class, you can't easily substitute a mock in unit tests. Wrapping that class behind an adapter with a clean target interface lets you inject test doubles through dependency injection.

Bridging subsystems within the same application is also valid. If two modules evolved independently and ended up with incompatible interfaces, an adapter can connect them without a large-scale refactor.

Benefits and Drawbacks

Like any design pattern, the adapter design pattern involves trade-offs. Understanding both sides helps you apply it with intention rather than by reflex.

Benefits

Single Responsibility Principle. The adapter separates interface conversion logic from business logic. Your client code stays focused on its job, and the adapter handles the translation.

Open/Closed Principle. You can introduce new adapters for new adaptees without modifying existing client code. The target interface stays stable, and each new integration gets its own adapter class.

Loose coupling. Client code depends on the target interface, not on the adaptee's concrete class. This makes it easy to swap implementations, test in isolation, and evolve the system without cascading changes.

Reusability. You can reuse existing classes that do useful work, even if their interfaces don't match what you need. The adapter makes it possible without modifying the adaptee.

Drawbacks

Added indirection. Every adapter adds another class to the codebase and another layer of abstraction. For simple cases, the indirection might not be worth it -- especially if the interface mismatch is trivial.

Potential for proliferation. If you're adapting many different classes, you can end up with a large number of adapter classes. Each one is small and focused, but the sheer count can make the codebase harder to navigate.

Data loss or approximation. When translating between interfaces, some information might not map cleanly. In the SMS adapter example above, the subject and body were concatenated into a single string -- the distinction between them was lost. Be deliberate about how you handle these translation gaps.

Relationship to Other Structural Patterns

The adapter design pattern belongs to the family of structural patterns, and it's easy to confuse it with similar patterns. Here's how it compares.

The facade pattern also wraps existing code behind a new interface, but the intent is different. A facade simplifies a complex subsystem by providing a higher-level interface. An adapter converts one specific interface into another. A facade typically wraps multiple classes; an adapter typically wraps one.

The decorator pattern also implements the same interface as the object it wraps, but its goal is to add behavior -- logging, caching, validation -- not to convert between interfaces. Decorators keep the same interface; adapters change it.

The proxy pattern also wraps an object, but the proxy provides the exact same interface as the original. It controls access (lazy loading, remote communication, caching) rather than converting between different interfaces.

The strategy pattern is behavioral rather than structural, but it often appears alongside adapters. You might use the strategy pattern to select which adapter to use at runtime based on some criteria. For example, choosing between an email adapter and an SMS adapter based on the notification type.

Common Mistakes to Avoid

Several mistakes come up repeatedly when developers implement the adapter design pattern. Knowing these upfront helps you write cleaner adapter code.

Putting business logic in the adapter. The adapter design pattern's job is translation -- converting one interface to another. It should not contain business rules, validation logic, or complex computations. If your adapter is getting large, you're likely mixing concerns. Keep the adapter thin and focused on the mapping between interfaces.

Adapting when you should refactor. If you control both the client and the incompatible class, consider refactoring one of them to match the other instead of introducing an adapter. Adapters are most valuable when you can't or shouldn't modify the adaptee -- third-party code, legacy systems, or shared libraries.

Creating adapters for trivial differences. If the only difference between two interfaces is a method name, the adapter design pattern might be overkill. Consider whether a simple wrapper method or a delegate would be clearer. The adapter design pattern earns its keep when there's genuine translation work -- parameter mapping, data format conversion, or protocol differences.

Ignoring error handling in translation. The adaptee might throw different exception types than your target interface's callers expect. A well-written adapter catches adaptee-specific exceptions and translates them into exceptions appropriate for the target interface.

Tightly coupling the adapter to concrete types. If your adapter takes a concrete adaptee in its constructor, it's coupled to that specific class. This is often fine -- the adapter exists specifically for that adaptee. But if you want to test the adapter's translation logic independently, consider accepting an abstraction for the adaptee as well.

Frequently Asked Questions

What is the adapter design pattern in C#?

The adapter design pattern in C# is a structural pattern that converts the interface of an existing class into a different interface that client code expects. It wraps one class (the adaptee) in an adapter that implements the target interface, translating method calls, parameter formats, and data types between the two. You can explore other structural and behavioral patterns in the big list of design patterns.

What is the difference between the adapter pattern and the facade pattern?

The adapter pattern converts one interface into another so that an existing class can be used where a different interface is expected. The facade pattern simplifies a complex subsystem by providing a unified, higher-level interface that hides the subsystem's complexity. An adapter wraps a single class to change its interface; a facade wraps multiple classes to simplify interaction. Use an adapter when you have an interface mismatch and a facade when you want to reduce complexity.

Should I use an object adapter or a class adapter in C#?

In C#, the object adapter (composition-based) is almost always the better choice. C# does not support multiple class inheritance, so a true class adapter requires inheriting from the adaptee while implementing the target interface. This creates tight coupling and limits flexibility. The object adapter holds a reference to the adaptee, which allows runtime flexibility, easier testing, and compatibility with dependency injection. Composition gives you all the flexibility you need without the constraints of inheritance.

When should I use the adapter pattern instead of refactoring?

Use the adapter design pattern when you can't or shouldn't modify the incompatible class. This includes third-party libraries, shared NuGet packages, legacy code with high regression risk, or code owned by another team. If you control both the client code and the incompatible class and can safely modify either one, refactoring to align the interfaces is often simpler and more maintainable than introducing an adapter layer. The adapter design pattern is a bridge -- not a substitute for good API design.

How does the adapter pattern improve testability?

When your client code depends directly on a concrete third-party class, you can't easily substitute a test double in unit tests. Wrapping the third-party class in an adapter that implements a target interface lets you inject mock or stub implementations during testing. Your unit tests verify client behavior against the target interface, and you write separate integration tests to verify the adapter correctly translates calls to the real adaptee. This separation makes your test suite faster and more focused.

Can I combine the adapter design pattern with the strategy pattern?

Yes -- and it's a practical combination. You can define multiple adapters that implement the same target interface, each wrapping a different adaptee. The strategy pattern lets you select which adapter to use at runtime based on configuration or context. For example, you might choose between an email adapter, an SMS adapter, and a push notification adapter based on user preferences. The adapter design pattern handles the interface translation; the strategy handles the selection logic.

How does the adapter design pattern relate to the observer pattern?

The adapter design pattern and the observer pattern solve different problems but can work together.If you're integrating with an event system that uses a different event model than your application expects, you can write an adapter that subscribes to the external system's events and raises events through your application's observer infrastructure. The adapter translates between event protocols, while the observer pattern handles event distribution within your application.

Wrapping Up the Adapter Design Pattern in C#

The adapter design pattern in C# is a fundamental structural pattern for bridging incompatible interfaces. By wrapping an existing class behind a target interface, you enable integration with third-party libraries, legacy systems, and external services without modifying their code.

The object adapter approach -- using composition rather than inheritance -- is the natural fit for modern C# development. It works seamlessly with dependency injection and inversion of control, letting the DI container wire up adaptees and adapters automatically.

Start by identifying places in your codebase where you're working directly with concrete third-party classes or fighting against interface mismatches. Those are strong signals that the adapter design pattern can simplify your design. Keep your adapters thin -- focused on translation, not business logic. And remember that not every interface difference needs an adapter. When you own both sides, aligning the interfaces directly is often the simpler path.

What Is The Adapter Design Pattern? - Beginner Questions Answered

So, what is the adapter design pattern? Why use the adapter pattern? In this article, we'll look at one of the popular design patterns in detail!

Decorator Design Pattern in C#: Complete Guide with Examples

Master the decorator design pattern in C# with practical code examples, best practices, and real-world use cases for flexible object extension.

Composite Design Pattern in C#: Complete Guide with Examples

Master the composite design pattern in C# with practical examples showing tree structures, recursive operations, and uniform component handling.

An error has occurred. This application may no longer respond until reloaded. Reload