This point of the point

  • The new architecture of ASP.NET Core offers several benefits over traditional ASP.NET
  • ASP.NET Core has included support for dependency injection from the beginning
  • The single responsibility principle simplifies implementation and design.
  • The port and adapter patterns separate business logic from other dependencies
  • The decoupled architecture makes testing easier and more robust

.NET Core was first released in 2016, with. With the release of NET Core 2.0, Microsoft has the next universal, modular, cross-platform and open source platform major release. .NET Core has created a number of apis that are available in the current version of the.NET framework. It was originally created for the next generation OF ASP.NET solutions, but now drives and underlies many other scenarios, including the Internet of Things, cloud computing, and the next generation of mobile solutions. In this series, we will explore. Some of the benefits of NET Core and how it can not only make traditional. NET developers benefit, but also all technical people who need to bring robust, efficient and economical solutions to the market.

This InfoQ article is part of a series called.net Core. You can receive notifications via RSS subscriptions.

The Internet is a very different place today than it was five years ago, let alone 20 years ago when I started out as a professional developer. Today, Web apis connect the modern Internet driven by Web and mobile applications. One skill that is in high demand is to create robust Web apis that other developers can also use. The apis that drive most modern Web and mobile applications need to be stable and reliable in order to continue serving when traffic reaches performance limits.

The purpose of this article is to describe the architecture of the ASP.NET Core 2.0 Web API solution, which uses the Hexagonal architecture and port and adapter patterns. First, let’s see. NET Core and new features of ASP.NET Core that help with modern Web apis.

The solution and all of the code in the examples in this article can be found in my GitHub repository, ChinookASPNETCoreAPIHex.

For Web API. NET Core and ASP.NET Core

ASP.NET Core is Microsoft’s version of A new Web framework built on the basis of NET Core to get rid of. Legacy technologies since NET 1.0. By contrast,ASP.NET 4.6 still uses system. Webassembly(which contains all the WebForms libraries), thus introducing the more recent ASP.NET MVC 5 solution. By moving away from these legacy dependencies and developing the framework from the ground up, with an architecture designed for cross-platform execution, ASP.NET Core 2.0 provides better performance for developers. With ASP.NET Core 2.0, your solution will run well on Linux as well as Windows.

For more on.net Core and the benefits of ASP.NET Core, you can read the other three articles in this series. The first was “Performance is a Core.net Feature” by Maarten Balliauw, “ASP.NET Core: The Power of Simplicity” by Chris Klug, and finally “Azure and.NET Core Fit Together” by Eric Boyd.

The architecture

Building a good API depends on a great architecture. We’ll examine many aspects of API design and development from the built-in capabilities of ASP.NET Core to shape the philosophy and architecture of the final design pattern. There’s a lot of planning and thinking behind this architecture, so let’s get started.

Dependency injection

Before we delve into the ASP architecture. NET Core Web API solution, I want to discuss what I think makes. The single benefit of NET core developers being better off; Dependency injection (DI). Now, I know you’re gonna say we’re. Dependency injection is available in the ASP.NET framework and ASP.NET solutions. I agree, but we used to use di from third party commercial providers or open source libraries. They did a good job, but for. There is a steep learning curve for NET developers, and all di libraries have their own unique approach. Today, with.net Core, we have integrated dependency injection into the framework from the beginning. In addition, its usage is very simple and it is immediately available.

The reason we need to use dependency injection in the API is that it allows us to have the best experience with decoupling the architectural layers and allows us to emulate the data layer or build multiple data sources for the API.

To use the.NET Core dependency injection framework, . Please make sure that your project refers to the Microsoft AspNetCore. AllNuGet package (. It contains the Microsoft Extnesions. DependencyInjection. Abstractionspackage dependencies). This package provides access to the IServiceCollection interface, which has a System.IServiceProvider interface. You can call GetService<TService> to get the required services from the IServiceCollection interface. The services required by the project need to be added.

To learn more about. For information about DEPENDENCY injection in NET Core, I recommend you read the following documentation about MSDN: Introduction to dependency injection in ASP.NET.

Now let’s look at the rationale for why we should do API architecture design like I do. Two aspects of designing any architecture rely on these two concepts: allowing deep maintainability, and using proven patterns and architectures in solutions.

Maintainability of the API

For any engineering process, the maintainability refers to a product easy to be maintenance: finding defects, correct the defects found, without replacement is still in the work of the component repair or replacement of defective components, prevent accidental failures, maximize the service life of the product, has the ability to meet the new requirements, make it easier for future maintenance, and can respond to climate change. None of this is possible without a well-planned and executable architecture.

Maintainability is a long-term issue and should be viewed from the perspective of your API. With this in mind, you need to make a decision to pursue your vision for the future rather than take the easy way out. Making tough decisions at the beginning will enable your project to have a long life and provide the benefits that users need.

What makes a software architecture highly maintainable? How do you evaluate whether an API is maintainable?

  • Does our architecture allow for minimal or zero impact on other areas of the system?
  • Debugging an API should be easy; it doesn’t have to be difficult. We should establish patterns and use common methods (such as browser debugging tools).
  • Tests should be automated, clear and uncomplicated.

Interfaces and implementations

The key to my API architecture is the use of C# interfaces to support other implementations. If you already write it in C#. NET code, then you probably already use the interface. I use interfaces in the solution to build a contract in the domain layer that guarantees that any data layer I develop for the API follows the contract of the data repository. It also allows the controller in my API project to comply with another established contract to get the right way to handle the API methods in the Supervisor of the domain project. Interfaces are very important to.net Core, and if you need more information, click here.

Port and adapter mode

We want objects throughout the API solution to have a single responsibility. This will allow us to keep objects simple and easy to modify when we need to fix bugs or enhance code. If there is some “code smell” in your code, you may be violating the single liability principle. In general, I focus on the length and complexity of the implementation of interface contracts. There’s no line limit in my method, but if it’s already longer than a view in your IDE, it’s probably too long. In addition, I examine the cyclomatic complexity of the method to determine the complexity of the project methods and functions.

The port and adapter pattern (also known as hexagonal architecture) solves the problem of business logic being too tightly coupled to other dependencies, such as data access or API frameworks. Using this pattern will allow your API solutions to have well-defined, well-named objects with a single responsibility, ultimately making them easier to develop and maintain.

This pattern can be visualized as an onion, with ports located outside the hexagon and adapters and business logic located closer to the core. I think of the external connections of the architecture as ports. A consumed API endpoint or database connection used by Entity Framework Core 2.0 would be a typical port example, while an internal data repository would be an adapter.

Next, let’s look at the logical parts of the architecture and some demo code examples.

Domain layer

Before we look at the API and domain layers, we need to explain how contracts are built from implementations of the interface and API business logic. Let’s look at the domain layer. The domain layer has the following functions:

  • Define entity objects that will be used throughout the solution. These models will represent the DataModel (DataModel) for the data layer.
  • Define the ViewModel that will be used by the API layer as a single object or set of objects for HTTP requests and responses.
  • Define interfaces through which our data layer can implement data access logic.
  • The implementation will contain the Supervisor of the method called from the API layer. Each method represents an API call and transforms the data from the injected data layer to the viewmodel for return.

Our domain entity object represents the database we use to store and retrieve data for our API business logic. Each entity object will contain attributes from the SQL table. The following is a photo entity Album.

__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__public sealed class Album { public  int AlbumId { get; set; } public string Title { get; set; } public int ArtistId { get; set; } public ICollection<Track> Tracks { get; set; } = new HashSet<Track>(); public Artist Artist { get; set; } }__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__Copy the code

The Album table in the SQL database has three tables :AlbumId, Title, and ArtistId. These three properties are part of the album entity, along with the name of the artist and the associated artist and a set of related songs. As we will see in other layers of the API architecture, we will build the definition of this entity object against the view model in this project.

The viewmodel is an extension of the entity and helps provide more information to users of the API. Let’s look at the viewmodel. It is very similar to the album entity, but with additional attributes. In the design of the API, I determined that each album should include the name of the artist in the payload returned from the API. This allows the API consumer to have key information about the Album without passing the Artist view model in the data payload (especially if we return a large number of albums). Here is an example of our Album view model.

__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__public class AlbumViewModel
{
    public int AlbumId { get; set; }
    public string Title { get; set; }
    public int ArtistId { get; set; }
    public string ArtistName { get; set; }

    public ArtistViewModel Artist { get; set; }
    public IList<TrackViewModel> Tracks { get; set; }
}__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__Copy the code

Another part of the domain layer that needs to be developed is the contracts, which pass through the interfaces defined for each entity in the layer. Again, we’ll use the Album entity to display the defined interface.

__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__public interface IAlbumRepository : IDisposable
{
	Task<List<Album>> GetAllAsync(CancellationToken ct = default(CancellationToken));
Task<Album> GetByIdAsync(int id, CancellationToken ct = default(CancellationToken));
       Task<List<Album>> GetByArtistIdAsync(int id, CancellationToken ct = default(CancellationToken));
       Task<Album> AddAsync(Album newAlbum, CancellationToken ct = default(CancellationToken));
       Task<bool> UpdateAsync(Album album, CancellationToken ct = default(CancellationToken));
       Task<bool> DeleteAsync(int id, CancellationToken ct = default(CancellationToken));
}__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__Copy the code

As shown in the previous example, the interface defines the methods needed to implement the data access methods for the Album entities. Each entity object and interface is well defined and simplified so that the next layer can be well defined.

Finally, the Supervisor class is the core of the domain project. Its purpose is to transform between entities and view models, and to perform business logic outside of API endpoints and data access logic. Having the Supervisor handle this also isolates the logic so that the transformation and business logic can be unit tested.

Looking at the Supervisor method that gets and passes individual albums to API endpoints, we can see that the logic that connects the API front end to data access is injected into the Supervisor, while still keeping each Album separate.

__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__public async Task<AlbumViewModel> GetAlbumByIdAsync(int id, CancellationToken ct = default(CancellationToken))
{
    var albumViewModel = AlbumCoverter.Convert(await _albumRepository.GetByIdAsync(id, ct));
    albumViewModel.Artist = await GetArtistByIdAsync(albumViewModel.ArtistId, ct);
    albumViewModel.Tracks = await GetTrackByAlbumIdAsync(albumViewModel.AlbumId, ct);
    albumViewModel.ArtistName = albumViewModel.Artist.Name;
    return albumViewModel;
}__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__Copy the code

Maintaining most of the code and logic in a domain project will keep and adhere to the single responsibility principle for each project.

The data layer

The next layer of THE API architecture we’ll see is the data layer. In our example solution, Entity Framework Core 2.0 is used. This means that we not only have the DBContext defined for Entity Framework Core, but also the data model generated for each Entity in the SQL database. If we look at the data model for the album entity as an example, we see that there are three properties in the database, along with a set of songs related to the album, and related properties for the artist object.

While you can have a large data layer implementation, keep in mind that it must comply with the requirements of documentation at the domain layer; Each data layer implementation must work with a detailed view model and repository interface in the domain layer. The architecture we developed for the API uses the repository pattern to connect the API layer to the data layer. Dependency injection (as we discussed earlier) is used for each repository object we implement. We’ll discuss how to use dependency injection and code when looking at the API layer. The key to the data layer is to implement each entity repository using interfaces developed in the domain layer. The domain-level album repository, for example, implements the IAlbumRepository interface. Each repository will inject DBContext, allowing access to SQL databases using the Entity Framework core.

__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__public class AlbumRepository : IAlbumRepository { private readonly ChinookContext _context; public AlbumRepository(ChinookContext context) { _context = context; } private async Task<bool> AlbumExists(int id, CancellationToken ct = default(CancellationToken)) { return await GetByIdAsync(id, ct) ! = null; } public void Dispose() { _context.Dispose(); } public async Task<List<Album>> GetAllAsync(CancellationToken ct = default(CancellationToken)) { return await _context.Album.ToListAsync(ct); } public async Task<Album> GetByIdAsync(int id, CancellationToken ct = default(CancellationToken)) { return await _context.Album.FindAsync(id); } public async Task<Album> AddAsync(Album newAlbum, CancellationToken ct = default(CancellationToken)) { _context.Album.Add(newAlbum); await _context.SaveChangesAsync(ct); return newAlbum; } public async Task<bool> UpdateAsync(Album album, CancellationToken ct = default(CancellationToken)) { if (! await AlbumExists(album.AlbumId, ct)) return false; _context.Album.Update(album); _context.Update(album); await _context.SaveChangesAsync(ct); return true; } public async Task<bool> DeleteAsync(int id, CancellationToken ct = default(CancellationToken)) { if (! await AlbumExists(id, ct)) return false; var toRemove = _context.Album.Find(id); _context.Album.Remove(toRemove); await _context.SaveChangesAsync(ct); return true; } public async Task<List<Album>> GetByArtistIdAsync(int id, CancellationToken ct = default(CancellationToken)) { return await _context.Album.Where(a => a.ArtistId == id).ToListAsync(ct); } }__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__Copy the code

Having a data layer that encapsulates all data access will help you better test the API. We can build multiple data access implementations: one for SQL database storage, another for the cloud NoSQL storage schema, and finally a mock storage implementation for unit tests in the solution.

The API layer

The final layer we’ll look at is the area where your API consumers will interact. This layer contains the code for the Web API endpoint logic, including the controller. The API project for this solution will have a separate responsibility for processing HTTP requests received by the Web server and returning HTTP responses, whether successful or unsuccessful. In this project, there will be very little business logic. We handle exceptions and errors that occur in the domain or data project to effectively communicate with consumers of the API. This communication will use the HTTP response code and any data returned in the HTTP response message.

In ASP.NET Core 2.0 Web API, Routing is handled using the Routing attribute. If you need to learn more about the Routing property in ASP.NET Core, go here. We also assign the Supervisor to each controller using dependency injection. Each controller action method has a corresponding Supervisor method that handles the logic of the API call. Below I have a snippet of the Album controller to illustrate these concepts.

__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__[Route("api/[controller]")] public  class AlbumController : Controller { private readonly IChinookSupervisor _chinookSupervisor; public AlbumController(IChinookSupervisor chinookSupervisor) { _chinookSupervisor = chinookSupervisor; } [HttpGet] [Produces(typeof(List<AlbumViewModel>))] public async Task<IActionResult> Get(CancellationToken ct = default(CancellationToken)) { try { return new ObjectResult(await _chinookSupervisor.GetAllAlbumAsync(ct)); } catch (Exception ex) { return StatusCode(500, ex); }}... }__Wed Jun 20 2018 14:53:22 GMT+0800 (CST)____Wed Jun 20 2018 14:53:22 GMT+0800 (CST)__Copy the code

The Web API project for this solution is very brief. I try to keep as little code in this solution as possible because it can be replaced by another form of interaction in the future.

conclusion

As I’ve shown, it is insightful to design and develop a great ASP.NET Core 2.0 Web API solution in order to have a decoupled architecture that will allow each layer to be testable and follow a single principle of responsibility. I hope my information will allow you to create and maintain your product Web apis to meet the needs of your organization.

About the author

Chris Woodruff (Woody) holds a degree in computer science from Michigan State University College of Engineering. Woody has been developing and architecting software solutions for over 20 years and has worked on many different platforms and tools. He is a community leader and has contributed to events such as GRDevNight, GRDevDay, West Michigan Day of.NET and CodeMash. He also helped bring the popular Give Camp to Western Michigan, where technology professionals offer their time and development expertise to help local nonprofits. As a speaker and podcast author, Woody has spoken and discussed many topics, including database design and open source. He has been Microsoft’s MVP for Visual C#, data platform and SQL, and was recognized as one of the top 20 MVPs in the world in 2010. Woody is the developer of JetBrains and is promoting it in North America. NET,.net Core and JetBrains products.

.NET Core was first released in 2016, with. With the release of NET Core 2.0, Microsoft has the next universal, modular, cross-platform and open source platform major release. NETCore has created a number of apis that are available in the current version of the.NET framework. It was originally created for the next generation OF ASP.NET solutions, but now drives and underlies many other scenarios, including the Internet of Things, cloud computing, and the next generation of mobile solutions. In this series, we will explore. Some of the benefits of NET Core and how it can not only make traditional. NET developers benefit, but also all technical people who need to bring robust, efficient and economical solutions to the market.

This InfoQ article is part of the.net Core series. You can receive notifications via RSS subscriptions.

Advanced Architecture for ASP.NET Core Web API