Learnixo

.NET & C# Development · Lesson 35 of 229

Adapter — Bridge Incompatible Interfaces

Adapter — Bridge Incompatible Interfaces

The Adapter pattern converts the interface of a class into another interface that clients expect. It lets classes work together that otherwise couldn't due to incompatible interfaces.


The Problem

C#
// Your code expects this interface:
public interface IPaymentGateway
{
    Task<PaymentResult> ChargeAsync(string cardToken, decimal amount, string currency);
}

// But you must integrate a third-party library with a completely different API:
public class StripeClient   // from Stripe NuGet package — you can't change this
{
    public async Task<StripeCharge> CreateChargeAsync(StripeChargeOptions options) { /* ... */ }
}

// Without Adapter: your code is tightly coupled to Stripe — impossible to swap later.

Object Adapter (Preferred)

C#
// Adapter wraps the incompatible class and implements your interface
public class StripePaymentAdapter : IPaymentGateway
{
    private readonly StripeClient _stripe;

    public StripePaymentAdapter(StripeClient stripe)
        => _stripe = stripe;

    public async Task<PaymentResult> ChargeAsync(string cardToken, decimal amount, string currency)
    {
        // Translate from your interface to Stripe's API
        var options = new StripeChargeOptions
        {
            Amount      = (long)(amount * 100),   // Stripe uses pence/cents
            Currency    = currency.ToLower(),
            Source      = cardToken,
            Description = "SystemForge order",
        };

        try
        {
            var charge = await _stripe.CreateChargeAsync(options);
            return new PaymentResult(
                Success:       charge.Status == "succeeded",
                TransactionId: charge.Id,
                Message:       charge.FailureMessage
            );
        }
        catch (StripeException ex)
        {
            return new PaymentResult(
                Success:       false,
                TransactionId: null,
                Message:       ex.Message
            );
        }
    }
}

// Consuming code never knows it's talking to Stripe:
public class CheckoutService(IPaymentGateway payment)
{
    public async Task<bool> ProcessAsync(string cardToken, decimal amount)
    {
        var result = await payment.ChargeAsync(cardToken, amount, "GBP");
        return result.Success;
    }
}

// Wire up in DI:
builder.Services.AddSingleton<StripeClient>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentAdapter>();

Legacy Code Adapter

C#
// Old XML-based service from 2008 — cannot be changed
public class LegacyReportingSystem
{
    public string GenerateXmlReport(int userId, string reportType, DateTime startDate, DateTime endDate)
    {
        // Returns XML string
        return $"<report type='{reportType}'>...</report>";
    }
}

// Modern interface your application uses:
public interface IReportGenerator
{
    Task<ReportDto> GenerateAsync(ReportRequest request);
}

public class LegacyReportAdapter : IReportGenerator
{
    private readonly LegacyReportingSystem _legacy;

    public LegacyReportAdapter(LegacyReportingSystem legacy)
        => _legacy = legacy;

    public Task<ReportDto> GenerateAsync(ReportRequest request)
    {
        // Adapt modern request to legacy parameters
        string xml = _legacy.GenerateXmlReport(
            request.UserId,
            request.Type.ToString(),
            request.Period.Start,
            request.Period.End
        );

        // Adapt legacy XML output to modern DTO
        var report = ParseXml(xml);
        return Task.FromResult(report);
    }

    private static ReportDto ParseXml(string xml)
    {
        // XML → DTO conversion
        return new ReportDto { Content = xml, Format = "xml" };
    }
}

Two-Way Adapter

C#
// Adapter that satisfies two different interfaces at once
public class UniversalLogAdapter : ILogger, Microsoft.Extensions.Logging.ILogger
{
    private readonly ILogger _internal;
    private readonly Microsoft.Extensions.Logging.ILogger _msLogger;

    // Implements both — useful when bridging two logging systems
}

When to Use

✓ Integrating third-party libraries you can't modify
✓ Wrapping legacy code with a modern interface
✓ Making multiple services with different APIs interchangeable
✓ Testing — adapter makes it easy to swap real for fake

✗ When you can change the original class directly (no adapter needed)
✗ When the interfaces differ so dramatically that adaptation causes data loss

Interview Answer

"The Adapter pattern wraps an incompatible class and exposes the interface your code expects — the classic use case is integrating a third-party library (Stripe, SendGrid) or legacy code you can't modify. Prefer the object adapter (composition) over the class adapter (inheritance): it's more flexible and works with sealed classes. In .NET, nearly every infrastructure integration is an adapter — your IPaymentGateway adapter wraps Stripe, your IEmailSender adapter wraps SendGrid, your ICacheService adapter wraps Redis. The key benefit is decoupling: consuming code depends only on the interface, so swapping the underlying provider requires changing only the adapter and its DI registration."