.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
// 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)
// 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
// 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
// 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 lossInterview 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
IPaymentGatewayadapter wraps Stripe, yourIEmailSenderadapter wraps SendGrid, yourICacheServiceadapter 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."