BrandGhost
When to Use Adapter Pattern in C#: Decision Guide with Examples

When to Use Adapter Pattern in C#: Decision Guide with Examples

When to Use Adapter Pattern in C#: Decision Guide with Examples

You've got two systems that need to talk to each other, but their interfaces don't line up. Maybe it's a legacy service with method signatures that predate your current architecture. Maybe it's a third-party library that models data differently than your domain. The when to use adapter pattern in C# question comes up every time you face an interface mismatch and need a clean way to bridge the gap without rewriting either side.

This article gives you a structured decision framework for recognizing when this pattern is the right tool and -- just as importantly -- when it isn't. We'll walk through real code examples covering legacy system integration, third-party library wrapping, interface incompatibility, and data format conversion. You'll also learn how it compares to alternatives like the facade pattern and simple type conversion. For a broader overview of where this pattern fits in the landscape, see the big list of design patterns.

Signs You Need the Adapter Pattern in C#

The adapter pattern solves a specific problem: making incompatible interfaces work together. Not every integration headache calls for it, but certain patterns in your codebase are strong signals that an adapter is the right approach.

You're Integrating a Legacy System

Legacy systems rarely expose interfaces that match your modern architecture. You might have a SOAP-based order service that returns XML objects while the rest of your application works with strongly-typed records. Rewriting the legacy system isn't practical -- it works, it's tested, and nobody wants to touch it. An adapter lets you create a thin translation layer between the old interface and the new one your code expects.

This is one of the most common scenarios for this pattern. The legacy system keeps running as-is. Your new code programs against a clean interface. The adapter sits in between and handles the translation.

You're Wrapping a Third-Party Library

Third-party libraries come with their own conventions, naming patterns, and data models. When you take a direct dependency on a library's types throughout your codebase, you couple yourself to that library's design decisions. If the library changes its API in a major version update -- or if you need to swap it out entirely -- you're facing a painful refactor.

An adapter gives you a boundary. Your application code depends on an interface you define. The adapter implements that interface by delegating to the third-party library. If the library changes or you switch providers, you only update the adapter class. Everything else stays the same.

Two Interfaces Don't Match But Represent the Same Concept

Sometimes you have two parts of your system that model the same concept differently. One service defines a CustomerInfo class with FirstName and LastName properties. Another expects an IUserProfile with a single FullName property. The concept is the same -- a person's identity -- but the shapes don't align.

An adapter bridges this gap without forcing either side to change. This is especially valuable in large codebases with multiple teams or when integrating microservices that evolved independently.

You Need to Convert Data Formats

Data format conversion is a specialized case of interface incompatibility. Your application works with JSON-based DTOs, but an external API returns CSV data. Or your internal events use a flat structure while a partner's webhook sends nested payloads. An adapter handles these transformations cleanly by encapsulating the conversion logic in a single class.

When NOT to Use the Adapter Pattern

Knowing when to use the adapter pattern in C# is only valuable if you also know when to skip it. Reaching for it in situations where a simpler approach works adds indirection without benefit.

Simple Type Conversion Is All You Need

If you just need to convert one type to another -- say, mapping a DTO to a domain model -- you don't need a full adapter class. A mapping method, an extension method, or a tool like AutoMapper handles straightforward property-to-property conversion without the ceremony of defining an interface and an adapter class. This pattern is about bridging incompatible interfaces, not about mapping fields.

You Control Both Interfaces

When you own both the calling code and the code being called, consider just changing one of the interfaces to match the other. This pattern exists specifically for situations where you can't modify one or both sides. If you can modify both, aligning the interfaces directly is simpler and eliminates the intermediary entirely.

This comes up surprisingly often. A developer reaches for an adapter out of habit when the real solution is a five-minute interface refactor.

A Facade Is the Better Fit

These two patterns look similar on the surface -- both wrap existing code behind a new interface. But they solve different problems. An adapter converts one interface to another so that existing code can be used where a different interface is expected. A facade simplifies a complex subsystem by providing a higher-level interface.

If you're wrapping five related service calls into one clean method, that's a facade. If you're making one service's interface compatible with another's expectations, that's an adapter. Choosing the wrong one leads to a class that's either too simple to justify or too complex for its stated purpose.

Decision Framework for the Adapter Pattern in C#

When evaluating whether this pattern fits your situation, walk through these four questions. If most of them get a "yes," an adapter is likely the right choice.

Is there an interface mismatch? The adapter pattern exists to resolve incompatibility between interfaces. If both sides already speak the same language, you don't need a translator. This is the most fundamental criterion for when to use the adapter pattern in C#.

Can you modify the incompatible interface? If you can change the source interface to match what your code expects, do that instead. Adapters are most valuable when one or both sides are out of your control -- third-party libraries, legacy systems, external APIs, or shared contracts you can't break.

Is the adaptation logic focused? A good adapter does one thing: translate between two interfaces. If your "adapter" is also filtering data, applying business rules, orchestrating multiple calls, or managing state, it's trying to do too much. The translation logic should be mechanical and predictable.

Will this boundary help you in the future? Adapters create an abstraction boundary. If there's a realistic chance you'll need to swap the underlying implementation -- different payment provider, different logging framework, different data source -- the adapter gives you a clean seam. If the implementation is permanent and stable, the boundary adds indirection without payoff.

Scenario 1: Wrapping a Legacy Order System

You have a legacy order service with an older API that your new application needs to consume. The legacy service uses its own types and method signatures that don't match your domain model. Here's the setup:

// Legacy system types you cannot modify
public class LegacyOrderService
{
    public LegacyOrderResult SubmitOrder(
        string customerCode,
        string[] itemCodes,
        double totalPrice)
    {
        Console.WriteLine(
            $"Legacy: Submitting order for {customerCode}, " +
            $"{itemCodes.Length} items, ${totalPrice}");

        return new LegacyOrderResult
        {
            OrderNumber = $"LEG-{Guid.NewGuid():N}",
            StatusCode = 1,
            Timestamp = DateTime.UtcNow.ToString("o")
        };
    }
}

public class LegacyOrderResult
{
    public string OrderNumber { get; set; } = "";
    public int StatusCode { get; set; }
    public string Timestamp { get; set; } = "";
}

Your modern application defines a clean interface that represents what an order service should look like:

public interface IOrderService
{
    OrderConfirmation PlaceOrder(OrderRequest request);
}

public record OrderRequest(
    string CustomerId,
    List<string> ProductIds,
    decimal Total);

public record OrderConfirmation(
    string OrderId,
    bool IsSuccessful,
    DateTimeOffset ConfirmedAt);

The adapter bridges these two worlds:

public sealed class LegacyOrderAdapter : IOrderService
{
    private readonly LegacyOrderService _legacyService;

    public LegacyOrderAdapter(LegacyOrderService legacyService)
    {
        _legacyService = legacyService
            ?? throw new ArgumentNullException(
                nameof(legacyService));
    }

    public OrderConfirmation PlaceOrder(OrderRequest request)
    {
        var result = _legacyService.SubmitOrder(
            request.CustomerId,
            request.ProductIds.ToArray(),
            (double)request.Total);

        return new OrderConfirmation(
            result.OrderNumber,
            result.StatusCode == 1,
            DateTimeOffset.Parse(result.Timestamp));
    }
}

The rest of your application programs against IOrderService without knowing a legacy system exists behind it. When the legacy system eventually gets replaced, you swap out the adapter -- not every caller. You can register this in your dependency injection container so the rest of the application is completely decoupled from the legacy implementation.

Scenario 2: Adapting a Third-Party Notification Library

You're using a third-party email library that has its own types and method signatures. Your application has a notification abstraction that's provider-agnostic. Here's the third-party library's API:

// Third-party library types you don't control
public class SmtpMailClient
{
    public SmtpResponse SendMail(
        string fromAddress,
        string toAddress,
        string subject,
        string htmlBody,
        Dictionary<string, string>? headers = null)
    {
        Console.WriteLine(
            $"SMTP: Sending '{subject}' " +
            $"from {fromAddress} to {toAddress}");

        return new SmtpResponse
        {
            Code = 250,
            Message = "OK"
        };
    }
}

public class SmtpResponse
{
    public int Code { get; set; }
    public string Message { get; set; } = "";
}

Your application defines its own notification interface:

public interface INotificationSender
{
    NotificationResult Send(Notification notification);
}

public record Notification(
    string Recipient,
    string Subject,
    string Body,
    string Sender = "[email protected]");

public record NotificationResult(
    bool Delivered,
    string? ErrorMessage = null);

The adapter translates between the two:

public sealed class SmtpNotificationAdapter
    : INotificationSender
{
    private readonly SmtpMailClient _smtpClient;

    public SmtpNotificationAdapter(SmtpMailClient smtpClient)
    {
        _smtpClient = smtpClient
            ?? throw new ArgumentNullException(
                nameof(smtpClient));
    }

    public NotificationResult Send(Notification notification)
    {
        var response = _smtpClient.SendMail(
            notification.Sender,
            notification.Recipient,
            notification.Subject,
            notification.Body);

        return new NotificationResult(
            Delivered: response.Code == 250,
            ErrorMessage: response.Code != 250
                ? response.Message
                : null);
    }
}

If you later switch from SMTP to a different provider like SendGrid or an SMS gateway, you write a new adapter that implements INotificationSender. No other code changes. This is a textbook case for when to use the adapter pattern in C# -- your domain interface stays stable while the underlying provider changes behind the adapter.

Scenario 3: Bridging Incompatible Event Interfaces

Your application publishes domain events through an IEventPublisher interface, but you need to integrate with an external analytics platform that expects a completely different event format:

public interface IEventPublisher
{
    void Publish(DomainEvent domainEvent);
}

public record DomainEvent(
    string EventType,
    string AggregateId,
    Dictionary<string, object> Payload,
    DateTimeOffset OccurredAt);

The analytics platform SDK uses a flat structure:

// External analytics SDK
public class AnalyticsPlatformClient
{
    public void TrackEvent(
        string eventName,
        string entityId,
        string jsonProperties,
        long unixTimestampMs)
    {
        Console.WriteLine(
            $"Analytics: Tracked '{eventName}' " +
            $"for entity {entityId} " +
            $"at {unixTimestampMs}");
    }
}

The adapter handles the format conversion:

public sealed class AnalyticsEventAdapter : IEventPublisher
{
    private readonly AnalyticsPlatformClient _analytics;

    public AnalyticsEventAdapter(
        AnalyticsPlatformClient analytics)
    {
        _analytics = analytics
            ?? throw new ArgumentNullException(
                nameof(analytics));
    }

    public void Publish(DomainEvent domainEvent)
    {
        var jsonProperties =
            System.Text.Json.JsonSerializer.Serialize(
                domainEvent.Payload);

        var unixMs = domainEvent.OccurredAt
            .ToUnixTimeMilliseconds();

        _analytics.TrackEvent(
            domainEvent.EventType,
            domainEvent.AggregateId,
            jsonProperties,
            unixMs);
    }
}

This adapter converts your rich DomainEvent record into the flat parameters the analytics SDK expects. The conversion logic is mechanical -- serializing a dictionary to JSON, converting a DateTimeOffset to Unix milliseconds. That's exactly the kind of focused translation an adapter is designed for.

Scenario 4: Converting External API Data Formats

You're consuming an external weather API that returns data in a structure that doesn't match your application's model. Your app expects weather data through a domain interface:

public interface IWeatherService
{
    WeatherForecast GetForecast(string city);
}

public record WeatherForecast(
    string Location,
    double TemperatureCelsius,
    string Condition,
    DateTimeOffset RetrievedAt);

The external API client returns a nested JSON-like structure:

// External API response model
public class ExternalWeatherResponse
{
    public ExternalLocation Location { get; set; } = new();
    public ExternalCurrentCondition Current { get; set; } = new();
}

public class ExternalLocation
{
    public string Name { get; set; } = "";
    public string Region { get; set; } = "";
    public string Country { get; set; } = "";
}

public class ExternalCurrentCondition
{
    public double TempF { get; set; }
    public ExternalConditionDetail Condition { get; set; } = new();
}

public class ExternalConditionDetail
{
    public string Text { get; set; } = "";
    public int Code { get; set; }
}

// External API client
public class WeatherApiClient
{
    public ExternalWeatherResponse FetchWeather(
        string query)
    {
        Console.WriteLine(
            $"API: Fetching weather for '{query}'");

        return new ExternalWeatherResponse
        {
            Location = new ExternalLocation
            {
                Name = query,
                Region = "ON",
                Country = "Canada"
            },
            Current = new ExternalCurrentCondition
            {
                TempF = 72.0,
                Condition = new ExternalConditionDetail
                {
                    Text = "Partly cloudy",
                    Code = 1003
                }
            }
        };
    }
}

The adapter flattens the nested structure and converts units:

public sealed class WeatherApiAdapter : IWeatherService
{
    private readonly WeatherApiClient _client;

    public WeatherApiAdapter(WeatherApiClient client)
    {
        _client = client
            ?? throw new ArgumentNullException(nameof(client));
    }

    public WeatherForecast GetForecast(string city)
    {
        var response = _client.FetchWeather(city);

        double celsius =
            (response.Current.TempF - 32.0) * 5.0 / 9.0;

        return new WeatherForecast(
            $"{response.Location.Name}, " +
            $"{response.Location.Country}",
            Math.Round(celsius, 1),
            response.Current.Condition.Text,
            DateTimeOffset.UtcNow);
    }
}

This adapter does three things: flattens the nested response, converts Fahrenheit to Celsius, and formats the location string. All of it is mechanical translation -- no business logic, no side effects. That's the hallmark of a well-written adapter.

Adapter vs Alternatives: When to Choose What

Understanding when to use the adapter pattern in C# means knowing how it stacks up against similar patterns. Here's a comparison that helps clarify the decision:

Criteria Adapter Facade Strategy Direct Mapping
Primary purpose Convert one interface to another Simplify a complex subsystem Swap algorithms at runtime Transform data between types
When to use Incompatible interfaces Many moving parts to coordinate Multiple interchangeable behaviors Simple field-to-field mapping
Modifies behavior? No -- translates only May simplify or combine Replaces core behavior No -- converts only
Number of wrapped types Usually one Often multiple One (selected at runtime) N/A
Best for Integration boundaries API simplification Behavioral flexibility DTO/model conversion

The adapter pattern and the strategy pattern sometimes get confused when both involve programming against interfaces. The key difference is intent. An adapter makes an existing implementation fit an expected interface. A strategy lets you choose between multiple implementations of the same interface. You might use both together -- a strategy selects which payment provider to use, and an adapter wraps each provider's SDK behind a common interface.

The facade simplifies complexity. The adapter resolves incompatibility. If you're wrapping a single class to make its interface compatible, that's an adapter. If you're wrapping five classes to present a simpler API, that's a facade. Understanding inversion of control helps clarify why these boundaries matter -- your high-level code should depend on abstractions, not on the specific interfaces of third-party or legacy systems.

Red Flags: When the Adapter Pattern Is Overkill

Even when an adapter looks applicable, there are signs it's adding more complexity than value.

Your adapter contains business logic. If the adapter is validating inputs, applying rules, or making decisions beyond simple translation, it's doing too much. Pull that logic into a service layer and keep the adapter focused on interface conversion.

You're adapting interfaces you own and can change. This is the most common misuse. If you control the source interface and the target interface, align them directly. An adapter exists for situations where changing one side isn't feasible.

The adapted interface is trivially different. If the only difference between two interfaces is a method name or a parameter order, a simple wrapper method or extension method is sufficient. The full pattern -- with its own class, constructor injection, and DI registration -- is overhead for trivial mismatches.

You're creating adapters for testing only. If you're writing an adapter solely to make a class testable, consider whether you should be using an interface on the original class instead. Adapters for testability are a code smell that suggests the original design needs improvement.

Frequently Asked Questions

What is the adapter pattern in C# and when should I use it?

The adapter pattern is a structural design pattern that converts the interface of one class into another interface that clients expect. Use it when you need to integrate code with incompatible interfaces -- typically when wrapping legacy systems, third-party libraries, or external APIs. It acts as a translator, letting two pieces of code work together without modifying either one. It's one of the most practical patterns in the design patterns catalog because it solves a problem that comes up constantly in real-world development.

How is the adapter pattern different from the facade pattern?

An adapter converts one interface to match another expected interface. The facade pattern simplifies a complex subsystem by providing a unified, higher-level interface. An adapter typically wraps a single class and performs mechanical translation. A facade typically wraps multiple classes and orchestrates their interactions. If you're making one thing compatible, use an adapter. If you're making many things simpler, use a facade.

Should I use the adapter pattern for simple DTO mapping?

No. If you're just mapping properties from one class to another -- like converting a database entity to an API response DTO -- a mapping method or a library like AutoMapper is more appropriate. The adapter pattern is designed for bridging incompatible interfaces, not for field-level data transformation. Using a full adapter for simple mapping adds unnecessary indirection.

Can I use multiple adapters together in C#?

Yes. You might have an adapter for a legacy database, another for a third-party email service, and another for an external API. Each adapter implements a domain interface and wraps a specific external dependency. This is a common architecture in systems with multiple integration points. Each adapter is independent and focused on its own translation concern. You can register them all through dependency injection and let the container resolve the right one.

What is the difference between a class adapter and an object adapter?

A class adapter uses inheritance to adapt one interface to another -- the adapter class inherits from the adaptee and implements the target interface. An object adapter uses composition -- the adapter holds a reference to the adaptee and delegates calls to it. In C#, the object adapter is almost always preferred because C# supports single inheritance only. Composition also provides better flexibility, testability, and adherence to inversion of control principles.

When should I use the strategy pattern instead of the adapter pattern?

Use the strategy pattern when you need to choose between different algorithms or behaviors at runtime. Use an adapter when you need to make an existing implementation conform to a different interface. Strategies swap behavior. Adapters translate interfaces. They solve fundamentally different problems, though they can work together -- you might use strategies to select a payment provider and adapters to wrap each provider's SDK.

Does the adapter pattern violate SOLID principles?

No -- when used correctly, the adapter pattern supports SOLID principles. It upholds the Single Responsibility Principle by keeping translation logic separate from business logic. It supports the Dependency Inversion Principle by letting high-level code depend on abstractions rather than concrete third-party types. It enables the Open/Closed Principle by allowing you to add new integrations (new adapters) without modifying existing code. The only risk is overuse -- creating adapters where they aren't needed adds complexity that undermines the clarity SOLID is meant to provide.

Wrapping Up the Adapter Pattern Decision Guide

Deciding when to use the adapter pattern in C# comes down to one core question: do you have incompatible interfaces that need to work together without modifying either side? If the answer is yes and you can't change the source interface, an adapter gives you a clean, testable, maintainable bridge.

The decision framework is practical. Check for interface incompatibility, verify that you can't just change one side, confirm the translation logic is focused and mechanical, and assess whether the abstraction boundary will pay off over time. The scenarios in this article -- legacy system wrapping, third-party library adaptation, event format bridging, and external API conversion -- cover the most common real-world situations where an adapter earns its place.

Start simple. If a mapping method or extension method handles the job, use that. If you find yourself building increasingly complex translation logic, if the external dependency might change, or if you need to test your code in isolation from external systems, that's your signal to reach for an adapter. Keep adapters thin, focused on translation, and free of business logic. Let the pattern do what it does best -- make incompatible things work together.

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!

How to Implement Adapter Pattern in C#: Step-by-Step Guide

Learn how to implement adapter pattern in C# with a step-by-step guide covering target interfaces, adaptees, object adapters, and dependency injection registration.

Adapter Design Pattern in C#: Complete Guide with Examples

Master the adapter design pattern in C# with practical examples showing interface conversion, legacy integration, and third-party library wrapping.

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