Technologies: C#, .NET Core 3.1, xUnit, Newtonsoft.Json
Table of Contents
- 5.1 Step 1 — Initial state: reading from file
- 5.2 Step 2 — Two data sources with conditional logic (inline)
- 5.3 Step 3 — Two separate supplier classes
- 5.4 Step 4 — Introduction of the Adapter interface
- 5.5 Step 5 — Dependency Injection
- 5.6 Step 6 — Adapt on return types (Result Wrapper)
- Final full example: ThirdPartyApiAdapter
- UML diagram of the Adapter pattern
- Linked Design Patterns
- Structure of the demonstration project
- Key Takeaways
- Summary and conclusion
1. Course Overview
The C# Design Patterns: Adapter course is presented by Steve Smith (ardalis), an experienced .NET developer, architect and trainer. His corporate workshops and Pluralsight courses have helped thousands of developers write better code.
The major themes covered are:
- What problem is the Adapter pattern supposed to solve?
- What software design principles apply to this pattern?
- How to apply this pattern to refactor and improve existing code?
- What other design patterns are similar or often used in conjunction with the Adapter pattern?
By the end of the course, you will be able to recognize situations where the Adapter pattern is appropriate and apply it with confidence to your code.
2. The problem that the Adapter pattern solves
2.1 Analogy with the real world
The Adapter pattern aims to resolve the problem of incompatible interfaces. There is client code that must use one or more service providers, but the interface that the client is configured to call does not match the one that the service provider defines.
The classic real-world comparison is electrical outlet adapters:
- Electrical outlets are real-world service providers (they supply electricity).
- In the United States, sockets have a specific interface. Other countries have their own interfaces and often provide different voltages.
- Electronic devices typically have a single plug designed to work with a specific interface and cannot use incompatible outlets on their own.
- Adapters are available to convert, for example, a device designed for a European outlet to plug into a US outlet.
Definition: Adapters convert the interface of a class into an interface that a client expects. This is their only reason for being. In software, adapters are sometimes called wrappers because they wrap an incompatible object into a compatible object.
2.2 Demonstration of the problem in code
The seed code is a Star Wars character display service (StarWarsCharacterDisplayService). In its initial state, it reads characters from a JSON file on the local disk, formats them, and displays them.
The data file People.json contains:
[
{
"name": "Luke Skywalker",
"hair_color": "blond",
"gender": "male"
},
{
"name": "C-3PO",
"hair_color": "n/a",
"gender": "n/a"
},
{
"name": "R2-D2",
"hair_color": "n/a",
"gender": "n/a"
},
{
"name": "Darth Vader",
"hair_color": "none",
"gender": "male"
},
{
"name": "Leia Organa",
"hair_color": "brown",
"gender": "female"
}
]
The basic data model is the Person class:
namespace DesignPatternsInCSharp.Adapter
{
public class Person
{
public virtual string Name { get; set; }
public virtual string Gender { get; set; }
[Newtonsoft.Json.JsonProperty("hair_color")]
public virtual string HairColor { get; set; }
}
}
And the ApiResult<T> class to deserialize paginated API responses:
using System.Collections.Generic;
namespace DesignPatternsInCSharp.Adapter
{
public class ApiResult<T>
{
public int Count { get; set; }
public List<T> Results { get; set; }
}
}
3. Related design principles
Several design principles suggest when to use an adapter as a solution to certain interface incompatibility problems.
Replace conditional logic with polymorphism
This is a specific refactoring which consists of establishing a common interface for several related operations. In the code seen previously, all the conditional logic could be replaced if we could call a method to obtain data from a common abstraction.
Loose Coupling
Code should be loosely coupled to third-party dependencies (specific files or APIs). This means that it should be possible to plug in different service providers without having to modify existing code.
Testability
This goes hand in hand with weak coupling: strong coupling to infrastructure concerns makes code very difficult to unit test. Unit tests should be able to run without infrastructure present. The initial code simply doesn’t work if the JSON file or Star Wars API is not available.
SOLID Principles
- Single Responsibility Principle (SRP): Small, focused types that only do one thing are easy to scale because they will be small and focused.
- Interface Segregation Principle (ISP): types with few public methods and properties are easier to adapt.
- Liskov Substitution Principle (LSP): By following the SRP and ISP, it is less likely that one will decide not to implement a large interface altogether.
- Open/closed principle (OCP): we want to be able to modify the behavior of the client code without having to change its code. The
ListCharactersmethod should be open to extension (new data sources) but closed to modification (no need to change existing code).
4. The two types of Adapter
There are two variations of the Adapter pattern, which differ in how they reference the class they wrap (the adapted).
4.1 Object Adapter (composition)
- Contains an instance of the appropriate class (composition).
- Can implement an adapter interface or inherit from an adapter type.
- Uses composition and simple inheritance.
- This is the preferred variant in C# because C# does not support multiple inheritance.
- The design philosophy of C# is to prefer composition over inheritance.
Structure:
Client --> IAdapter <-- SpecificAdapter --> Adaptee
The client can work with one or more incompatible classes (the adaptees) by referencing and calling methods on an adapter abstraction. A specific adapter is created for each specific adaptee.
4.2 Class Adapter (multiple inheritance)
- Inherits from both adapter and adapter type.
- Requires multiple inheritance, which is not supported in C# for classes.
- A variation in C# is to inherit from the adapted and implement the target interface (without multiple class inheritance).
In C#, you will mainly use composition-based object adapters.
5. Progressive application of the Adapter pattern
The course presents a 6-step progression that illustrates how to refactor code to the Adapter pattern.
5.1 Step 1 — Initial state: reading from file
The service reads characters from a JSON file, formats them and returns them. A single TestRunner calls the service.
// Namespace: DesignPatternsInCSharp.Adapter.Initial
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
public class StarWarsCharacterDisplayService
{
public async Task<string> ListCharacters()
{
string filePath = @"Adapter/People.json";
var people = JsonConvert.DeserializeObject<List<Person>>(
await File.ReadAllTextAsync(filePath));
var sb = new StringBuilder();
int nameWidth = 30;
sb.AppendLine($"{"NAME".PadRight(nameWidth)} {"HAIR"}");
foreach (Person person in people)
{
sb.AppendLine($"{person.Name.PadRight(nameWidth)} {person.HairColor}");
}
return sb.ToString();
}
}
Test:
public class TestRunner
{
private readonly ITestOutputHelper _output;
public TestRunner(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public async Task DisplayCharacters()
{
var service = new StarWarsCharacterDisplayService();
var result = await service.ListCharacters();
_output.WriteLine(result);
}
}
Problem: The code is rigid. If you want to add another source (e.g.: an API), you must modify the class directly.
5.2 Step 2 — Two data sources with conditional logic (inline)
To support two data sources, we modified the method to accept a CharacterSource parameter (an enum). All data retrieval logic is inlined in the method via if/else blocks.
// Namespace: DesignPatternsInCSharp.Adapter.TwoProviders
public class StarWarsCharacterDisplayService
{
public enum CharacterSource
{
File,
Api
}
public async Task<string> ListCharacters(CharacterSource source)
{
List<Person> people;
if (source == CharacterSource.File)
{
string filePath = @"Adapter/People.json";
people = JsonConvert.DeserializeObject<List<Person>>(
await File.ReadAllTextAsync(filePath));
}
else if (source == CharacterSource.Api)
{
using (var client = new HttpClient())
{
string url = "https://swapi.co/api/people";
string result = await client.GetStringAsync(url);
people = JsonConvert.DeserializeObject<ApiResult<Person>>(result).Results;
}
}
else
{
throw new Exception("Invalid character source");
}
var sb = new StringBuilder();
int nameWidth = 30;
sb.AppendLine($"{"NAME".PadRight(nameWidth)} {"HAIR"}");
foreach (Person person in people)
{
sb.AppendLine($"{person.Name.PadRight(nameWidth)} {person.HairColor}");
}
return sb.ToString();
}
}
Tests:
[Fact]
public async Task DisplayCharactersFromFile()
{
var service = new StarWarsCharacterDisplayService();
var result = await service.ListCharacters(StarWarsCharacterDisplayService.CharacterSource.File);
_output.WriteLine(result);
}
[Fact]
public async Task DisplayCharactersFromApi()
{
var service = new StarWarsCharacterDisplayService();
var result = await service.ListCharacters(StarWarsCharacterDisplayService.CharacterSource.Api);
_output.WriteLine(result);
}
Identified issues:
- Code non-sustainable: if you add another different source, you must modify this code again.
- Lots of duplication in the way we retrieve the list of people.
- Does not respect SOLID principles:
- Violates the SRP: the service is doing too many things.
- Violates OCP: every time we want to change the source, we must modify the method. It is not closed to modification during an extension.
5.3 Step 3 — Two separate supplier classes
First refactoring: extract data fetching logic into two separate classes. The two classes intentionally have different signatures to illustrate the incompatibility of interfaces.
// Namespace: DesignPatternsInCSharp.Adapter.TwoProviderClasses
// Source fichier
public class CharacterFileSource
{
public async Task<List<Person>> GetCharactersFromFile(string filename)
{
var characters = JsonConvert.DeserializeObject<List<Person>>(
await File.ReadAllTextAsync(filename));
return characters;
}
}
// Source API
public class StarWarsApi
{
public async Task<List<Person>> GetCharacters()
{
using (var client = new HttpClient())
{
string url = "https://swapi.co/api/people";
string result = await client.GetStringAsync(url);
var people = JsonConvert.DeserializeObject<ApiResult<Person>>(result).Results;
return people;
}
}
}
The service now uses these classes:
public class StarWarsCharacterDisplayService
{
public enum CharacterSource { File, Api }
public async Task<string> ListCharacters(CharacterSource source)
{
List<Person> people;
if (source == CharacterSource.File)
{
string filePath = @"Adapter/People.json";
var characterSource = new CharacterFileSource();
people = await characterSource.GetCharactersFromFile(filePath);
}
else if (source == CharacterSource.Api)
{
var swapiSource = new StarWarsApi();
people = await swapiSource.GetCharacters();
}
else
{
throw new Exception("Invalid character source");
}
var sb = new StringBuilder();
int nameWidth = 30;
sb.AppendLine($"{"NAME".PadRight(nameWidth)} {"HAIR"}");
foreach (Person person in people)
{
sb.AppendLine($"{person.Name.PadRight(nameWidth)} {person.HairColor}");
}
return sb.ToString();
}
}
Observation:
CharacterFileSource.GetCharactersFromFile(string filename)has a different signature thanStarWarsApi.GetCharacters(). This is exactly where the Adapter pattern shines: bridging this incompatibility.
5.4 Step 4 — Introduction of the Adapter interface
This is the central step of the pattern. We identify a common interface (ICharacterSourceAdapter) and we create specific adapters for each source.
Thinking steps for choosing interface:
- Look at how
people(the list of people) is used in the client: only in aforeach, so we only need anIEnumerable<Person>. - Choose the interface that requires the least information possible to execute the method.
- We do not want to pass a
filePathin the common interface because this parameter does not make sense for an API source. - Since all calls are
async, the interface returns aTask<IEnumerable<Person>>.
Adapter interface:
// Namespace: DesignPatternsInCSharp.Adapter.AdapterIntroduction
using System.Collections.Generic;
using System.Threading.Tasks;
public interface ICharacterSourceAdapter
{
Task<IEnumerable<Person>> GetCharacters();
}
Adapt for API source (simple — signature already matches):
// StarWarsApi + son adapter dans le même fichier
public class StarWarsApi
{
public async Task<List<Person>> GetCharacters()
{
using (var client = new HttpClient())
{
string url = "https://swapi.co/api/people";
string result = await client.GetStringAsync(url);
var people = JsonConvert.DeserializeObject<ApiResult<Person>>(result).Results;
return people;
}
}
}
public class StarWarsApiCharacterSourceAdapter : ICharacterSourceAdapter
{
private StarWarsApi _starWarsApi = new StarWarsApi();
public async Task<IEnumerable<Person>> GetCharacters()
{
return await _starWarsApi.GetCharacters();
}
}
Adapting the API is trivial: it simply delegates to the
StarWarsApiinstance. TheGetCharacters()interface already matches.
Adapt for file source (requires more work — incompatible signature):
// CharacterFileSource + son adapter dans le même fichier
public class CharacterFileSource
{
public async Task<List<Person>> GetCharactersFromFile(string filename)
{
var characters = JsonConvert.DeserializeObject<List<Person>>(
await File.ReadAllTextAsync(filename));
return characters;
}
}
public class CharacterFileSourceAdapter : ICharacterSourceAdapter
{
private CharacterFileSource _fileSource = new CharacterFileSource();
private string _fileName;
public CharacterFileSourceAdapter(string fileName)
{
_fileName = fileName;
}
public async Task<IEnumerable<Person>> GetCharacters()
{
return await _fileSource.GetCharactersFromFile(_fileName);
}
}
The
fileNamewhich was a method parameter has been moved to constructor argument of adapt.
Customer service refactored:
public class StarWarsCharacterDisplayService
{
public async Task<string> ListCharacters(CharacterSource source)
{
ICharacterSourceAdapter characterSource;
if (source == CharacterSource.File)
{
string filePath = @"Adapter/People.json";
characterSource = new CharacterFileSourceAdapter(filePath);
}
else if (source == CharacterSource.Api)
{
characterSource = new StarWarsApiCharacterSourceAdapter();
}
else
{
throw new Exception("Invalid character source");
}
// L'appel est maintenant polymorphique — peu importe le type d'adapter
var people = await characterSource.GetCharacters();
var sb = new StringBuilder();
int nameWidth = 30;
sb.AppendLine($"{"NAME".PadRight(nameWidth)} {"HAIR"}");
foreach (Person person in people)
{
sb.AppendLine($"{person.Name.PadRight(nameWidth)} {person.HairColor}");
}
return sb.ToString();
}
}
Key improvement: The
ifstatement no longer handles data loading. It only creates the instance of the right type of adapter. The actual call to load people is done once and polymorphically. Theifblock could be replaced by a factory in a future refactoring.
5.5 Step 5 — Dependency Injection
To allow the dependency injection container (IoC container) to manage the lifetime of instances, we inject the dependencies via the constructors of the adapters instead of instantiating them directly.
Adapt file with injection:
// Namespace: DesignPatternsInCSharp.Adapter.DependencyInjection
public class CharacterFileSourceAdapter : ICharacterSourceAdapter
{
private string _fileName;
private readonly CharacterFileSource _characterFileSource;
public CharacterFileSourceAdapter(string fileName, CharacterFileSource characterFileSource)
{
_fileName = fileName;
_characterFileSource = characterFileSource;
}
public async Task<IEnumerable<Person>> GetCharacters()
{
return await _characterFileSource.GetCharactersFromFile(_fileName);
}
}
Adapt API with injection:
public class StarWarsApiCharacterSourceAdapter : ICharacterSourceAdapter
{
private StarWarsApi _starWarsApi;
public StarWarsApiCharacterSourceAdapter(StarWarsApi starWarsApi)
{
_starWarsApi = starWarsApi;
}
public async Task<IEnumerable<Person>> GetCharacters()
{
return await _starWarsApi.GetCharacters();
}
}
Service with full injection adapt:
public class StarWarsCharacterDisplayService
{
private readonly ICharacterSourceAdapter _characterSourceAdapter;
public StarWarsCharacterDisplayService(ICharacterSourceAdapter characterSourceAdapter)
{
_characterSourceAdapter = characterSourceAdapter;
}
public async Task<string> ListCharacters()
{
var people = await _characterSourceAdapter.GetCharacters();
var sb = new StringBuilder();
int nameWidth = 30;
sb.AppendLine($"{"NAME".PadRight(nameWidth)} {"HAIR"}");
foreach (Person person in people)
{
sb.AppendLine($"{person.Name.PadRight(nameWidth)} {person.HairColor}");
}
return sb.ToString();
}
}
Major advantage: The
StarWarsCharacterDisplayServicenow fully respects the OCP principle. It can be adjusted to work with any character data source without changing a single line of code inside this class. There is no longer aCharacterSourceenum or conditional logic.
Tests with explicit injection:
public class TestRunner
{
private readonly ITestOutputHelper _output;
public TestRunner(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public async Task DisplayCharactersFromFile()
{
string filename = @"Adapter/People.json";
var service = new StarWarsCharacterDisplayService(
new CharacterFileSourceAdapter(filename, new CharacterFileSource()));
var result = await service.ListCharacters();
_output.WriteLine(result);
}
[Fact]
public async Task DisplayCharactersFromApi()
{
var service = new StarWarsCharacterDisplayService(
new StarWarsApiCharacterSourceAdapter(new StarWarsApi()));
var result = await service.ListCharacters();
_output.WriteLine(result);
}
}
In an ASP.NET Core application, dependency wiring would be done in the
ConfigureServicesmethod of theStartup.csclass.
5.6 Step 6 — Adapt on return types (Result Wrapper)
The Adapter pattern can also apply to result types, not just service providers.
Scenario: Let’s imagine that the source file returns a list of Character instead of a list of Person. These two types are similar but have different properties:
Character | Person |
|---|---|
FullName (string) | Name (string) |
Hair (string) | HairColor (string) |
Gender (string) | Gender (string) |
The client code expects Person objects, so we must adapt Character to Person.
The Character class (third-party source):
// Namespace: DesignPatternsInCSharp.Adapter.ResultWrapper
public class Character
{
[Newtonsoft.Json.JsonProperty("name")]
public string FullName { get; set; }
public string Gender { get; set; }
[Newtonsoft.Json.JsonProperty("hair_color")]
public string Hair { get; set; }
}
The file source that returns Character:
public class CharacterFileSource
{
public async Task<List<Character>> GetCharactersFromFile(string filename)
{
var characters = JsonConvert.DeserializeObject<List<Character>>(
await File.ReadAllTextAsync(filename));
return characters;
}
}
To use Person as base adapter, its properties must be virtual:
// La classe Person avec propriétés virtuelles (dans le namespace Adapter)
public class Person
{
public virtual string Name { get; set; }
public virtual string Gender { get; set; }
[Newtonsoft.Json.JsonProperty("hair_color")]
public virtual string HairColor { get; set; }
}
If we did not have the possibility of making the properties
virtual, we would have had to introduce a new type to serve as an envelope and modify the client code to work with this new type.
Result adapter CharacterToPersonAdapter:
using System;
namespace DesignPatternsInCSharp.Adapter.ResultWrapper
{
public class CharacterToPersonAdapter : Person
{
private readonly Character _character;
public CharacterToPersonAdapter(Character character)
{
_character = character ?? throw new ArgumentNullException(nameof(character));
}
public override string Name
{
get => _character.FullName;
set => _character.FullName = value;
}
public override string HairColor
{
get => _character.Hair;
set => _character.Hair = value;
}
}
}
Adapt from source file using LINQ for transformation:
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class CharacterFileSourceAdapter : ICharacterSourceAdapter
{
private string _fileName;
private readonly CharacterFileSource _characterFileSource;
public CharacterFileSourceAdapter(string fileName, CharacterFileSource characterFileSource)
{
_fileName = fileName;
_characterFileSource = characterFileSource;
}
public async Task<IEnumerable<Person>> GetCharacters() =>
(await _characterFileSource.GetCharactersFromFile(_fileName))
.Select(character => new CharacterToPersonAdapter(character));
}
The LINQ
Selectinstruction is very well suited to this type of transformation and produces short and clear code.
Tests identical to the previous case:
[Fact]
public async Task DisplayCharactersFromFile()
{
string filename = @"Adapter/People.json";
var service = new StarWarsCharacterDisplayService(
new CharacterFileSourceAdapter(filename, new CharacterFileSource()));
var result = await service.ListCharacters();
_output.WriteLine(result);
}
[Fact]
public async Task DisplayCharactersFromApi()
{
var service = new StarWarsCharacterDisplayService(
new StarWarsApiCharacterSourceAdapter(new StarWarsApi()));
var result = await service.ListCharacters();
_output.WriteLine(result);
}
6. Final complete example: ThirdPartyApiAdapter
The demo project also contains a more complete example with a third-party service (ThirdPartyService) which has an even more different interface. This example illustrates how to adapt a full third-party API with its own DTO, enum, and JSON converter.
Third-party service types:
// Namespace: DesignPatternsInCSharp.Adapter.ThirdPartyApi
// Enum Gender
public enum Gender
{
Male,
Female,
NotApplicable
}
// DTO utilisé par le service tiers
public class PersonDTO
{
[JsonProperty("name")]
public string CharacterName { get; set; }
[JsonConverter(typeof(GenderConverter))]
public Gender Gender { get; set; }
}
// Résultat paginé du service tiers
public class PersonDTOApiResult
{
public int Count { get; set; }
public List<PersonDTO> Results { get; set; }
}
The JSON converter for the Gender enum:
public class GenderConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Gender gender = (Gender)value;
writer.WriteValue(gender.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var enumString = (string)reader.Value;
if (enumString == "n/a") return Gender.NotApplicable;
return Enum.Parse(typeof(Gender), enumString, true);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
}
The third-party service (incompatible):
public class ThirdPartyService
{
public async Task<List<PersonDTO>> ListCharactersAsync(int count)
{
var people = new List<PersonDTO>();
using (var client = new HttpClient())
{
string url = "https://swapi.co/api/people";
string result = await client.GetStringAsync(url);
people = JsonConvert.DeserializeObject<PersonDTOApiResult>(result).Results;
}
return people.Take(count).ToList();
}
}
Adapt it for third-party service — inherits from abstract class PeopleDataAdapter:
public abstract class PeopleDataAdapter
{
public abstract Task<List<Person>> GetPeople();
}
public class ThirdPartyApiAdapter : PeopleDataAdapter
{
public const int MAX_RESULT_COUNT = 100;
public override async Task<List<Person>> GetPeople()
{
var thirdPartyService = new ThirdPartyService();
var result = await thirdPartyService.ListCharactersAsync(MAX_RESULT_COUNT);
return result.Select(dto => new Person
{
Name = dto.CharacterName,
Gender = dto.Gender.ToString()
}).ToList();
}
}
Concrete adapters for other sources (with the abstract class):
// Adapter pour le fichier local
public class LocalFilePeopleAdapter : PeopleDataAdapter
{
public override async Task<List<Person>> GetPeople()
{
string filePath = @"Adapter/People.json";
return JsonConvert.DeserializeObject<List<Person>>(
await File.ReadAllTextAsync(filePath));
}
}
// Adapter pour l'API Star Wars
public class StarWarsApiPeopleAdapter : PeopleDataAdapter
{
public override async Task<List<Person>> GetPeople()
{
var people = new List<Person>();
using (var client = new HttpClient())
{
string url = "https://swapi.co/api/people";
string result = await client.GetStringAsync(url);
people = JsonConvert.DeserializeObject<ApiResult<Person>>(result).Results;
}
return people;
}
}
The final service with abstract class:
public class StarWarsCharacterDisplayService
{
private readonly PeopleDataAdapter _peopleDataAdapter;
public StarWarsCharacterDisplayService(PeopleDataAdapter peopleDataAdapter)
{
_peopleDataAdapter = peopleDataAdapter;
}
public async Task<string> ListCharacters()
{
var people = await _peopleDataAdapter.GetPeople();
var sb = new StringBuilder();
int nameWidth = 20;
sb.AppendLine($"{"NAME".PadRight(nameWidth)} {"GENDER"}");
foreach (Person person in people)
{
sb.AppendLine($"{person.Name.PadRight(nameWidth)} {person.Gender}");
}
return sb.ToString();
}
}
Complete unit tests:
public class AdapterTests
{
[Fact]
public async Task ListCharactersGivenStarWarsApiAdapterShouldReturnSomething()
{
var swAdapter = new StarWarsApiPeopleAdapter();
var service = new StarWarsCharacterDisplayService(swAdapter);
await CallListCharactersAndAssertContainsExpectedCharacters(service);
}
[Fact]
public async Task ListCharactersGivenLocalFileAdapterShouldReturnSomething()
{
var localAdapter = new LocalFilePeopleAdapter();
var service = new StarWarsCharacterDisplayService(localAdapter);
await CallListCharactersAndAssertContainsExpectedCharacters(service);
}
[Fact]
public async Task ListCharactersGivenThirdPartyApiAdapterShouldReturnSomething()
{
var adapter = new ThirdPartyApiAdapter();
var service = new StarWarsCharacterDisplayService(adapter);
await CallListCharactersAndAssertContainsExpectedCharacters(service);
}
private static async Task CallListCharactersAndAssertContainsExpectedCharacters(
StarWarsCharacterDisplayService service)
{
var result = await service.ListCharacters();
Assert.Contains("Luke Skywalker", result);
Assert.Contains("Darth Vader", result);
}
}
7. UML diagram of the Adapter pattern
The PlantUML diagram provided with the course illustrates the structure of a class adapter:
@startuml
title Class Adapter
class Client
interface IAdapter {
+void SomeMethod()
}
class SpecificAdapter {
+void SomeMethod()
}
class Adaptee {
+void IncompatibleMethod()
}
IAdapter <|-left- Client: Calls
IAdapter <|-down- SpecificAdapter: Implements
SpecificAdapter -right-|> Adaptee: Calls
@enduml
Reading the diagram:
- Client: Code that needs a feature but cannot call it directly.
- IAdapter: the interface that the client knows and can call.
- SpecificAdapter: implements
IAdapter.SomeMethod()internally by callingAdaptee.IncompatibleMethod(). - Adaptee: the existing class with an incompatible interface.
8. Related Design Patterns
The Adapter pattern is structurally similar to several other patterns. The difference lies in the intention:
| Pattern | Structure | Intent |
|---|---|---|
| Adapt | Similar to Decorator, Bridge, Proxy | Convert an incompatible interface to a compatible interface |
| Decorator | Similar to Adapt | Add functionality to an existing object |
| Bridge | Similar to Adapt | Allow interfaces and implementations to vary independently |
| Proxy | Similar to Adapt | Control access to a resource |
| Repository | Sometimes similar | Providing a common interface for persistence |
| Strategy | Frequently used with Adapter | Inject different behavior implementations into a client |
| Facade | Similar in intent | Simplify a complex set of operations (rather than allowing interchangeability) |
Important note: Patterns are not only about different ways of structuring code, but also about communicating design intent. These four patterns may have similar structures on paper, but their different names reflect how they are used.
Adapter vs Facade difference:
- The Adapter provides a way to easily switch between different incompatible operations.
- The Facade is often placed in front of several different types and aims to simplify a complex set of operations, not necessarily to allow interchangeability.
9. Structure of the demonstration project
The demo project is a .NET Core 3.1 solution that uses xUnit for testing.
Project file (DesignPatternsInCSharp.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0-preview-20191115-01" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Content Include="Adapter\People.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Structure of demo folders:
02/demos/DesignPatternsInCSharp/DesignPatternsInCSharp/Adapter/
├── 01_Initial/
│ ├── StarWarsCharacterDisplayService.cs (lecture fichier uniquement)
│ └── TestRunner.cs
├── 02_TwoProviders/
│ ├── StarWarsCharacterDisplayService.cs (logique conditionnelle inline)
│ └── TestRunner.cs
├── 03_TwoProviderClasses/
│ ├── CharacterFileSource.cs
│ ├── StarWarsApi.cs
│ ├── StarWarsCharacterDisplayService.cs (classes séparées + if/else)
│ └── TestRunner.cs
├── 04_AdapterIntroduction/
│ ├── CharacterFileSource.cs (+ CharacterFileSourceAdapter)
│ ├── CharacterSource.cs (enum)
│ ├── ICharacterSourceAdapter.cs (interface)
│ ├── StarWarsApi.cs (+ StarWarsApiCharacterSourceAdapter)
│ ├── StarWarsCharacterDisplayService.cs (utilise l'interface)
│ └── TestRunner.cs
├── 05_DependencyInjection/
│ ├── CharacterFileSource.cs
│ ├── CharacterFileSourceAdapter.cs (injection via constructeur)
│ ├── ICharacterSourceAdapter.cs
│ ├── StarWarsApi.cs (+ StarWarsApiCharacterSourceAdapter DI)
│ ├── StarWarsCharacterDisplayService.cs (injection complète)
│ └── TestRunner.cs
├── 06_ResultWrapper/
│ ├── Character.cs (type incompatible)
│ ├── CharacterFileSource.cs (retourne Character)
│ ├── CharacterFileSourceAdapter.cs (avec Select LINQ)
│ ├── CharacterToPersonAdapter.cs (hérite de Person)
│ ├── ICharacterSourceAdapter.cs
│ ├── StarWarsApi.cs
│ ├── StarWarsCharacterDisplayService.cs
│ └── TestRunner.cs
├── ThirdPartyApi/
│ ├── Gender.cs
│ ├── GenderConverter.cs
│ ├── PersonDTO.cs
│ ├── PersonDTOApiResult.cs
│ └── ThirdPartyService.cs
├── AdapterTests.cs (tests de la version finale)
├── ApiResult.cs
├── LocalFilePeopleAdapter.cs
├── PeopleDataAdapter.cs (classe abstraite)
├── People.json
├── Person.cs
├── StarWarsApiPeopleAdapter.cs
├── StarWarsCharacterDisplayService.cs (version finale)
├── ThirdPartyApiAdapter.cs
└── Adapter-PlantUML.txt
10. Key Takeaways
When to use the Adapter pattern
- When you have client code that cannot directly call an existing class due to an interface incompatibility.
- When you want to reuse an existing class without modifying its code.
- When you integrate third-party libraries or services with interfaces different from your internal abstractions.
- When you want to eliminate conditional logic (if/else, switch) based on source or provider type.
Steps to apply the Adapter pattern
- Identify classes with incompatible interfaces that the client should use.
- Define a common interface (target) that the client expects. Choose the interface with the least information required.
- Create adapters: a class for each adapter that implements the common interface.
- Delegate common interface calls to incompatible adapted methods.
- Inject the adapter into the client via the constructor (dependency injection).
Best practices
- Prefer composition to inheritance (object adapter) in C#.
- Inject dependencies into adapters via constructors to facilitate testing and lifecycle management.
- Adapters can also adapt return types, not just service providers.
- LINQ
Selectis a very handy tool for transforming collections when adapting return types. - Adapters should not modify the behavior of the adapter, only convert the interface.
What the Adapter brings
| Before Adapt | After Adapt |
|---|---|
| Conditional logic in the client | Clean polymorphic code |
| Strong coupling to implementations | Coupling on abstractions |
| Difficult to test code | Easily testable code |
| OCP Violation | Respects the OCP |
| SRP Violation | Respect the SRP |
| Difficult to add new sources | Easy addition of new adapters |
11. Summary and conclusion
Summary of key points
-
The intention of the Adapter pattern is to convert an incompatible interface into a compatible interface for a given client.
-
In C# you will use composition rather than multiple inheritance. Your adapter implementations will contain instances of the incompatible type and delegate calls to that instance’s incompatible methods or properties.
-
Adapters are very similar to many other patterns (Decorator, Proxy, Bridge) and are one of the most commonly used patterns.
-
In addition to wrapping incompatible service providers, adapters can also be used to wrap result types to make them compatible.
-
The source code is available on GitHub in the ardalis/DesignPatternsInCSharp repository.
Refactoring progress seen in the course
Étape 1 : Code simple avec une seule source (fichier)
↓
Étape 2 : Deux sources avec logique if/else inline (problème)
↓
Étape 3 : Extraction des deux sources dans des classes séparées
↓
Étape 4 : Introduction de l'interface ICharacterSourceAdapter + adapters
↓
Étape 5 : Injection de dépendances dans les adapters et le service
↓
Étape 6 : Adaptation aussi des types de résultat (Result Wrapper)
Links and Resources
- GitHub source code: ardalis/DesignPatternsInCSharp
- Trainer blog: ardalis.com
- Podcast: weeklydevtips.com
Search Terms
c-sharp · design · patterns · adapter · testing · architecture · c# · .net · development · pattern · conditional · demonstration · logic · principles · types