Chapter 10: Creating an ASP.NET Core 5 Web API

In recent times, web services have become an important part of web application development. With ever-changing requirements and increased business complexity, it is very important to loosely couple various components/layers involved in web application development, and there is nothing better than decoupling the user interface part of the application with the core business logic. This is where the simplicity of web services using the RESTful approach helps us to develop scalable web applications.

In this chapter, we will learn how to build RESTful services using an ASP.NET Core web API and, along the way, we will build all the required APIs for our e-commerce application.

In this chapter, we will cover the following topics in detail:

  • Introduction to Representational State Transfer (REST)
  • Understanding the internals of an ASP.NET Core 5 Web API
  • Handling requests using controllers and actions
  • Integration with the data layer
  • Understanding gRPC

Technical requirements

For this chapter, you will require a basic knowledge of C#, .NET Core, web APIs, HTTP, Azure, dependency injection, Postman, and the .NET CLI.

The code for this chapter can be found here: https://github.com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/tree/master/Chapter10/TestApi.

For more code examples, refer to the following link: https://github.com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/tree/master/Enterprise%20Application

Introduction to Representational State Transfer (REST)

Representational State Transfer (REST) is an architectural guideline for building a web service. Primarily, it defines a set of constraints that can be followed while designing a web service. One of the key principal REST approaches recommends that the APIs should be designed around resources and should be media- and protocol-agnostic. The underlying implementation of the API is independent of the client consuming the API.

Considering an example of our e-commerce application, say we are searching for a product on the UI using a product's search field. There should be an API that is created for products, and here products are nothing but a resource in the context of an e-commerce application. The URI for that API could be something like the following, which clearly states that we are trying to perform a GET operation on product entities:

GET http://ecommerce.packt.com/products

The response of the API should be independent of the client that is calling the API, that is, in this case, we are using a browser to load a list of products on the product search page. However, the same API can be consumed in a mobile application as well without any changes. Secondly in this case, in order to retrieve product information internally, an application may be using one or more physical data stores; however, that complexity is hidden from the client application and the API is exposed to the client as a single business entity – products. Although REST principles do not dictate the protocol to be HTTP, the majority of the RESTful services are built over HTTP. Some of the key design principles/constraints/rules of HTTP-based RESTful APIs are as follows:

  • Identify the business entities of the system and design APIs around those resources. In the case of our e-commerce application, all our APIs would be around resources such as products, orders, payments, and users.
  • REST APIs should have a uniform interface that assists in making it independent of the client. As all the APIs need to be resource-oriented, each resource is uniquely identified by a URI; additionally, various operations on resources are uniquely identified by HTTP verbs such as GET, POST, PUT, PATCH, and DELETE. For example, GET (http://ecommerce.packt.com/products/1) should be used to retrieve a product with an ID of 1. Similarly, DELETE (http://ecommerce.packt.com/products/1) should be used to delete a product.
  • As HTTP is stateless, REST dictates a number of things for RESTful APIs. What this means is that APIs should be atomic and conclude the processing of a request within the same call. Any subsequent request, even from the same client (same IP), is treated as a new request. For example, if an API accepts an authentication token, it should accept authentication for each request. One major advantage of statelessness is the scalability that servers can eventually achieve as a client can make an API call to any of the available servers and still receive the same response.
  • Apart from sending back the response, APIs should make use of HTTP status codes and response headers to send any additional information to the client. For example, if a response can be cached, an API should send the relevant response headers to the client so that it can be cached. Response Caching, discussed in Chapter 8, Understanding Caching, is based on these headers. Another example is where an API should return the relevant HTTP status codes in the event of both success and failure scenarios, that is, 1xx for information, 2xx for success, 3xx for redirection, 4xx for client errors, and 5xx for server errors.
  • Hypermedia As The Engine Of Application State (HATEOAS): APIs should give information about the resource such that client should be easily able to discover them without any prior information relating to the resource. For example, if there is an API to create a product, once a product is created, the API should respond with the URI of that resource so that the client can use that to retrieve the product later.

    Refer to the following response for an API that retrieves a list of all the products (GET /products) and has information to retrieve further details regarding each product:

    {

    "Products": [

    {

    "Id": "1",

    "Name": "Men's T-Shirt",

    "Category": "Clothing"

    "Uri": "http://ecommerce.packt.com/products/1"

    }

    {

    "Id": "2",

    "Name": "Mastering enterprise application development Book",

    "Category": "books"

    "Uri": "http://ecommerce.packt.com/products/2"

    }

    ]

    }

The preceding example is one way to implement the HATEOAS principle, but it can be designed in a more descriptive way, like a response containing information about accepted HTTP verbs, relationships, and so on.

The REST maturity model

These are various guidelines that an API should follow in order for it to be RESTful. However, it's not necessary that all the principles are followed to make it perfectly RESTFUL. It's more important that an API should fulfill the business goal rather than being 100% REST-compliant. Leonard Richardson, an expert on RESTful API design, came up with following model to categorize the maturity of an API:

  • Level 0: The Swamp of Plain Old XML – Any API that has a single POST URI to perform all the operations will fall under this category. An example would be a SOAP-based web service that has a single URI and all operations are segregated based on the SOAP envelop.
  • Level 1: Resources – All the resources are URI driven and APIs that have a dedicated URI pattern per resource fall under this maturity model.
  • Level 2: HTTP verbs – Apart from a separate URI for each resource, each URI has a separate action based on the HTTP verb. As discussed earlier, a product API that supports GET and DELETE using the same URI with different HTTP verbs falls under this maturity model. Most enterprise application RESTful APIs fall under this category.
  • Level 3: HATEOAS – APIs that are designed with all the additional discovery information (the URI for the resources, various operations that the resource supports) fall under this maturity model. Very few APIs are compliant with this maturity level; however, as discussed earlier, it's important that our APIs should fulfill the business objective and be as compliant as possible with RESTful principles rather than 100% compliant but not fulfilling the business objective.

The following diagram illustrates Richardson's maturity model:

Figure 10.1 – Richardson's maturity model

Figure 10.1 – Richardson's maturity model

Till now, we have discussed various principles of REST architecture. In next section, let's get into using an ASP.NET Core Web API, which we will create various RESTful services for in our e-commerce application.

Understanding the internals of an ASP.NET Core 5 web API

ASP.NET Core is a unified framework that runs on top of .NET Core and is used to develop web applications (MVC/Razor), RESTful services (web API) and, most recently, web assembly-based client application (Blazor apps). The fundamental design of ASP.NET Core applications is based on the Model View Controller (MVC) pattern, which divides code into three primary categories:

  • Model: This is a POCO class that holds the data and is used to pass data between various layers of the application. Layers include passing data between the repository class and the service class, or passing information back and forth between the client and server. The model primarily represents the resource state, or the domain model of the application, and contains information that you have requested. For example, if we wanted to store user profile information, it can be represented by the POCO class UserInformation, and can contain all the profile information. This will be further used to pass between repositories and service classes and can also be serialized into JSON/XML before being sent back to the client. In enterprise applications, there are different types of models that we will encounter while creating models for our e-commerce application in the Integration with the data layer section.
  • Controller: These are a set of classes that receive all the requests, perform all the required processing, populate the model, and then send it back to the client. In enterprise applications, they typically make use of service classes to handle the business logic, and repositories to communicate with the underlying data store. With the unified approach in ASP.NET core, both MVC/Razor applications and web API applications use the same Microsoft.AspNetCore.Mvc.ControllerBase class to define controllers.
  • View: These are the pages that represent UIs. All our models retrieved from controllers are bound to various HTML controls on views and are presented to users. Views are usually common in MVC/Razor applications; for web API applications, the process ends at serializing models as a response.

So, in a web application developed using ASP.NET Core, whenever a request comes from a client (browser, mobile apps, and similar sources), it goes through the ASP.NET core request pipeline and reaches a controller that interacts with the data store to populate models/view-models and send them back either as a response in the form of JSON/XML or to a view to further bind the response and present it to the user. As you can see, there is a clear separation of concern where a controller is not aware of any UI aspect and performs the business logic in the current context and responds via models, while views, on the other hand, receive models and use them to present them to the user in the HTML pages. This separation of concern easily helps unit test the application, as well as maintain and scale it as needed. MVC patterns are not only applicable to web applications, and can be used for any application that requires a separation of concern.

As the focus of this chapter is to build RESTful services, we will focus on an ASP.NET Core web API in this chapter and discuss ASP.NET MVC and Razor pages in Chapter 11, Creating an ASP.NET Core 5 Web Application.

To develop RESTful services, there are many frameworks that are available, but the following are a few of the advantages of going with ASP.NET Core on .NET 5:

  • Cross-platform support: Unlike ASP.NET, which used to be part of .NET framework, which is coupled with the Windows operating system, ASP.NET Core is now part of the application, thereby eliminating the platform dependency and making it compatible with all platforms.
  • Highly customizable request pipelines use middlewares and support to inject various out-of-the-box modules, such as logging and configuration.
  • Out-of-the-box HTTP server implementation, which can listen to HTTP requests and forward them to controllers. The server implementation includes cross-platform servers such as Kestrel, and platform-specific servers such as IIS and HTTP.sys.

    Note

    By default, Kestrel is the HTTP server used in ASP.NET Core templates; however, that can be overridden as required.

  • Strong tooling support, in the form of VS Code, Visual Studio, and the DOTNET CLI, along with the project templates, means developers can start working on implementing the business logic with very little setup.
  • Finally, the entire framework is open sourced and is available at https://github.com/aspnet/AspNetCore.

So now we know why we picked ASP.NET Core as our framework to develop RESTful services. Let's now look into some of the key components that assist in execution of the request and create a sample web API using the following command:

dotnet new webapi -o TestApi

Once the preceding command is successfully executed, let's navigate to the TestApi folder and open it in Visual Studio 2019 or VS Code to see the various files that are generated, as shown in the following screenshot:

Figure 10.2 – Test web API project in VS Code

Figure 10.2 – Test web API project in VS Code

Here you can see a few classes, such as Program, Startup, and settings files, such as appsettings.json, that are used to run a web API project, and there is WeatherForecast, which is a model class used in the controller class. Let's examine each of the components of the Test API in the following sections.

Program and Startup classes

The Program and Startup classes are the classes that are used to bootstrap web API projects in ASP.NET Core. Let's look at the activities performed by the Program class in the following steps:

  1. The Program class is the entry point for our web API, and it tells ASP.NET Core to begin execution from the Main method whenever someone executes the web API project. Primarily, this is the class that is used to bootstrap the application. As you can see, it follows the typical convention of console applications that has the static void Main method and .NET Core looks for the main method with which to begin execution. Similarly, in ASP.NET Core applications, the Program class has this static void Main method that the ASP.NET Core runtime looks for in order to begin execution of the application.
  2. Inside the Main method we have CreateHostBuilder(args).Build().Run(), which calls the CreateHostBuilder method to get an instance of IHostBuilder, which is nothing but the Host for our application. Earlier, we discussed the fact that ASP.NET Core comes with an inbuilt HTTP server implementation and various middlewares to plug in and that Host is nothing more than an object that encapsulates these components, like the HTTP server defaulted to Kestrel, all the middleware components, and any additional services, such as logging, that are injected. CreateHostBuilder internally calls CreateDefaultBuilder and ConfigureWebHostDefaults.
  3. The CreateDefaultBuilder method loads all the configurations from various providers, such as appsettings.json, environment variables, and any command-line parameters (see that args are passed as parameters). Then, default logging providers are loaded so that all the logs are logged to the console and debug window.
  4. ConfigureWebHostDefaults enables Kestrel as the default HTTP server and initializes the Startup class.
  5. Additionally, we can call additional methods on both CreateDefaultBuilder and ConfigureWebHostDefaults. Let's say, for example, that we wanted to call an additional configuration provider, along the lines of what we implemented in Chapter 6, Configuration in .NET Core, or change some Kestrel default parameters, we can configure additional parameters as shown in the following code snippet:

    public static IHostBuilder CreateHostBuilder(string[] args) =>

        Host.CreateDefaultBuilder(args)

            .ConfigureWebHostDefaults(webBuilder =>

            {

                webBuilder.UseStartup<Startup>()

                .ConfigureKestrel((options) =>

                {

                    options.AddServerHeader = false;

                });

            });

    From the preceding code, we see that finally, on the object of IhostBuilder, the following applies:

    • The Build() method is called to run the actions and initialize the Host.
    • The Run() method is called to keep the Host running.

Now we have the Host loaded with all the default components and it is up and running. As part of this, we have seen that one of the steps is to instantiate the Startup class. Let's examine what the Startup class is for and see how it can be used to inject additional ASP.NET Core classes/application-specific classes (repositories, services, options) and middlewares. The Startup class looks like the following:

public Startup(IConfiguration configuration)

{

    Configuration = configuration;

}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)

{

    services.AddControllers();

}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

    if (env.IsDevelopment())

    {

        app.UseDeveloperExceptionPage();

    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>

    {

        endpoints.MapControllers();

    });

}

As you can see, primarily, the Startup class has two methods aside from the IConfiguration property, which is initialized to the application configuration (the configuration is loaded during host setup) in the constructor using constructor injection. This property can further be used in the Startup class to load any of the configurations as explained in Chapter 6, Configuration in .NET Core. The two other methods in the Startup class have the following functionality:

  • ConfigureServices: This method is used to inject any ASP.NET Core provided services so that applications can use those services. A few of the common services that enterprise applications can inject are shown in the following code snippet:

    services.AddAuthentication() // To enable authentication.

    services.AddControllers(); // To enable controllers like web API.

    services.AddControllersWithViews(); // To enable controller with views.

    services.AddDistributedMemoryCache(); // To enable distributed caching.

    services.AddApplicationInsightsTelemetry(appInsightInstrumentKey); // To enable application insights telemetry.

    services.AddDbContext<EmployeeContext>(options =>

     options.UseSqlite(Configuration.GetConnectionString("EmployeeContext"));

    }); // Integrating SQL lite.

    Apart from services provided by ASP.NET Core, we can also inject any custom services specific to our application. For example, ProductService can be mapped to IProductService and can be made available for the entire application. Primarily, this is the place where we can use to plumb anything into the Dependency Injection (DI) container, as explained in Chapter 5, Dependency Injection in .NET. Some of the key points regarding the ConfigureServices method are as follows:

  • As this loads application-specific services, this method is optional.
  • All the services, including ASP.NET Core services and custom services, can be plumbed into the application and are available as extension methods of IServiceCollection.
  • Configure: This method is used to integrate all the middlewares required to be applied to the request pipeline. This method primarily controls how applications respond to the HTTP requests, that is, how applications should respond to exceptions or how they should respond to static files, or how URI routing should happen. All can be configured in this method. Additionally, any specific handling on a request pipeline, such as calling a custom middleware or adding specific response headers, or even defining a specific endpoint, can be implemented here in the configure method. So, apart from what we have seen earlier, the following code snippet shows a few common additional configurations that can be integrated in this method:

    // Endpoint that responds to /subscribe route.

    app.UseEndpoints(endpoints =>

    {

    endpoints.MapGet("/subscribe", async context =>

    {   

      await context.Response.WriteAsync("subscribed");

    });

    });

    // removing any unwanted headers.

    app.Use(async (context, next) =>

    {

    context.Response.Headers.Remove("X-Powered-By");

    context.Response.Headers.Remove("Server");

    await next().ConfigureAwait(false);

    });

    Here, app.UseEndpoints is configuring a response for a URI that matches /subscribe. app.UseEndPoints works alongside routing rules and is explained in the Handling requests using controllers and actions section, while app.Use, on the other hand, is used to add an inline middleware. In this case, we are removing X-Powered-By, Server response headers from the response.

To sum up, the Program and Startup classes play a vital role in bootstrapping the application and then customizing application services and HTTP request/response pipelines as needed. Let's now see how middlewares help in customizing the HTTP request/response pipeline.

Note

The Configure and ConfigureServices methods can be part of the Program class as there are methods available in IHostBuilder, IWebHostBuilder, and WebHostBuilderExtensions. However, using the Startup class makes the code much cleaner.

Understanding middleware

We have been referring to middleware for a while now, so let's understand what middlewares are and how we can we build one and use it in our enterprise application. Middlewares are classes that intercept the incoming requests, perform some processing on the request, and then hand it over to the next middleware or skip it as required. Middlewares are bidirectional, hence all the middlewares intercept both requests and responses. Let's assume that an API retrieves product information and, in the process, it goes through various middlewares. Representing them in pictorial form would look something like this:

Figure 10.3 – Middleware processing

Figure 10.3 – Middleware processing

Each middleware has an instance of Microsoft.AspNetCore.Http.RequestDelegate. As a result of using this, the middleware invokes the next middleware. So typically, flows would process the request as per some processing logic that you want the middleware to perform on the request and then invoke RequestDelegate to hand the request over to the next middleware in the pipeline.

If we take an analogy from manufacturing, it would be like an assembly line in a manufacturing process, where parts are added/modified from workstation to workstation until the final product is produced. In the previous diagram, let's consider each middleware as a workstation, so it will be going through the following steps: (The following explanation of each middleware is just a hypothetical explanation for our understanding; the internal workings of these middlewares differ slightly from what is explained here. More details can be found here: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder?view=aspnetcore-3.1&viewFallbackFrom=aspnetcore-5.0.

  • UseHttpsRedirection: An HTTP request arrives for GET/Products and is inspected for the protocol. If the request is via HTTP, a redirect is sent back through the HTTP status code; if the request is on HTTPS, it's handed over to the next middleware.
  • UseStaticFiles: If the request is for a static file (usually detected based on the extension – the MIME type), this middleware processes the request and sends the response back, or else hands the request on to the next middleware. Here, as you can see, if the request is for a static file, the rest of the pipeline is not even executed as this middleware can process the complete request, thereby reducing the load on the server for any unwanted processing and also reducing the response time. This process is also known as short circuiting, which every middleware can support.
  • UseRouting: The request is inspected further and the controller/action that can process the request is identified. If there isn't any match, this middleware usually responds with a 404 HTTP status code.
  • UseAuthorization: Here, if the controller/action needs to be available for authenticated users, then this middleware will look for any valid token in the header and respond accordingly.

Once the controller gets the data from services/repositories, the response goes through the same middlewares in reverse order, that is, UseAuthorization first, followed by UseHttpsRedirection, and the response is processed as needed.

As mentioned earlier, all middlewares are installed using the Configure method (either in the Startup class or the Program class) and are configured using extension methods of an instance of the class that implements the IApplicationBuilder interface, which is passed as a parameter to the Configure method. So, the order of middleware execution would follow precisely the way it is configured in the Configure method.

Armed with this understanding, let's create a middleware that will be used to handle exceptions across the RESTful services of our e-commerce application, so instead of polluting code with try…catch blocks, we will create a middleware that gets installed at the beginning of the request pipeline and then catches any exceptions throughout.

Building a custom middleware

As the middleware is going to be reused across all RESTful services, we will add the middleware to the Packt.Ecommerce.Common project inside the Middlewares folder.

Let's first create a POCO class that represents the response in case of errors. Typically, this model will hold the error message, a unique identifier to search in our log store application insights, and an inner exception if needed. In production environments, an inner exception should not be exposed; however, for development environments, we can send the inner exception for debugging purposes, and we will control this behavior inside our middleware logic using a configuration flag. So, on this basis, add a class file named ExceptionResponse inside the Models folder of the Packt.Ecommerce.Common project and add the following code:

public class ExceptionResponse

{

    public string ErrorMessage { get; set; }

    public string CorrelationIdentifier { get; set; }

    public string InnerException { get; set; }

}

Now, create another POCO class that can hold the configuration to toggle the behavior of sending an inner exception in our response. This class will be populated using the Options pattern, which was discussed in Chapter 6, Configuration in .NET Core. Since it needs to hold only one setting, it will have one property. Add a class file named ApplicationSettings in the Options folder and then add the following code to it:

public class ApplicationSettings

{

    public bool IncludeExceptionStackInResponse { get; set; }

}

This class will be extended further for any configuration that will be common across all our APIs.

Navigate to the Middlewares folder and create a class named ErrorHandlingMiddleware. As we discussed, one of the key properties in any middleware is a property of the RequestDelegate type. Additionally, we will add a property for ILogger to log the exception to our logging provider, and finally, we will add a property of the bool includeExceptionDetailsInResponse type to hold the flag that controls masking the inner exception. With this, the ErrorHandlingMiddleware class will be as follows:

public class ErrorHandlingMiddleware

{

private readonly RequestDelegate requestDelegate;

private readonly ILogger logger;

private readonly bool includeExceptionDetailsInResponse;

}

Add a parameterized constructor where we inject RequestDelegate and ILogger for our logging provider and IOptions<ApplicationSettings> for configuration and assign them to the properties created earlier. Here again, we are relying on the constructor injection of ASP.NET Core to instantiate the respective objects. With this, the constructor of ErrorHandlingMiddleWare will appear as follows:

public ErrorHandlingMiddleware(RequestDelegate, ILogger<ErrorHandlingMiddleware> logger, IOptions<ApplicationSettings> applicationSettings)

{

    NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));

    this.requestDelegate = requestDelegate;

    this.logger = logger;

    this.includeExceptionDetailsInResponse = applicationSettings.Value.IncludeExceptionStackInResponse;

}

Finally, add the InvokeAsync method that will have the logic to process on the request and then call the next middleware using RequestDelegate. Since this is an exceptionn-handling middleware as part of our logic, all we are going to do is wrap the request in a try…catch block. In the catch block, we will log it to the respective logging provider using ILogger, and finally send an object, ExceptionResponse, back as the response. With this, InvokeAsync will appear as follows:

public async Task InvokeAsync(HttpContext context)

{

    try

    {

        if (this.requestDelegate != null)

        {

            // invoking next middleware.

this.requestDelegate.Invoke(context).ConfigureAwait(false);

        }

    }

    catch (Exception innerException)

    {

        this.logger.LogCritical(1001, innerException, "Exception captured in error handling middleware"); // logging.

        ExceptionResponse currentException = new ExceptionResponse()

        {

            ErrorMessage = Constants.ErrorMiddlewareLog,

            CorrelationIdentifier = System.Diagnostics.Activity.Current?.RootId,

        };

        if (this.includeExceptionDetailsInResponse)

        {

            currentException.InnerException = $"{innerException.Message} {innerException.StackTrace}";

        }

        context.Response.StatusCode = StatusCodes.Status500InternalServerError;

        context.Response.ContentType = "application/json";

  await context.Response.WriteAsync(JsonSerializer.Serialize(innerException)).ConfigureAwait(false);

    }

}

Now we can inject this middleware into the Configure method of the Startup class with the following code:

app.UseMiddleware<GlobalExceptionHandlingMiddleware>();

Since this is an exception handler, it is recommended to configure it at the beginning of the Configure method so that any exceptions in all subsequent middlewares are caught. Additionally, we need to ensure that we map the ApplicationSettings class to a configuration, so add the following code to the ConfigureServices method:

services.Configure<ApplicationSettings>(this.Configuration.GetSection("ApplicationSettings"));

Add the relevant section to appsettings.json:

"ApplicationSettings": {

    "IncludeExceptionStackInResponse": true

  }

Now, if there is any error in any of our APIs, the response will look like the one shown in the following code snippet:

{

"ErrorMessage": "Exception captured in error handling middleware",

"CorrelationIdentifier": "03410a51b0475843936943d3ae04240c ",

"InnerException": "No connection could be made because the target machine actively refused it.    at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)    at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)    at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)    at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)    at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)    at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)    at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)    at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)    at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)    at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts, CancellationToken callerToken, Int64 timeoutTime)    at Packt.Ecommerce.Product.Services.ProductsService.GetProductsAsync(String filterCriteria) in src\platform-apis\services\Packt.Ecommerce.Product\Services\ProductsService.cs:line 82    at Packt.Ecommerce.Product.Controllers.ProductsController.GetProductsAsync(String filterCriteria) in src\platform-apis\services\Packt.Ecommerce.Product\Controllers\ProductsController.cs:line 46    at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)    at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)    at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)    at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)    at Packt.Ecommerce.Common.Middlewares.ErrorHandlingMiddleware.InvokeAsync(HttpContext context) in src\platform-apis\core\Packt.Ecommerce.Common\Middlewares\ErrorHandlingMiddleware.cs:line 65"

}

From the preceding code snippet, we can take CorrelationIdentifier, which is 03410a51b0475843936943d3ae04240c, search the value in our logging provider, Application Insights, and we can ascertain additional information regarding the exception, as shown in the following screenshot:

Figure 10.4 – Tracing CorrelationIdentifier in App Insights

Figure 10.4 – Tracing CorrelationIdentifier in App Insights

CorrelationIdentifier is extremely helpful in production environments where there is no inner exception.

This concludes our discussion regarding middleware. In the next section, let's look at what controllers and actions are and how they help in handling requests.

Handling requests using controllers and actions

Controllers are the fundamental blocks for designing RESTful servicers using an ASP.NET Core web API. These are the primary classes that hold the logic to process the request, which includes retrieving data from a database, inserting a record into a database, and so on. Controllers are the classes where we define methods to process the request. These methods usually include validating the input, talking to a data store, applying business logic (in enterprise applications, controllers will also call service classes), and finally serializing the response and sending it back to the client using HTTP protocols in JSON/XML form. All these methods that hold the logic to process requests are known as Actions. All the requests received by the HTTP server are handed over to action methods using a routing engine. However, a routing engine transfers requests to Actions based on the certain rules that can be defined in a request pipeline. These rules are what we define in routing. Let's see how a URI is mapped to a particular action in a controller.

Understanding ASP.NET Core routing

Till now, we have seen that any HTTP request goes through the middleware and is finally handed over to the controller or an endpoint defined in the configure method, but who is responsible for this handover to a controller/endpoint and how does ASP.NET Core know which controller and method inside the controller to trigger? That is what the routing engine is for, which was injected when adding the following middlewares:

app.UseRouting();

app.UseEndpoints(endpoints =>

{

     endpoints.MapControllers();

});

Here, app.UseRouting() injects Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware, which is used to take all the routing decisions based on the URI. The primary job of this middleware is to set the instance of the Microsoft.AspNetCore.Http.Endpoint method with the value of the action that needs to be executed for a particular URI.

For example, if we are trying to get the details of a product according to its ID and have a product controller that has the GetProductById method to fulfill this request, when we make an API call to the api/products/1 URI, putting a breakpoint in a middleware after EndpointRoutingMiddleware shows you that an instance of the Endpoint class is available with information regarding the action that matches the URI and should be executed. We can see this in the following screenshot:

Figure 10.5 – Routing middlewares

Figure 10.5 – Routing middlewares

This object would be null if there wasn't any matching controller/action. Internally, EndpointRoutingMiddleware uses the URI, query string parameters, and HTTP verbs and request headers to find the correct match.

Once the correct action method is identified, it's the job of app.UseEndPoints to hand over control to the action method identified by the Endpoint object and execute it. UseEndPoints injects Microsoft.AspNetCore.Routing.EndpointMiddleware to execute the appropriate method to fulfill a request. One important aspect of populating an appropriate EndPoint object is the various URIs that are configured inside UseEndPoints that can be achieved through the static extension methods available in ASP.NET Core. For example, if we want to configure just controllers, we can use MapControllers extension methods, which add endpoints for all actions in controllers for UseRouting to match further. If we are building RESTful APIs, it is recommended to use MapControllers extensions. However, there are many such extension methods for the following extensions that are commonly used:

  • MapGet/MapPost: These are the extension methods that can match specific patterns for GET/POST verbs and execute the request. They accept two parameters, one being the pattern of the URI and second the request delegate that can be used to execute when the pattern is matched. For example, the following code can be used to match the /aboutus route and respond with the text, Welcome to default products route:

    endpoints.MapGet("/aboutus", async context =>

    {

    await context.Response.WriteAsync("Welcome to default products route");

    });

  • MapRazorPages: This extension method is used if we are using Razor Pages and need to route to appropriate pages based on routes.
  • MapControllerRoute: This extension method can be used to match controllers with a specific pattern; for example, the following code can be seen in the ASP.NET Core MVC template, which matches methods based on a pattern:

    endpoints.MapControllerRoute(

    name: "default",

    pattern: "{controller=Home}/{action=Index}/{id?}");

The request URI is split based on the forward slash(/) and is matched to the controller, action method, and ID. So, if you wanted to match a method in a controller, you need to pass the controller name (ASP.NET Core automatically suffixes the controller keyword) and method name in the URI. Optionally, the ID can be passed as a parameter to that method. For example, if I have GetProducts in ProductsController, you would be calling it using the absolute URI, products/GetProducts. This kind of routing is known as conventional routing and is a good fit for UI-based web applications, and so can be seen in the ASP.NET Core MVC template.

This concludes our discussion of the basics of routing and there are many such extension methods available in ASP.NET Core that can be plumbed into the request pipeline based on application requirements. Now, let's look at attribute-based routing, the routing technique recommended for RESTful services built using ASP.NET Core.

Note

Another important aspect of routing, as with any other middleware sequence, is that injection is very important and UseRouting should be called before UseEndpoints.

Attribute-based routing

For RESTful services, conventional routing contravenes a few REST principles, especially the principle that states that the operation on entities performed by the action method should be based on HTTP verbs, so ideally, in order to get products, the URI should be GET api/products. This is where attribute-based routing comes into play, in which routes are defined using attributes either at the controller level, or at the action method level, or both. This is achieved using the Microsoft.AspNetCore.Mvc.Route attribute, which takes a string value as an input parameter and is used to map the controller and action. Let's take an example of ProductsController, which has the following code:

[Route("api/[controller]")]

[ApiController]

public class ProductsController : ControllerBase

{

    [HttpGet]

    [Route("{id}")]

    public IActionResult GetProductById(int id)

    {

        return Ok($"Product {id}");

    }

    [HttpGet]

    public IActionResult GetProducts()

    {

        return Ok("Products");

    }

}

Here, in the Route attribute at the controller level, we are passing the value api/[controller], which means that any URI matching api/products is mapped to this controller, where products is the name of the controller. Using the controller keyword inside square brackets is a specific way to tell ASP.NET Core to map the controller name automatically to the route. However, if you want to stick to a specific name irrespective of the controller name, this can be used without square brackets. As a best practice, it is recommended to decouple controller names with routes. Hence, for our e-commerce application, we will go with exact values in routes, that is, ProductsController will have a route prefix of [Route("api/products")].

The Route attribute can also be added to action methods and can be used to additionally identify specific methods uniquely. Here, we are also passing a string that can be used to identify the method. For example, [Route("GetProductById/{id}")] would be matched to the URI api/products/GetProductById/1, and the value inside the curly brackets is a dynamic value that can be passed as a parameter to the action method and match with the parameter name. What this means is that in the preceding code, there is a parameter ID, and the value inside the curly brackets should also be named ID so that ASP.NET Core can map values from the URI to the method parameter. Hence, for the api/products/1 URI, the ID parameter in the GetProductById method will have a value of 1 if the route attribute looks like [Route("{id}")].

Finally, the HTTP verb is represented by attributes such as [HttpGet], which will be used to map the HTTP verb from the URI to the method. The following table shows various examples and possible matches, assuming that ProductsController has [Route("api/products")]:

Table 10.1

Table 10.1

As you can see, the name of the method is immaterial here and so is not part of the URI matching unless it is specified in the Route attribute.

Note

One important aspect is that the web API supports the reading of parameters from various locations within a request, be it in the request body, header, query string, or URI. The following documentation covers the various options available: https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0#binding-source-parameter-inference.

A summary of an entire API routing in ASP.NET Core can be represented as follows:

Figure 10.6 – ASP.NET Core API routing

Figure 10.6 – ASP.NET Core API routing

Attribute-based routing is more RESTful, and we will follow this kind of routing in our e-commerce services. Now, let's look at the various helper classes available in ASP.NET Core that can be used to simplify the building of RESTful services.

Tip

The expression {id} in routing is known as a routing constraint, and ASP.NET Core comes with a varied set of such routing constraints that can also be found here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0#route-constraint-reference.

The ControllerBase class, the ApiController attribute, and the ActionResult class

If we go back to any of the controllers created hitherto, you can see that all the controllers are inherited from the ControllerBase class. In ASP.NET Core, ControllerBase is an abstract class that provides various helper methods that assist in handling requests and responses. For example, if I wanted to send an HTTP status code 400 (bad request), there is a helper method, BadRequest, in ControllerBase that can be used to send an HTTP status code of 400, otherwise we have to manually create an object and populate it with the HTTP status code 400. There are many such helper methods in ControllerBase that are available out of the box; however, it's not necessary that every API controller should be inherited from the ControllerBase class. All the helper methods from the ControllerBase class are mentioned here: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase?view=aspnetcore-3.1&viewFallbackFrom=aspnetcore-5.0.

This brings us to a discussion as to what the return type of our controller methods should be, because there could be at least two possible responses for any API in general, as follows:

  • A successful response with a 2xx status code and which possibly responds with a resource or a list of resources
  • A validation failure case with a 4xx status code

To handle such scenarios, we need to create a generic type that can be used to send different response types, and this is where ASP.NET Core's IActionResult and ActionResult types come into play, providing us with derived response types for various scenarios. A few of the important response types that IActionResult supports are as follows:

  • OkObjectResult: This is a response type that sets the HTTP status code to 200 and adds the resource to the body of the response containing the details of the resource. This type is ideal for all the APIs that respond with the resource or a list of resources, for example, get products.
  • NotFoundResult: This is a response type that sets the HTTP status code to 404 and an empty body. This can be used if a particular resource is not found. However, in the case of a resource not found, we will use NoContentResult (204), as 404 will also be used for an API not found.
  • BadRequestResult: This is a response type that sets the HTTP status code to 400 and an error message in the response body. This is ideal for any validation failures.
  • CreatedAtActionResult: This is a response type that sets the HTTP status code to 201 and can add the newly created resource URI to the response. This is ideal for APIs that create resources.

All these response types are inherited from IActionResult and there are methods available in the ControllerBase class that can create these objects, so IActionResult, along with ControllerBase, would solve most of the business requirements, and this is what we will have as the return type for all our API controller methods.

The final important class available in ASP.NET Core that comes in handy is the ApiController class, which can be added as an attribute to the controller class or to an assembly, and adds the following behaviors to our controllers:

  • It disables conventional routing and makes attribute-based routing mandatory.
  • It validates models automatically, so we don't need to explicitly call ModelState.IsValid in every method. This behavior is very useful in case of insert/update methods.
  • It facilitates automatic parameter mapping from the body/route/header/query strings. What this means is that we don't specify whether a parameter of an API is going to be part of the body or route. For example, in the following code, we don't need to explicitly say that the ID parameter is going to be part of the route as ApiController automatically uses something known as inference rules and a prefix in the ID with [FromRoute]:

    [Route("{id}")]

    public IActionResult GetProductById(int id)

    {

      return Ok($"Product {id}");

    }

  • Similarly, in the following code snippet, ApiController will automatically add [FromBody] based on the inference rules:

    public IActionResult CreateProduct(Product product)

    {

    //

    }

  • A couple of other behaviors that ApiController adds are inferring request content to multipart/form data and more detailed error responses as per https://tools.ietf.org/html/rfc7807.

So, all in all, ControllerBase, ApiController, and ActionResult provide various helper methods and behaviors, thereby providing developers with all the tools needed to write RESTful APIs and allowing them to focus on business logic while writing APIs using ASP.NET Core.

With this foundation, let's design various APIs for our e-commerce application in the next section.

Integration with the data layer

The response from our APIs may or may not look like our domain models. Instead, their structure can resemble the fields that the UI or Views need to bind. Hence, it is recommended to create a separate set of POCO classes that integrates with our UI. These POCOs are known as Data Transfer Objects (DTOs).

In this section, we will implement our DTOs, domain logic integrating with the data layer, and integrate the cache services discussed in Chapter 8, Understanding Caching, using the Cache-Aside pattern, and then finally the required RESTful APIs using controllers and actions. Along the way, we will use HTTP Client factory for our service-to-service communication, and the AutoMapper library for mapping domain models to DTOs.

We will pick a products service that is part of Packt.Ecommerce.Product, a web API project using .NET 5, and discuss its implementation in detail. By the end of this section, we will have implemented the projects highlighted in the following screenshot:

Figure 10.7 – Product service and DTOs

Figure 10.7 – Product service and DTOs

Similar implementation is replicated across all RESTful services with slight modifications in business logic, as required, but the high-level implementation remains the same across the following various services:

  • Packt.Ecommerce.DataAccess
  • Packt.Ecommerce.Invoice
  • Packt.Ecommerce.Order
  • Packt.Ecommerce.Payment
  • Packt.Ecommerce.UserManagement

To start with, we will have the corresponding section in appsettings.json, which is shown as follows:

    "ApplicationSettings": {

    "UseRedisCache": false, // For in-memory

    "IncludeExceptionStackInResponse": true,

    "DataStoreEndpoint": "",

    "InstrumentationKey": ""

  },

  "ConnectionStrings": {

    "Redis": ""

  }

For the local development environment, we will use Manage User Secrets, as explained here, https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0&tabs=windows, and set the following values. However, once the service is deployed, it will make use of Azure KeyVault, as explained in Chapter 6, Configuration in .NET Core:

{

  "ApplicationSettings:InstrumentationKey": "", //relevant key

  "ConnectionStrings:Redis": "" //connection string

}

Let's begin by creating DTOs for the Products API.

Creating DTOs

The key requirements in terms of product services are to provide the ability to search the products, view additional details relating to the products, and then proceed with purchase. Since a listing of products can have limited details, let's create a POCO (all DTOs are created in the Packt.Ecommerce.DTO.Models project), and name it ProductListViewModel. This class will have all the properties that we want to show on the product's list page, and it should look like the following:

public class ProductListViewModel

{

        [JsonProperty(PropertyName = "id")]

        public string Id { get; set; }

        public string Name { get; set; }

        public int Price { get; set; }

        public Uri ImageUrl { get; set; }

        public double AverageRating { get; set; }

}

As you can see, these are minimum fields that are usually displayed on any e-commerce application. Hence, we will go with these fields, but the idea is to extend as the application evolves. Here, the Id and Name properties are important properties as those will be used to query the database once the user wants to retrieve all further details regarding the product. We are annotating the Id property with the JsonProperty(PropertyName = "id") attribute to ensure that the property name remains as Id during serialization and deserialization. This is important because in our Cosmos DB instance, we are using Id as the key for most of the containers. Let's now create another POCO that represents the details of a product, as shown in the following code snippet:

public class ProductDetailsViewModel

{

        [Required]

        public string Id { get; set; }

        [Required]

        public string Name { get; set; }

        [Required]

        public string Category { get; set; }

        [Required]

        [Range(0, 9999)]

        public int Price { get; set; }

        [Required]

        [Range(0, 999, ErrorMessage = "Large quantity, please reach out to support to process request.")]

        public int Quantity { get; set; }

        public DateTime CreatedDate { get; set; }

        public List<string> ImageUrls { get; set; }

        public List<RatingViewModel> Rating { get; set; }

        public List<string> Format { get; set; }

        public List<string> Authors { get; set; }

        public List<int> Size { get; set; }

        public List<string> Color { get; set; }

        public string Etag { get; set; }

}

public class RatingViewModel

{

        public int Stars { get; set; }

        public int Percentage { get; set; }

}

Here, you can see that POCO is very much like our domain model, and that's because of our denormalized domain models. However, in general, if we go with normalized domain models, you will notice a significant difference between domain models and DTOs. Here, it can be argued further as to why we cannot reuse the Product domain model. However, for extensibility purposes, and for further loosely coupling domain models with our UI, it's preferable to go with separate POCO classes. So, in this DTO, apart from Id and Name, one of the important properties is Etag, which will be used for entity tracking to avoid concurrent overwrites on an entity. For example, if two users access a product and user A updates it before user B, using Etag, we can stop user B overwriting user A's changes and force user B to take the latest copy of the product prior to updating.

Another important aspect is that we are using ASP.NET Core's built-in validation attributes on our model to define all the constraints on the models. Primarily, we will be using the [Required] attribute and any relevant attributes as per https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#built-in-attributes.

All the DTOs would be part of the Packt.Ecommerce.DTO.Models project as they will be reused in our ASP.NET MVC application, which will be used to build the UI of our e-commerce application. Now, let's look at the contracts needed for the Products service.

Service class contracts

Add a Contracts folder to Packt.Ecommerce.Product and create a contract/interface of a product's service class, for which we will refer our requirements and define methods as needed. To start with, it will have all the methods to perform CRUD operations on products based on that interface, and these will appear as follows:

public interface IProductService

    {

        Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null);

        Task<ProductDetailsViewModel> GetProductByIdAsync(string productId, string productName);

        Task<ProductDetailsViewModel> AddProductAsync(ProductDetailsViewModel product);

        Task<HttpResponseMessage> UpdateProductAsync(ProductDetailsViewModel product);

        Task<HttpResponseMessage> DeleteProductAsync(string productId, string productName);

    }

Here you can see that we are returning Task in all methods, thereby sticking to our asynchronous approach discussed in Chapter 4, Threading and Asynchronous Operations.

The mapper class using AutoMapper

The next thing that we will need is a way to transform our domain models to DTOs, and here we will use a well-known library called AutoMapper to configure and add the following packages:

  • Automapper
  • AutoMapper.Extensions.Microsoft.DependencyInjection

To configure AutoMapper, we need to define a class that inherits from AutoMapper.Profile and then defines mapping between various domain models and DTOs. Let's add a class, AutoMapperProfile, to project Packt.Ecommerce.Product:

    public class AutoMapperProfile : Profile

    {

        public AutoMapperProfile()

        {

        }

    }

AutoMapper comes with many inbuilt methods for mapping, one of these being CreateMap, which accepts source and destination classes and maps them based on the same property names. Any property that does not have the same name can be manually mapped using the ForMember method. Since ProductDetailsViewModel has one-to-one mapping with our domain model, CreateMap should be good enough for their mapping. For ProductListViewModel, we have an additional field, AverageRating, for which we wanted to calculate the average of all the ratings given for a particular product. To keep it simple, we will use the Average method from Linq and then map it to the average rating. For modularization, we will have this in a separate method, MapEntity, that appears as follows:

private void MapEntity()

{

            this.CreateMap<Data.Models.Product, DTO.Models.ProductDetailsViewModel>();

            this.CreateMap<Data.Models.Rating, DTO.Models.RatingViewModel>();

            this.CreateMap<Data.Models.Product, DTO.Models.ProductListViewModel>()

                .ForMember(x => x.AverageRating, o => o.MapFrom(a => a.Rating != null ? a.Rating.Average(y => y.Stars) : 0));

}

Now, modify the constructor to call this method.

The final step involved in setting up AutoMapper is to inject it as one of the services, for which we will use the ConfigureServices method's Startup class, using the following line:

services.AddAutoMapper(typeof(AutoMapperProfile));

As explained earlier, this will inject the AutoMapper library into our API, and this will allow us to inject AutoMapper into various services and controllers. Let's now look at the configuration of the HttpClient factory, which is used for calling the data access service.

HttpClient factory for service-to-service calls

To retrieve data, we must call APIs exposed by our data access service defined in Packt.Ecommerce.DataAccess. For this we need a resilient library that can effectively use the available sockets, allowing us to define a circuit breaker as well as retry/timeout policies. IHttpClientFactory is ideal for such scenarios.

Note

One of the common issues with HttpClient is the potential SocketException, which happens as HttpClient leaves the TCP connection open even after the object has been disposed of, and the recommendation is to create HttpClient as a static/singleton, which has its own overheads, while connecting to multiple services. The following link summarizes all these issues, https://softwareengineering.stackexchange.com/questions/330364/should-we-create-a-new-single-instance-of-httpclient-for-all-requests, and these are all now addressed by IhttpClientFactory.

To configure IHttpClientFactory, perform the following steps:

  1. Install Microsoft.Extensions.Http.
  2. We will be configuring IHttpClientFactory using typed clients, so add a Services folder and a ProductsService class and inherit them from IProductService. For now, leave the implementation empty. Now, map IProductService and ProductsService in ConfigureServices of the Startup class using the following code:

    services.AddHttpClient<IProductService, ProductsService>()

           .SetHandlerLifetime(TimeSpan.FromMinutes(5))

           .AddPolicyHandler(RetryPolicy()) // Retry policy.

           .AddPolicyHandler(CircuitBreakerPolicy()); // Circuit breakerpolicy

Here, we are defining the timeout for HttpClient used by ProductsService as 5 minutes and additionally configuring a policy for retries and a circuit breaker.

Implementing a circuit breaker policy

To define these policies, we will use a library called Polly, which gives out-of-the-box resiliency and fault handling capabilities. Install the Microsoft.Extensions.Http.Polly package and then add the following static method to the Startup class that defines our circuit breaker policy:

private static IAsyncPolicy<HttpResponseMessage> CircuitBreakerPolicy()

{

    return HttpPolicyExtensions

        .HandleTransientHttpError()

        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));

}

Here, we are saying that the circuit would be opened if there are 5 failures within 30 seconds. A circuit breaker assists in avoiding unnecessary HTTP calls where there is a critical failure that cannot be fixed with a retry.

Implementing a retry policy

Now, let's add our retry policy, which is bit smarter compared with the standard retries that retire within a specified timeframe. So, we define a policy that will effect a retry and HTTP service calls on five occasions and each retry would have a time difference in seconds at a rate of power of two. To add some randomness in terms of the time variation, we will use a Random class of C# to generate a random number and add it to the time gap. This random generation will be as shown in the following code:

Random random = new Random();

TimeSpan.FromSeconds(Math.Pow(2, retry))  + TimeSpan.FromMilliseconds(random.Next(0, 100));

Here, retry is an integer that increments by one with every retry. With this, add a static method to the Startup class that has the preceding logic:

private static IAsyncPolicy<HttpResponseMessage> RetryPolicy()

{

    Random random = new Random();

    var retryPolicy = HttpPolicyExtensions

        .HandleTransientHttpError()

        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)

        .WaitAndRetryAsync(

        5,

        retry => TimeSpan.FromSeconds(Math.Pow(2, retry))

                          + TimeSpan.FromMilliseconds(random.Next(0, 100)));

    return retryPolicy;

}

This completes our HTTP client factory configuration, and ProductsService can use constructor injection to instantiate IHttpClientFactory, which can be further used to create HttpClient.

With all this configuration, we can now implement our service class. Let's look at that in the next section.

Implementing service classes

Let's now implement ProductsService by starting by defining various properties that we have now built and instantiating them using constructor injections, as shown in the following code block:

private readonly IOptions<ApplicationSettings> applicationSettings;

private readonly HttpClient httpClient;

private readonly IMapper autoMapper;

private readonly IDistributedCacheService cacheService;

public ProductsService(IHttpClientFactory httpClientFactory, IOptions<ApplicationSettings> applicationSettings, IMapper autoMapper, IDistributedCacheService cacheService)

{

    NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));

    IHttpClientFactory httpclientFactory = httpClientFactory;

    this.applicationSettings = applicationSettings;

    this.httpClient = httpclientFactory.CreateClient();

    this.autoMapper = autoMapper;

    this.cacheService = cacheService;

}

All our services are going to use the same exception handling middleware we defined in this chapter, so during service-to-service calls, if there is a failure in another service, the response would be of the ExceptionResponse type. Hence, let's create a private method, so deserialize the ExceptionResponse class and raise it accordingly. This is required because HttpClient would represent success or failure while using the IsSuccessStatusCode and StatusCode properties, so if there is an exception, we need to check IsSuccessStatusCode and rethrow it. Let's call this method ThrowServiceToServiceErrors and refer to the following code:

private async Task ThrowServiceToServiceErrors(HttpResponseMessage response)

{

    var exceptionReponse = await response.Content.ReadFromJsonAsync<ExceptionResponse>().ConfigureAwait(false);

    throw new Exception(exceptionReponse.InnerException);

}

Let's now implement the GetProductsAsync method, in which we will use CacheService to retrieve data from cache and, if it is not available in cache, we will call the data access service using HttpClient and finally map the Product domains model to a DTO and return it asynchronously. The code will look like the following:

public async Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null)

{

    var products = await this.cacheService.GetCacheAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>($"products{filterCriteria}").ConfigureAwait(false);

    if (products == null)

    {

        using var productRequest = new HttpRequestMessage(HttpMethod.Get, $"{this.applicationSettings.Value.DataStoreEndpoint}api/products?filterCriteria={filterCriteria}");

        var productResponse = await this.httpClient.SendAsync(productRequest).ConfigureAwait(false);

        if (!productResponse.IsSuccessStatusCode)

        {

            await this.ThrowServiceToServiceErrors(productResponse).ConfigureAwait(false);

        }

        products = await productResponse.Content.ReadFromJsonAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>().ConfigureAwait(false);

        if (products.Any())

        {

            await this.cacheService.AddOrUpdateCacheAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>($"products{filterCriteria}", products).ConfigureAwait(false);

        }

    }

    var productList = this.autoMapper.Map<List<ProductListViewModel>>(products);

    return productList;

}

We will follow a similar pattern and implement AddProductAsync, UpdateProductAsync, GetProductByIdAsync, and DeleteProductAsync. The only difference in each of these methods would be to use the relevant HttpClient method and handle them accordingly. Now that we have our service implemented, let's implement our controller.

Implementing action methods in the controller

Let's first inject the service created in the previous section into the ASP.NET Core 5 DI container so that we can use constructor injection to create an object of ProductsService. We will do this in ConfigureServices of the Startup class using the following code:

services.AddScoped<IProductService, ProductsService>();

Also ensure that all the required framework components, such as ApplicationSettings, CacheService, and AutoMapper, are configured.

Add a controller to the Controllers folder and name it ProductsController with the default route as api/products, and then add a property of IProductService and inject it using constructor injection. The controller should implement five action methods, each calling one of the service methods, and use various out-of-the-box helper methods and attributes discussed in The ControllerBase class, ApiController attribute, and ActionResult class section of this chapter. The methods for retrieving specific products and creating a new product are shown in the following code block:

[HttpGet]

[Route("{id}")]

public async Task<IActionResult> GetProductById(string id, [FromQuery][Required]string name)

{

    var product = await this.productService.GetProductByIdAsync(id, name).ConfigureAwait(false);

    if (product != null)

    {

        return this.Ok(product);

    }

    else

    {

        return this.NoContent();

    }

}

[HttpPost]

public async Task<IActionResult> AddProductAsync(ProductDetailsViewModel product)

{

    // Product null check is to avoid null attribute validation error.

    if (product == null || product.Etag != null)

    {

        return this.BadRequest();

    }

    var result = await this.productService.AddProductAsync(product).ConfigureAwait(false);

    return this.CreatedAtAction(nameof(this.GetProductById), new { id = result.Id, name = result.Name }, result); // HATEOS principle

}

The method implementation is self-explanatory and based purely on the fundamentals discussed in the Handling requests using controllers and actions section. Similarly, we will implement all the other methods (Delete, Update, and Get all products) by calling the corresponding service method and returning the relevant ActionResult. With that, we will have APIs shown in the following table to handle various scenarios related to the product entity:

Table 10.2

Table 10.2

Tip

One of the other common scenarios with API is to have an API that supports file upload/download. The upload scenario is achieved by passing IFormFile as an input parameter to the API. This serializes the uploaded file and can also save on the server. Similarly, for file downloading, FileContentResult is available and can stream files to any client. This is left to you as an activity to explore further.

For the testing API, we will use Postman (https://www.postman.com/downloads/). All the Postman collections can be found under the Solution Items folder file, Mastering enterprise application development Book.postman_collection.json. To import a collection once Postman has been installed, perform the following steps:

  1. Open Postman and then click on File.
  2. Click Import | Upload files, navigate to the location of the Mastering enterprise application development Book.postman_collection.json file and then click on Import.

A successful import will show the collection in the Collections menu of Postman, as shown in the following screenshot:

Figure 10.8 – Collections in Postman

Figure 10.8 – Collections in Postman

This completes our Products RESTful service implementation. All the other services mentioned at the beginning of this section are implemented in a similar way, where each of them is an individual web API project and handles the relevant domain logic for that entity.

Understanding gRPC

As per grpc.io, gRPC is a high performance, open source universal Remote Procedure Call (RPC) framework. Originally developed by Google, gRPC uses HTTP/2 for transport and a Protocol Buffer (protobuf) as the interface description language. gRPC is a contract-based binary communication system. It is available across multiple ecosystems. The following diagram from gRPC's official documentation (https://grpc.io) illustrates the client-server interaction using gRPC:

Figure 10.9 – gRPC client-server interaction

Figure 10.9 – gRPC client-server interaction

Like many of the distributed systems, gRPC is based around the idea of defining the service and specifying the interface with methods that can be invoked remotely along with the contracts. In gRPC, the server implements the interface and runs the gRPC server to handle the client calls. On the client side, it has the stub, which provides the same interface as defined by the server. The client calls the stub in the same way as it invokes methods in any other local object to invoke the method on the server.

By default, the data contracts use protocol buffers to serialize the data from and to the client. The protobufs are defined in a text file with the .proto extension. In a protobuf, the data is structured as a logical record of information contained in fields. In the upcoming section, we will learn about how to define a protobuf in Visual Studio for a .NET 5 application.

Note

Refer to the official documentation to learn more about gRPC: https://grpc.io. To learn more about the protobuf, refer to https://developers.google.com/protocol-buffers/docs/overview.

Given the benefits of high performance, language-agnostic implementation, and reduced network usage associated with the protobuf of gRPC, many teams are exploring the use of gRPC in their endeavors to build microservices.

In the next section, we will learn to build a gRPC server and client in .NET 5.

Building a gRPC server in .NET

After making its first appearance in .NET core 3.0, gRPC has become a first-class citizen in the .NET ecosystem. Fully managed gRPC implementation is now available in .NET. Using Visual Studio 2019 and .NET 5, we can create gRPC server and client applications easily. Let's create a gRPC service using the gRPC service template in Visual Studio and name it gRPCDemoService:

Figure 10.10 – gRPC VS 2019 project template

Figure 10.10 – gRPC VS 2019 project template

This will create a solution with the sample gRPC service named GreetService. Let's now understand the solution created with the template. The solution created will have a package reference to Grpc.AspNetCore. This will have the libraries required to host the gRPC service and the code generator for the .proto files. This solution will have the proto file created for GreetService under the Protos solution folder. The following code snippet defines the Greeter service:

service Greeter {

  // Sends a greeting

  rpc SayHello (HelloRequest) returns (HelloReply);

}

The Greeter service has only one method named SayHello, which takes the input parameter as HelloRequest and returns a message of the HelloReply type. HelloRequest and HelloReply messages are defined in the same proto file, as shown in the following code snippet:

message HelloRequest {

  string name = 1;

}

message HelloReply {

  string message = 1;

}

HelloRequest has one file named name, and HelloReply has one field named message. The number next to the field shows the ordinal position of the field in the buffer. The proto files are compiled with the Protobuf compiler to generate the stub classes with all the plumbing required. We can specify the kind of stub classes to generate from the properties of the proto file. Since this is a server, it will have the configuration set to Server only.

Now, let's look at the GreetService implementation. This will appear as shown in the following code snippet:

public class GreeterService : Greeter.GreeterBase

{

    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)

    {

        _logger = logger;

    }

    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)

    {

        return Task.FromResult(new HelloReply

        {

            Message = "Hello " + request.Name

        });

    }

}

GreetService inherits from Greeter.GreeterService, which is generated by the protobuf compiler. The SayHello method is overridden to provide the implementation so as to return the greeting to the caller by constructing HelloReply, as defined in the proto file.

To expose gRPC services in a .NET 5 application, all the required gRPC services are to be added to the service collection by calling AddGrpc in the ConfigureServices method of the Startup class. The GreeterSerivce gRPC service is exposed by calling MapGrpcService:

app.UseEndpoints(endpoints =>

{

    endpoints.MapGrpcService<GreeterService>();

});

That is everything that is required to expose a gRPC service in a .NET 5 application. In the next section, we will implement a .NET 5 client to consume GreeterService.

Building a gRPC client in .NET

As specified in the Understanding gRPC section, .NET 5 has very good tooling for building a gRPC client as well. In this section, we will be building a gRPC client in a console application:

  1. Create a .NET 5 console application and name it gRPCDemoClinet.
  2. Now, right-click on the project and click on the menu items Add | Service reference…. This will open the Connected Services tab, as shown in the following screenshot:
    Figure 10.11 – The gRPC Connected Services tab

    Figure 10.11 – The gRPC Connected Services tab

  3. Click on the Add button under Service References (OpenAPI, gRPC), select gRPC in the Add Service Reference dialog, and then click on Next.
  4. In the Add new gRPC service reference dialog, select the File option, select the greet.proto file from gRPCDemoService, and then click on the Finish button. This will add the proto file link to the project and marks the protobuf compiler to generate the Client stub classes:
    Figure 10.12 – Adding a gRPC service reference

    Figure 10.12 – Adding a gRPC service reference

    This will also add the required NuGet packages, Google.Protobuf, Grpc.Net.ClientFactory, and Grpc.Tools, to the project.

  5. Now, add the following code snippet to the main method of the gRPCDemoClient project:

    static async Task Main(string[] args)

    {

        var channel = GrpcChannel.ForAddress("https://localhost:5001");

        var client = new Greeter.GreeterClient(channel);

        HelloReply response = await client.SayHelloAsync(new HelloRequest { Name="Suneel" });

        Console.WriteLine(response.Message);

    }

    In this code, we are creating the gRPC channel to the gRPCDemoService endpoint, and then instantiating Greeter.GreeterClinet, which is the stub to gRPCDemoService, by passing in the gRPC channel.

Now, to invoke the service, we just need to call the SayHelloAsync method on the stub by passing the HelloRequest message. This call will return HelloReply from the service.

Till now, we have created a simple gRPC service and a console client for that service. In the next section, we will learn about gRPC curl, which is a generic client to test gRPC services.

Testing gRPC services

To test or invoke a REST service, we use tools such as Postman or Fiddler. gRPCurl is a command-line utility that helps us to interact with gRPC services. Using grpcurl, we can test the gRPC services without building the client apps. grpcurl is available for download from https://github.com/fullstorydev/grpcurl.

Once grpcurl is downloaded, we can call GreeterService using the following command:

grpcurl -d "{"name": "World"}" localhost:5001 greet.Greeter/SayHello

Note

Currently, gRPC applications can only be hosted in Azure App Service and IIS. Hence, we did not leverage gRPC in the demo e-commerce application that is hosted on Azure App Service. However, there is a version of the e-commerce application in this chapter demo where obtaining a product according to its ID is exposed as a gRPC endpoint in a self-hosted service.

Summary

In this chapter, we have covered the basic principles of REST and also designed enterprise-level RESTful services for our e-commerce application.

Along the way, we got to grips with the various web API internals of an ASP.NET Core 5 web API, including routing and sample middleware, and became familiar with tools for testing our services, while learning how to handle requests using a controller and its actions, which we also learned to build. Also, we have seen how to create and test basic gRPC client and server applications in .NET 5. By now, you should be able to confidently build RESTful services using an ASP.NET Core 5 web API.

In the next chapter, we will go through the fundamentals of ASP.NET MVC, build our UI layer using ASP.NET MVC, and integrate it with our APIs.

Questions

  1. Which of the following HTTP verbs is recommended for creating a resource?

    a. GET

    b. POST

    c. DELETE

    d. PUT

  2. Which of the following HTTP status codes represents No Content?

    a. 200

    b. 201

    c. 202

    d. 204

  3. Which of the following middlewares is used to configure routing?

    a. UseDeveloperExceptionPage()

    b. UseHttpsRedirection()

    c. UseRouting()

    d. UseAuthorization()

  4. If a controller is annotated with the [ApiController] attribute, do I need to class ModelState.IsValid explicitly in each action method?

    a. Yes, model validation isn't part of the ApiController attribute, hence, you need to call ModelState.Valid in each action method.

    b. No, model validation is handled as part of the ApiController attribute, hence, ModelState.Valid is triggered automatically for all action items.

Further reading

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.81.240