Adapter Pattern

Hello everyone, and welcome to .NET Core Central. In this blog post, I am going to walk through the Adapter Pattern.

The Adapter pattern is also part of the gang of four design patterns. And it is a structural design pattern.

The main intent of this pattern is to allow two classes to work together which will not otherwise due to incompatible contracts.

Adapter Pattern Usecase

So let us take an example to understand what it is. At a very high level as the name suggests it is an adapter, meaning it is used to adapt one type into another. That is the bottom line of this particular design pattern.

Now let us take an example. When we create a new .NET application, it comes with an out of box template for ASP.NET Core. And here I will be using ASP.NET 6 minimal API.

The Application for Adapter Pattern

And this template for minimal API comes out of the box with a Weather forecast API. And the weather forecast API returns an array of weather.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
       new WeatherForecast
       (
           DateTime.Now.AddDays(index),
           Random.Shared.Next(-20, 55),
           summaries[Random.Shared.Next(summaries.Length)]
       ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Create Provider class

Now let us consider that we want to extract this implementation into a different class. A class with the name WeatherForecastProvider.

So for that, first we will create a new class. And an interface that will be implemented by the class.

Secondly, I will cut and paste the record type WeatherForecast into this new class as a top-level type.

Thirdly, I will copy and paste the summaries array into this class.

Fourthly, I will copy and paste the implementation of returning the WeatherForecast array into a new method of the class called Get.

Fifthly, I will extract an interface out of this class and call it IWeatherForecastProvider.

namespace AdapterPattern.Demo;

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

internal interface IWeatherForecastProvider
{
    IEnumerable<WeatherForecast> Get();
}

internal class WeatherForecastProvider : IWeatherForecastProvider
{
    string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public IEnumerable<WeatherForecast> Get()
    {
        var forecast = Enumerable.Range(1, 5).Select(index =>
       new WeatherForecast
       (
           DateTime.Now.AddDays(index),
           Random.Shared.Next(-20, 55),
           summaries[Random.Shared.Next(summaries.Length)]
       ))
        .ToArray();
        return forecast;
    }
}

Update API Dependency

Once we moved all the logic into the WeatherForecastProvider class, I will update the API to inject the IWeatherForecastProvider interface. And then call the Get method to return WeatherForecast.

And also along with that, I will update the dependency injection container to register the WeatherForecastProvider class.

using AdapterPattern.Demo;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<IWeatherForecastProvider, WeatherForecastProvider>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/weatherforecast", ([FromServices] IWeatherForecastProvider weatherForecastProvider) =>
    weatherForecastProvider.Get())
    .WithName("GetWeatherForecast");

app.Run();

Now let us run this application. And once I run this application, I can go into swagger and try it out to get the weather in a JSON array format.

[
  {
    "date": "2021-12-08T20:33:33.960491-05:00",
    "temperatureC": 26,
    "summary": "Sweltering",
    "temperatureF": 78
  },
  {
    "date": "2021-12-09T20:33:33.9610627-05:00",
    "temperatureC": 36,
    "summary": "Bracing",
    "temperatureF": 96
  },
  {
    "date": "2021-12-10T20:33:33.9610738-05:00",
    "temperatureC": 35,
    "summary": "Hot",
    "temperatureF": 94
  },
  {
    "date": "2021-12-11T20:33:33.9610741-05:00",
    "temperatureC": 43,
    "summary": "Scorching",
    "temperatureF": 109
  },
  {
    "date": "2021-12-12T20:33:33.9610743-05:00",
    "temperatureC": 7,
    "summary": "Balmy",
    "temperatureF": 44
  }
]

The new requirement

Now let us say that we got a new requirement from one of our customers. And the requirement is to send the weather forecast information into a pipe-separated string.

So based on the requirement, we will need an adapter, which will adapt to the new client’s requirement. Hence, this is where the Adapter pattern will come in handy.

So, what we can do is, we can create a new class called WeatherAsStringAdapter. And then, in this new class, will create a public method Get that will return the weather as a string.

We will inject the IWeatherForecastProvider interface as a dependency to the constructor of this new class.

In the Get method, we will use the IWeatherForecastProvider to get the weather and convert the WeatherForecast array into a pipe-separated string.

namespace AdapterPattern.Demo;

internal interface IWeatherAsStringAdapter
{
    string Get();
}

internal class WeatherAsStringAdapter : IWeatherAsStringAdapter
{
    private readonly IWeatherForecastProvider weatherForecastProvider;

    public WeatherAsStringAdapter(IWeatherForecastProvider weatherForecastProvider)
    {
        this.weatherForecastProvider = weatherForecastProvider;
    }

    public string Get()
    {
        return string.Join(
            Environment.NewLine,
            weatherForecastProvider.Get().Select(w => $"{w.Date}|{w.TemperatureF}|{w.Summary}"));
    }
}

Update the API to use IWeatherAsStringAdapter

Firstly, let us add the newly created class WeatherAsStringAdapter to the dependency injection container. And for that, we will use the AddSingleton to add this class as a singleton instance.

Secondly, we will change the implementation of the weather forecast API. And instead of injecting IWeatherForecastProvider, we will inject IWeatherAsStringAdapter.

And finally, we will return the adapted string format of the weather.

using AdapterPattern.Demo;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<IWeatherForecastProvider, WeatherForecastProvider>();
builder.Services.AddSingleton<IWeatherAsStringAdapter, WeatherAsStringAdapter>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/weatherforecast", ([FromServices] IWeatherAsStringAdapter weatherAsStringProvide) =>
    weatherAsStringProvide.Get())
    .WithName("GetWeatherForecast");

app.Run();

Now if we run this application, what we are going to see is a string. Where each line is separated by a newline character. And then each attribute is separated by pipes.

12/9/2021 7:14:11 PM|0|Mild
12/10/2021 7:14:11 PM|25|Cool
12/11/2021 7:14:11 PM|11|Warm
12/12/2021 7:14:11 PM|16|Chilly
12/13/2021 7:14:11 PM|107|Balmy

Another requirement

Let us consider another new requirement, where we will need to return a CSV data format.

For this new requirement, now we can either go ahead and just change the code of WeatherAsStringProvide. Or we can install a third-party NuGet package and use that for creating CSV format.

So let us go ahead and use a third-party component that provides CSV format. For that, we will install the below-mentioned NuGet package.

<PackageReference Include="Csv" Version="2.0.62" />

Now similarly, we can create a new class called WeatherAsCsvAdapter and have a similar implementation as WeatherAsStringAdapter. But instead of returning a string, this will return CSV data.

using Csv;

namespace AdapterPattern.Demo;
internal interface IWeatherAsCsvAdapter
{
    string Get();
}

internal class WeatherAsCsvAdapter : IWeatherAsCsvAdapter
{
    private readonly IWeatherForecastProvider weatherForecastProvider;

    public WeatherAsCsvAdapter(IWeatherForecastProvider weatherForecastProvider)
    {
        this.weatherForecastProvider = weatherForecastProvider;
    }

    public string Get()
    {
        var headers = new[] { "Date", "Temp", "Summary" };
        var csv = weatherForecastProvider.Get().Select(w => new string[] { w.Date.ToString(), w.TemperatureF.ToString(), w.Summary });
        return CsvWriter.WriteToText(headers, csv);
    }

}

Use the IWeatherAsCsvAdapter

Now we can update the weather provider API to use this new adapter and send out a new version of the weather data in CSV format.

using AdapterPattern.Demo;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<IWeatherForecastProvider, WeatherForecastProvider>();
builder.Services.AddSingleton<IWeatherAsStringAdapter, WeatherAsStringAdapter>();
builder.Services.AddSingleton<IWeatherAsCsvAdapter, WeatherAsCsvAdapter>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/weatherforecast", ([FromServices] IWeatherAsCsvAdapter weatherAsCsvAdapter) =>
    weatherAsCsvAdapter.Get())
    .WithName("GetWeatherForecast");

app.Run();

And now if I run the application, I will see the result in CSV format.

Date,Temp,Summary
12/9/2021 7:40:39 PM,55,Mild
12/10/2021 7:40:39 PM,82,Freezing
12/11/2021 7:40:39 PM,35,Bracing
12/12/2021 7:40:39 PM,22,Mild
12/13/2021 7:40:39 PM,121,Freezing

Conclusion

As you can see, the adapter pattern can be very handy in a lot of scenarios. Especially dealing with external services for data consumption.

In our case, we could handle two different types of data structures returning from a service using two different adapters.

A Youtube video for this implementation is here.