What’s so special about interfaces anyway?  A quick dive into  extracting value from interfaces in C# through a real-world example.

Scenario

Recently Facundo Gauna and I set  out to develop a system that highlights the vast performance and  maintainability gaps between effective and ineffective implementations  of microservices.  While working on the system, we discovered that the  data we were consuming from a REST API was also available to us in a  format that we could toss into a database or even use in flat files if  we wanted to.  This was great news for us because it removed the limit  for us on how many requests we could make to access the data (aka API  rate-limiting).  So we implemented a database and moved on to changing  our code to use that database.

Initial State

By the time we discovered the raw data, we had already written several calls to the REST API using lines like this:

var result = await _httpClient.GetAsync(requestUri);

There’s nothing inherently wrong with that line.  In fact, it’s  perfectly fine.  But to use the database, we needed to replace every  HTTP request with a request to the database.  Additionally, I had a  curiosity regarding the performance flat files vs a database, so I also  wanted to test a third method of data access.  Here’s what the initial  implementation looked like (notice that our core logic had a hard  dependency on the specific class we were using to access the data):

Core
Http Client

This is a sign of poor planning (

:hand:

guilty!). In order to access the data another way, we needed to replace  every call to the Http Client with another way of accessing the data.   If we wanted to switch back again later, we would need to do another  refactor.  Yuck!  At this point, I recalled reading Robert Martin’s (aka  Uncle Bob) book, Clean Code.   In his book, Uncle Bob suggests separating business logic from data  access classes such that the business logic/core of an application  doesn’t require change when data access classes change or get replaced.   In practice, this means that an interface should be passed in place of  concrete data access classes.  This allows the core of an application to  know how to make requests to the classes that will access the data  (methods to call, data to send, and data to receive), but know nothing  else about the implementation of the class (how it interacts with the  data).
To drive the point home, an analogy is that a power company (data access class), a generator (data access class), and an inverter (data access class) all produce power in different ways.  Each of them has an outlet (interface) that an electrical cord (application core) can plug into regardless of the fact that the implementation (data access class) is different for each one.  They all provide electric in different ways, but the outlet (interface) still lets the electrical cord (application core) use each implementation without needing to change or know which one it is using.  Here’s what that looks like:

Power Cord (Core)
Socket (Interface)
Generator (Implementation)
Inverter (Implementation)
Power Company (Implementation)

Ultimately, we opted not to do a find and replace for implementing  the database, but instead to implement an interface and some data access  classes that implemented the interface.  This would allow us to swap on  the fly or easily try out additional data access layers if we found one  that interested us.

Implementing the Interface

To begin implementing the interface, we had to go to each of the  places where we wrote HTTP requests and replace those requests with a  call to an interface.  Behind the scenes, that interface needed to be  implemented by concrete classes that would have two primary tasks:

  1. Make a request to the appropriate datasource (API or database).
  2. Form and return the objects that our calling methods expected to receive.

For the sake of simplicity, I am going to narrow the focus to a single endpoint that gets the details of a food item.

Step 1: Create an Interface

public interface IDataAccessor
{
    Task<GetFoodResult> GetFood(List<int> ids);
}

The method in this interface takes in a list of integers and returns  an object containing a list of corresponding food items.  That’s all  there is to it.  Pretty simple.

Step 2: Create the Implementing Classes

We began with the class that would be interacting with the REST API.   I have removed the code to show the concept in a more focused manner.

REST API Data Access Class

public class RestDataAccessor : IDataAccessor
{
    public async Task<GetFoodResult> GetFood(List<int> ids)
    {
        // 1. Retrieve data from the API
        // 2. Convert the HTTP response to an object
        // 3. Return the object
    }
}

This gave us:

Interface
RestDataAccessor

Database Data Access Class

public class DbDataAccessor : IDataAccessor
{
    public async Task<GetFoodResult> GetFood(List<int> ids)
    {
        // 1. Retrieve data from the database
        // 2. Convert database response to an object
        // 3. Return the object
    }
}

And now:

Interface
RestDataAccessor
RestDataAccessor

Step 3: Call the Interface

In the places where we were making direct HTTP requests, we now needed to call the interface instead.  So this:

var result = await _httpClient.GetAsync(requestUri);

Was altered to look more like this:

var result = await _dataAccessor.GetFoodAsync(ids);

Doesn’t look much different, huh?  After this was completed, we were now matching our electrical cord example with this:

Application Core
Interface
RestDataAccessor
RestDataAccessor

Making it Configurable

Our primary goal when we began this task was to produce a solution  that would allow us to easily swap out the concrete classes that we  created (DbDataAccessor and RestDataAccessor).   With the work we had done to this point, code changes were still  required to choose which data source we wanted to use.  This meant that  we had to go in to our code and change all lines with new <type>DataAccessor to new <differentType>DataAccessor,  do a build, and then release.  But since any of those classes were  going to be implementing the IDataAccessor interface, we were able to  leverage dependency injection and app settings in ASP.Net Core.

In the ConfigureServices method of our Startup.cs, we added this:

var datasources = Configuration.GetSection("Datasources");
var datasource = datasources.GetValue<string>("Datasource");
if (datasource == "NdbApi")
{
    services.AddHttpClient();
    var apiSettings = datasources.GetSection("NdbApi").Get<NdbApiSettings>();
    services.AddSingleton(apiSettings);
    services.AddSingleton<IDataAccessor, RestDataAccessor>();
}
else if (datasource == "Database")
{
    var connectionString = datasources.GetValue<string>("Database:ConnectionString");
    services.AddSingleton(connectionString);
    services.AddDbContext<DbContext>(options => options.UseSqlServer(connectionString));
    services.AddTransient<IDataAccessor, DbDataAccessor>();
}
else
{
    throw new Exception("No datasource specified");
}

From a high level, the block above is looking at a field from app  settings and determining which data source the application should use.   Then, it is adding to the services any of the required dependencies that  it will need to inject.  The key lines are these two:

services.AddSingleton<IDataAccessor, RestDataAccessor>();
services.AddTransient<IDataAccessor, DbDataAccessor>();

Those two lines specify that we want to add the IDataAccessor  interface to the services with a specific implementation.  Once that is  complete, the class that uses the data accessor (var result = await _dataAccessor.GetFoodAsync(ids);) no longer needs to do a new <type>DataAccessor().   Instead, that class can have a constructor that receives the  IDataAccessor interface from the service list via dependency injection.   That looks like this:

public GetFoodController([FromServices]IDataAccessor dataAccessor)
{
    _dataAccessor = dataAccessor;
}

Notice that the class does not care which specific implementation is  behind the interface.  All it cares about is that it has the interface.

Now, one last piece for configurability.  Here’s what our appsettings.json looks like:

}
  "Datasources": {
    "Datasource": "Database",
    "Database": {
      "ConnectionString": "<redacted>"
    },
    "NdbApi": {
      "Key": "<redacted>",
      "Uri": "<url>"
    }
  }
}

The most interesting line in there is this one "Datasource": "Database" because that’s what we are using in our Startup.cs to determine which  datasource to use when the application starts.  So, if we wanted to  suddenly switch to the REST API, we would only need to change "Datasource": "Database" to "Datasource": "NdbApi" and restart the application.  No rebuilding or redeploying.

Wrap Up

Interfaces are a very powerful tool when used appropriately, but they  don’t belong everywhere.  For the places where they do make sense, here  are a few benefits of their implementation:

  • Increased testability (easy mocking)
  • Modular code (plug and play)
  • Greater degree of configurability
  • Core logic is more protected from bugs that can result from refactoring

If you would like to follow the progress of our microservices journey, stay tuned here or follow me on twitter @FundamentalDev.  Facundo can be found on Twitter @gaunacode.

The code used for this article can be found here in this repository (Look at the GetFood folder specifically).