Abstracting Infrastructure Concerns
Organizing and creating the architecture for a .NET application can be largely a matter of preference. How many projects should you use? What code belongs in each project? Let’s take a look at infrastructure concerns.
What is Infrastructure?
In my context, infrastructure refers to components that interact with external systems or frameworks, such as the following:
- Data Access (e.g. EntityFramework Core, Dapper)
- HTTP Requests (e.g. HttpClient)
- Emails (e.g. MimeKit)
- Messaging (e.g. RabbitMQ)
- Caching (e.g. Redis)
- Threading (e.g. Task.WhenAll, Task.Delay)
- System Clock (e.g. DateTime.UtcNow, DateTimeOffset.UtcNow)
- File Systems (e.g. Directory, File)
Prefer Abstractions
Wherever practical, I try to ensure these components provide their functionality via abstractions. This allows for the core logic that uses these components to be unaware of how they are implemented. This reduces the coupling between the external components and the core (internal) components of an application. This makes the code more complex, but also makes it easier to test and change over time. This is a tradeoff I am willing to make for all but the most trivial of applications.
Let’s take a look at a typical Web API solution. I would expect to find (at a minimum) the following:
- A Web API (ASP.NET Core) project for the API endpoints to be configured
- A Class Library project for all my core (domain) logic specific to the application
- A Class Library project for all my infrastructure concerns
A simple scenario is an API that interacts with a database. In this case, the Infrastructure project might have a dependency on Entity Framework Core or Dapper. You may create this solution:
MyApp.sln
=> MyApp.Api
=> MyApp.Core
=> MyApp.Data
Depending on your architecture, the project dependencies may be:
// N-tier Architecture
MyApp.Api => MyApp.Core => MyApp.Data
or
// Clean Architecture
MyApp.Api => MyApp.Core <= MyApp.Data
Over time, you may include more infrastructure, say accessing a 3rd party API. You have a few options for incorporating the new external dependency. One option would be to introduce a new project:
MyApp.sln
=> MyApp.Api
=> MyApp.Core
=> MyApp.Data
=> MyApp.ThirdPartyApi
Or you may refactor the code into a new Infrastructure
project, using separate directories for the
different external services:
MyApp.sln
=> MyApp.Api
=> MyApp.Core
=> MyApp.Infrastructure
=> Data
=> ThirdPartyApi
Either approach is fine. Your application will grow and you will find a way to organize the code as it grows.
Where do the Abstractions Live?
In my opinion, all the external systems should only be accessed via abstractions, typically
interfaces in .NET. The components in the infrastructure project are implementations of those
abstractions. Let’s say you have a domain entity called Widget
and you require a means to persist
it to a database and query the contents of this database. You could introduce an abstraction in
the Core
project:
public interface IWidgetDataAccess
{
int Create(Widget widget);
Widget GetWidget(int id);
IList<Widget> GetWidgets(Expression<Func<Widget, bool>> filter);
}
This makes sense when using the ‘clean architecture’ patterns. If you are using the ‘N-tier’ patterns,
it may make more sense to include these abstractions in the Infrastructure
project. In small
projects this can work quite well, as long as the components that implement the abstractions are
internal
not public
classes.
You could also consider creating a separate Class Library for these abstractions (e.g. MyApp.Abstractions
):
MyApp.sln
=> MyApp.Api
=> MyApp.Core
=> MyApp.Abstractions
=> MyApp.Infrastructure
=> Data
=> ThirdPartyApi
The important idea is that the Core
project should only depend on these abstractions, not the
implementations. This is the common thread between all these approaches.
What is the Best Level of Abstraction for Infrastructure Components?
Introducing abstractions is usually done to make the code easier to understand and maintain. Choosing the best level of abstraction can greatly depend on your context. It may be driven by the experiences and personalities of your team. It could be driven by standards the team must adhere to. Or it could be driven by components being abstracted. In the end, it depends on a number of factors that you will need to consider.
Let’s take the data access abstractions. Some will argue that using the Repository pattern is a good abstraction for data access. Others will argue that the built-in abstractions of Entity Framework are all you should require. Some might argue that the interface should only expose low-level api (Create/Read/Update/Delete) methods. Others might prefer to include more complex queries to be encapsulated behind a data access abstraction.
For example, the IWidgetDataAccess
above might expose some additional methods:
public interface IWidgetDataAccess
{
int Create(Widget widget);
int CreateOrUpdate(Widget widget);
Widget GetWidget(int id);
Widget GetWidgetIncludingMetadata(int id);
IList<Widget> GetWidgets(Expression<Func<Widget, bool>> filter);
IList<Widget> GetInStockWidgets();
}
Regardless of which approach you prefer, it’s the ability to use the abstractions within your core application logic that remains.
Let’s take the System Clock, as another example. If we need an abstraction for DateTimeOffset.UtcNow
,
should the abstraction be a mirror of the underlying implementation?
public interface ISystemClock
{
DateTimeOffset UtcNow
}
Or should there be a new abstraction?
public interface ISystemClock
{
Instance GetCurrentInstant()
}
The best advice is to follow fundamental design rules for abstraction and encapsulation. If you do, then you will unlikely make a choice that you can’t undo if you don’t like it. I typically recommend that you create abstractions that closely mimic the underlying implementation. At least start there. It reduces the burden on you and your team to create new abstractions when you may not be clear exactly how they will end up being used. Over time, you should be able to refactor the code if and when a new abstraction makes more sense in your context.
Dependency Injection Considerations
The only way any of this works is with the assistance of Dependency Injection. .NET applications have a good dependency injection framework built-in, making it easy to inject implementations of the abstractions. But you have some choices to make on where the rules of the dependency injection framework are stored.
You might start with them being in the Program.cs
file:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ISystemClock, SystemClock>();
But you can move these to another project, like the Infrastructure project using extension methods:
public static class ServiceRegistrations
{
public static IServiceCollection AddSystemClock(IServiceCollection services)
{
services.AddScoped<ISystemClock, SystemClock>();
return services;
}
}
If you move these registrations to the Infrastructure
project, you will need to add a reference to
the Microsoft.Extensions.DependencyInjection.Abstractions
NuGet package:
dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions
Summary
Infrastructure concerns are one of the most important influences on how you design and organize your code. Where possible, use abstractions for all external components to allow your code to be easy to test, easy to understand and easy to evolve and grow.