top of page

Implementing an Effective Architecture for ASP.NET Core Web API


ASP.NET Core 3.1 is a cross-platform web framework that can be deployed to Windows, macOS, Linux or even to Containers. It’s the perfect framework to develop the Web APIs that drive the hottest web and mobile apps. The key to leveraging the benefits that ASP.NET Core brings to developers is having the right architecture to go along with it. In this article, I’ll demonstrate an effective architecture, using the ports and adapter pattern, to allow your APIs to be efficient and testable. It should allow a clear decoupling of the API endpoints, data access (synthetic or production) and finally the data model classes. Separation of responsibilities means that testing can be done easily and without issues. Finally, data access segments of the architecture can easily be switched out without impacting the domain or API endpoints.­ By using the ports and adapter pattern, not only does the development story become easier but the end users get a much more stable API set to consume.

The Internet is a vastly different place than it was five years ago, let alone 25 years ago when I first started as a professional developer. Web APIs connect the modern internet and drive both web applications and mobile apps. The skill of creating robust web APIs that other developers can consume is in high demand. APIs that drive most modern web and mobile apps need to have the stability and reliability to continue operating, even when traffic is at the performance limits.

This article describes the architecture of an ASP.NET Core 3.1 Web API solution using the ports and adapters pattern. First, we’ll look at the new features of .NET Core and ASP.NET Core that benefit modern Web APIs.

The solution and all code from this article’s examples can be found in my GitHub repository ChinookASPNETCore3APINTier.

.NET Core and ASP.NET Core for Web API

.NET Core, unlike .NET Framework, is a new web framework that Microsoft built to shed the legacy technology that’s been around since .NET 1.0. For example, in ASP.NET 4.6, the System.Web assembly that contains all the WebForms libraries carries over into more recent ASP.NET MVC 5 solutions. By shedding these legacy dependencies and developing the framework from scratch, ASP.NET Core 3.1 gives the developer much better performance and is architected for cross-platform execution, so your solutions will work as well on Linux as they do on Windows.

Dependency Injection

Before we dig into the architecture of the ASP.NET Core Web API solution, I want to discuss a single benefit that makes .NET Core developers’ lives so much better: Dependency Injection (DI). We had DI in .NET Framework and ASP.NET solutions, but the DI we used in the past was from third-party commercial providers or open source libraries. They did a good job, but a good portion of .NET developers experienced a big learning curve, and all DI libraries had their idiosyncrasies. With .NET Core, DI is built right into the framework from the start. Moreover, it’s quite simple to work with.

Using DI in your API gives you the best experience decoupling your architecture layers — as I’ll demonstrate later in the article — and allows you to mock the data layer or have multiple data sources built for your API.

Many updates in ASP.NET Core 3.1 help our solutions, including not needing to manually add the Microsoft.AspNetCore.All NuGet package. The needed assemblies are added to the projects by default and give access to the IServiceCollection interface, which has a System.IServiceProvider interface that you can call GetService<TService>. To get the services you need from the IServiceCollection interface, you’ll need to add the services your project needs.

To learn more about .NET Core DI, I suggest you review the following document on MSDN: Introduction to Dependency Injection in ASP.NET Core.

We’ll now look at the philosophy of why I architected my web API as I did. The two aspects of designing any architecture depends on these two ideas: allowing deep maintainability and using proven patterns and architectures in your solutions.


Building a great API requires great architecture. We’ll be looking at many aspects of API design and development, from built-in functionality of ASP.NET Core to architecture philosophy and design patterns.

Maintainability Of The API

Maintainability for any engineering process is the ease with which a product can be preserved. Maintenance activities can include finding defects, correcting found defects, repairing or replacing defective components without having to replace still-working parts, preventing unexpected malfunctions, maximizing a product’s useful life, having the ability to meet new requirements, making future maintenance easier or coping with a changing environment. This can be a difficult road to go down without a well-planned and well-executed architecture.

Maintainability is a long-term issue, and you should be looking at your API with a long-term vision. You need to make decisions that lead to this vision, rather than shortcuts that may make life easier right now. Making hard decisions at the start will allow your project to have longevity and provide benefits that users demand.

What gives a software architecture high maintainability? How do you evaluate if your API can be maintained? Changes to our architecture should allow for minimal, if not zero, impact on the other areas. Debugging should be easy and not require difficult setup. You should have established patterns and used common methods (such as browser debugging tools). Testing should be as automated as possible and be clear and simple.

Interfaces And Implementations

The key to my API architecture is the use of C# interfaces, which allows alternative implementations. If you have written .NET code with C#, you’ve probably used interfaces. In my solution, I use them to build out a contract in my Domain layer that guarantees any Data layer I develop for my API adheres to the contract for data repositories. It also allows the controllers in my API project to adhere to another established contract for getting the correct methods to process the API methods in the domain project’s supervisor. Interfaces are crucial to .NET Core. If you need a refresher on interfaces, check out this article.

Ports And Adapter Pattern

You want your objects throughout your API solution to have single responsibilities. This keeps your objects simple and maintainable — in case we need to fix bugs or enhance our code. If you have these “code smells” in your code, then you might be violating the single responsibility principle. As a rule, I look at the implementations of the interface contracts for length and complexity. I don’t have a limit to the lines of code in my methods, but if you passed a single view in your integrated development environment (IDE), it might be too long. Also, check the cyclomatic complexity of your methods to determine the complexity of your project’s methods and functions.

The Ports and Adapter Pattern fixes this problem by having business logic decoupled with other dependencies — such as data access or API frameworks. Using this pattern allows your solution to have clear boundaries and well-named objects with single responsibilities, allowing for easier development and maintainability.

Picture the pattern like an onion, with ports located on the outside layers and the adapters and business logic located closer to the core, as shown in Figure 1. The external connections of the architecture are the ports. The API endpoints that are consumed or the database connection used by Entity Framework Core 3.1 are examples of ports, while the internal domain supervisor and data repositories are the adapters.

Figure 1 — Visualization Of The Ports And Adapter Pattern

Next, let’s look at the logical segments of our architecture and some demo code examples. These three segments, shown in Figure 2, should follow a logical separation between the consumer end-point or API, the domain segment which encompasses the business domain for the solution and finally the segment that contains the code for accessing the data in our SQL Server database.

Figure 2 — Segments Of Architecture

Domain Layer

Before we look at the API and Domain layers, I need to explain how you build out the contracts through interfaces and how you implement our API business logic. Let’s look at the Domain layer. The Domain layer has the following functions:

  • Defines the Entities objects that will be used throughout the solution. These models will represent the Data layer’s DataModels.

  • Defines the ViewModels which will be used by the API layer for HTTP requests and responses as single objects or sets of objects.

  • Defines the interfaces through which your Data layer can implement the data access logic.

  • Implements the supervisor that will contain methods called from the API layer. Each method will represent an API call and will convert data from the injected Data layer to ViewModels to be returned.

Our Domain Entity objects are a representation of the database that you’re using to store and retrieve data used for the API business logic. Each Entity object will contain the properties represented, in this case the SQL table. For an example, reference the Album entity in Listing 1.

Listing 1 — The Album Entity Model Class

public class Album : IConvertModel<Album, AlbumApiModel> {     
    public int AlbumId { get; set; }     
    [StringLength(160, MinimumLength=3)]     
    public string Title { get; set; } 
    public int ArtistId { get; set; }     
    public virtual Artist Artist { get; set; }     
    public virtual ICollection <Track> Tracks { get; set; } =new HashSet<Track>();
      public AlbumApiModel Convert() =>
          new AlbumApiModel        
          { AlbumId=AlbumId, ArtistId=ArtistId, Title=Title        

The Album table in the SQL database has three columns: AlbumId, Title and ArtistId. These three properties are part of the Album entity, as well as the Artist’s name, a collection of associated Tracks and the associated Artist. As you’ll see in the other layers in the API architecture, I’ll build upon this entity object’s definition for the ViewModels in the project.

The ViewModels are the extension of the Entities, which help give more information for the consumer of the APIs. Let’s look at the Album ViewModel. It’s very similar to the Album Entity but with an additional property. In the design of my API, I determined that each Album should have the name of the Artist in the payload passed back from the API. This allows the API consumer to have that crucial piece of information about the Album without having to have the Artist ViewModel passed back in the payload (especially when we’re sending back a large set of Albums). An example of our AlbumViewModel is included below in Listing 2.

Listing 2 — The Album API Model Class

public class AlbumApiModel : IConvertModel <AlbumApiModel, Album> {     
    public int AlbumId { get; set; } 
    public string Title { get; set; }  
    public int ArtistId { get; set; } 
    public string ArtistName { get; set; } 
    public Artist ApiModel Artist { get; set; } 
    public IList <TrackApiModel> Tracks { get; set; }     
        public AlbumConvert() =>new Album        

The other area that’s developed into the Domain layer is the contracts via interfaces for each of the Entities defined in the layer. Again, we’ll use the Album entity (shown in Listing 3) to show the interface that is defined.

Listing 3 — The Album Repository Interface

public interface IAlbumRepository : IDisposable{ 
    List <Album> GetAll(); 
    Album GetById(int id); 
    List <Album> GetByArtistId(int id); 
    Album Add(Album newAlbum); 
    bool Update(Album album); 
    bool Delete(int id); 

As shown in Listing 3, the interface defines the methods needed to implement the data access methods for the Album entity. Each entity object and interface are well-defined and simplistic, allowing the next layer to be well-defined, too.

Finally, the core of the Domain project is the Supervisor class. Its purpose is to translate to and from Entities and ViewModels and perform business logic away from either the API endpoints or the Data access logic. Having the supervisor handle will also isolate the logic to allow unit testing on the translations and business logic.

Looking at the supervisor method for acquiring and passing a single Album APIModel from the API endpoint, as shown in Listing 4, we can see the logic in connecting the API front end to the data access injected into the supervisor, but still keeping each isolated.

Listing 4 — Supervisor Method for a Single Album

public AlbumApiModel GetAlbumById(int id) {     
    var album=_albumRepository.GetById(id); 
    if (album == null) return null; 
    var albumApiModel=album.Convert(); 
   return albumApiModel; 

Keeping most of the code and logic in the Domain project will allow every project to keep and adhere to the single responsibility principle.

Data Layer

The next layer of the API architecture we’ll look at is the Data Layer. In my example solution, I’m using Entity Framework Core 3.1. This will mean that I have the Entity Framework Core’s DBContext defined, but also the Data Models generated for each entity in the SQL database. If you look at the data model for the Album entity as an example, you’ll see that three properties are stored in the database, along with a property containing a list of associated tracks to the Album and a property that contains the Artist object.

While you can have a multitude of Data Layer implementations, just remember that it must adhere to the requirements documented on the Domain Layer. Each Data Layer implementation must work with the View Models and repository interfaces detailed in the Domain Layer. The architecture you’re developing for the API uses the Repository Pattern for connecting the API Layer to the Data Layer. This is done using Dependency Injection (as I discussed earlier) for each of the repository objects you implement. I’ll discuss how you use Dependency Injection when we look at the API Layer. The key to the Data Layer is the implementation of each entity repository using the interfaces developed in the Domain Layer. Looking at the Domain Layer’s Album repository in Listing 5 as an example, you can see that it implements the AlbumRepository interface. Each repository will inject the DBContext, which allows for access to the SQL database using Entity Framework Core.

Listing 5 — Album Repository Based On The Album Repository Interface

public class AlbumRepository : IAlbumRepository    
    private readonly ChinookContext _context; 
    public AlbumRepository(ChinookContext context)         
    private bool AlbumExists(int id) =>
        _context.Album.Any(a => a.AlbumId == id); 
    public void Dispose() =>_context.Dispose(); 
    public List <Album> GetAll() =>
    public Album GetById(int id)         
        var dbAlbum=_context.Album.Find(id); 
        return dbAlbum;         
    public AlbumAdd(Album newAlbum)         
        return newAlbum;         
    public bool Update(Albumalbum)         
    if (!AlbumExists(album.AlbumId)) 
        return false;
      return true;         
    public bool Delete(int id)         
    if (!AlbumExists(id)) 
        return false; 
    var toRemove=_context.Album.Find(id); 
    return true;         
    public List<Album> GetByArtistId(int id) =>
        _context.Album.Where(a => a.ArtistId == id).ToList();     

Having the Data Layer encapsulate all data access will allow facilitating a better testing story for your API. We can build multiple data access implementations: one for SQL database storage, another for a cloud NoSQL storage model and finally a mock storage implementation for the unit tests in the solution.

API Layer

The final layer we’ll look at is the area that your API consumers will interact with. This layer contains the code for the Web API endpoint logic including the Controllers. The API project for the solution will have a single responsibility, and that is to handle the HTTP requests received by the web server and return the HTTP responses with either success or failure. There will be a very minimal business logic in this project.

We will handle exceptions and errors that have occurred in the Domain or Data projects to effectively communicate with the consumer of APIs. This communication will use HTTP response codes and any data to be returned located in the HTTP response body.

In ASP.NET Core 3.1 Web API, routing is handled using Attribute Routing. You’re also using dependency injection to have the Supervisor assigned in each Controller. Each Controller’s Action method has a corresponding Supervisor method that will handle the logic for the API call. I have a segment of the Album Controller in Listing 6 to show these concepts.

Listing 6 — Segment Of The Album Controller

public class AlbumController : Controller{ 

    private readonly IChinookSupervisor_chinookSupervisor; 

    public AlbumController(IChinookSupervisor chinookSupervisor)     
    public ActionResult<List<AlbumApiModel>> Get()     
            return new ObjectResult(_chinookSupervisor.GetAllAlbum());        
        catch (Exception ex)         
            return StatusCode(500, ex);         

The Web API project for the solution is very simple and thin. I strive to keep as little code in this solution as possible, because it could be replaced with another form of interaction in the future.


As I have demonstrated, designing and developing a great ASP.NET Core 3.1 Web API solution takes insight to have a decoupled architecture that allows each layer to be testable and follow the Single Responsibility Principle. I hope this information will allow you to create and maintain your production Web APIs for your organization’s needs.

Source: Medium

The Tech Platform

bottom of page