Home .NET .NET Core: Microservices Interaction via Web api

.NET Core: Microservices Interaction via Web api

by admin

Introduction

Almost everyone who has dealt with microservices in .NET Core probably knows Christian Horsdal’s book, Microservices on the .NET Platform. It describes perfectly the approaches to building application based on microservices, and gives detailed description of monitoring, logging and access control issues. The only thing missing is an automation tool for microservices.
The usual approach for developing a microservice is to develop a web-client for it at the same time. And every time the web-interface of the microservice changes, you have to spend extra effort to change the web-client accordingly. The idea to generate a web-api/web-client pair using OpenApi is also quite laborious, I would like something more transparent to the developer.
So, with the alternative approach in the development of our application, we would like to :
Describe the microservice structure with an interface using attributes that describe the method type, route, and parameter passing method, as is done in MVC.
Microservice functionality is developed solely in the classroom,
that implements this interface. Publication of microservice endpoints should be automatic, not requiring complex customization.
The Web client for the microservice should be generated automatically based on the interface and provided via Dependency Injection.
There should be a mechanism to organize redirects to microservices endpoints from the main application interacting with the user interface.
The Nuget package Shed.CoreKit.WebApi is designed to meet these criteria. This is supplemented by the auxiliary package Shed.CoreKit.WebApi.Abstractions, which contains attributes and classes that can be used when developing generic build projects where the functionality of the main package is not required.
Below we look at using the capabilities of these packages when developing the MicroCommerce application described in Christian Horsdal’s book mentioned above.
Hereinafter we will use the following terminology :
Microservice – ASP.NET Core application (project) that can run console, under Internet Information Services (IIS) or in a Docker container.
Interface – A .NET entity, a set of methods and properties with no implementation.
Endpoint – The path to the root of a microservice application or interface implementation. Examples : localhost :5001, localhost :5000/products
Route – The path to the interface method from the endpoint. Can be defined by default in the same way as in MVC or set with an attribute.

MicroCommerce application structure

  1. ProductCatalog is a microservice that provides information about products.
  2. ShoppingCart is a microservice that provides information about the user’s purchases, as well as the ability to add/delete purchases. Events are generated to notify other microservices when the status of the user’s cart changes.
  3. ActivityLogger is a microservice that collects events from other microservices. It provides an endpoint for getting logs.
  4. WebUI – The user interface of the application, must be implemented as a Single Page Application.
  5. Interfaces – Microservice interfaces and model classes.
  6. Middleware – common functionality for all microservices

MicroCommerce application development

.NET Core: Microservices Interaction via Web api
Create an empty .Net Core solution. Add a WebUI project to it as an empty ASP.NET Core WebApplication. Next, add the ProductCatalog, ShoppingCart, ActivityLog microservice projects, also as empty ASP.NET Core WebApplication projects. Finally, we add two class libraries, Interfaces and Middleware.

1. Interfaces – microservice interfaces and class-models

Connect the Shed.CoreKit.WebApi.Abstractions package to the Nuget project.
Adding IProductCatalog interface and models for it :

//// Interfaces/IProductCatalog.cs//using MicroCommerce.Models;using Shed.CoreKit.WebApi;using System;using System.Collections.Generic;namespace MicroCommerce{public interface IProductCatalog{IEnumerable<Product> Get();[Route("get/{productId}")]public Product Get(Guid productId);}}

//// Interfaces/Models/Product.cs//using System;namespace MicroCommerce.Models{public class Product{public Guid Id { get; set; }public string Name { get; set; }public Product Clone(){return new Product{Id = Id, Name = Name};}}}

Using the Route attribute is no different than the same attribute in ASP.NET Core MVC, but you must remember that this attribute must be from namespace Shed.CoreKit.WebApi, and no other. The same goes for the HttpGet, HttpPut, HttpPost, HttpPatch, HttpDelete, and FromBody attributes if used.
The rules of Http[Methodname] type attributes are the same as in MVC, i.e. if interface method name prefix matches with required Http method name, then you don’t need to define it additionally, otherwise you use corresponding attribute.
The FromBody attribute is applied to a method parameter if that parameter is to be retrieved from the request body. Note that like ASP.NET Core MVC, it must always be specified, there are no default rules. And there can only be one parameter with this attribute in the method parameters.
Adding IShoppingCart interface and models for it

//// Interfaces/IShoppingCart.cs//using MicroCommerce.Models;using Shed.CoreKit.WebApi;using System;using System.Collections.Generic;namespace MicroCommerce{public interface IShoppingCart{Cart Get();[HttpPut, Route("addorder/{productId}/{qty}")]Cart AddOrder(Guid productId, int qty);Cart DeleteOrder(Guid orderId);[Route("getevents/{timestamp}")]IEnumerable<CartEvent> GetCartEvents(long timestamp);}}

//// Interfaces/IProductCatalog/Order.cs//using System;namespace MicroCommerce.Models{public class Order{public Guid Id { get; set; }public Product Product { get; set; }public int Quantity { get; set; }public Order Clone(){return new Order{Id = Id, Product = Product.Clone(), Quantity = Quantity};}}}

//// Interfaces/Models/Cart.cs//using System;namespace MicroCommerce.Models{public class Cart{public IEnumerable<Order> Orders { get; set; }}}

//// Interfaces/Models/CartEvent.cs//using System;namespace MicroCommerce.Models{public class CartEvent: EventBase{public CartEventTypeEnum Type { get; set; }public Order Order { get; set; }}}

//// Interfaces/Models/CartEventTypeEnum.cs//using System;namespace MicroCommerce.Models{public enum CartEventTypeEnum{OrderAdded, OrderChanged, OrderRemoved}}

//// Interfaces/Models/EventBase.cs//using System;namespace MicroCommerce.Models{public abstract class EventBase{private static long TimestampBase;static EventBase(){TimestampBase = new DateTime(2000, 1, 1).Ticks;}public long Timestamp { get; set; }public DateTime Time { get; set; }public EventBase(){Time = DateTime.Now;Timestamp = Time.Ticks - TimestampBase;}}}

A couple of words about the basic type of events EventBase. When publishing events we use the approach described in the book, i.e. any event contains a timestamp, when polling the source of the event the listener passes the last received timestamp. Unfortunately, long type doesn’t convert correctly to Number type in javascript for large values, so we use some trick – we subtract the timestamp of the base date (Timestamp = Time.Ticks – TimestampBase). The specific value of the base date is completely unimportant.
Adding IActivityLogger interface and models for it

//// Interfaces/IActivityLogger.cs//using MicroCommerce.Models;using System.Collections.Generic;namespace MicroCommerce{public interface IActivityLogger{IEnumerable<LogEvent> Get(long timestamp);}}

//// Interfaces/Models/LogEvent.cs//namespace MicroCommerce.Models{public class LogEvent: EventBase{public string Description { get; set; }}}

2. ProductCatalog microservice

Open Properties/launchSettings.json, bind the project to port 5001.

{"iisSettings": {"windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": {"applicationUrl": "http://localhost:60670", "sslPort": 0}}, "profiles": {"MicroCommerce.ProductCatalog": {"commandName": "Project", "environmentVariables": {"ASPNETCORE_ENVIRONMENT": "Development"}, "applicationUrl": "http://localhost:5001"}}}

Connect the Shed.CoreKit.WebApi package to the Nuget project and link to the Interfaces and Middleware projects. The Middleware will be explained in more detail below.
Adding the implementation of the interface IProductCatalog:

//// ProductCatalog/ProductCatalog.cs//using MicroCommerce.Models;using System;using System.Collections.Generic;using System.Linq;namespace MicroCommerce.ProductCatalog{public class ProductCatalogImpl : IProductCatalog{private Product[] _products = new[]{new Product{ Id = new Guid("6BF3A1CE-1239-4528-8924-A56FF6527595"), Name = "T-shirt" }, new Product{ Id = new Guid("6BF3A1CE-1239-4528-8924-A56FF6527596"), Name = "Hoodie" }, new Product{ Id = new Guid("6BF3A1CE-1239-4528-8924-A56FF6527597"), Name = "Trousers" }};public IEnumerable<Product> Get(){return _products;}public Product Get(Guid productId){return _products.FirstOrDefault(p => p.Id == productId);}}}

We store the product catalog in a static field, to simplify the example. Of course, in a real application, you need to use some other storage, which you can get as a dependency through Dependency Injection.
Now this implementation needs to be connected as an endpoint. If we were using the traditional approach, we would have to use the MVC infrastructure, that is, create a controller, pass our implementation to it as a dependency, set up routing, etc. With the Nuget package Shed.CoreKit.WebApi, this is much easier. Just register our implementation with Dependency Injection (services.AddTransient<IProductCatalog, ProductCatalogImpl> ()), then declare it as an endpoint (app.UseWebApiEndpoint()) using the UseWebApiEndpoint extension method from the Shed.CoreKit.WebApi package. This is done in Setup

//// ProductCatalog/Setup.cs//using MicroCommerce.Middleware;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using Shed.CoreKit.WebApi;namespace MicroCommerce.ProductCatalog{public class Startup{public void ConfigureServices(IServiceCollection services){services.AddCorrelationToken();services.AddCors();// registering the implementation as a dependency in the iocservices container.AddTransient<IProductCatalog, ProductCatalogImpl>();services.AddLogging(builder => builder.AddConsole());services.AddRequestLogging();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseCorrelationToken();app.UseRequestLogging();app.UseCors(builder =>{builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();});// Bind the implementation to an endpointapp.UseWebApiEndpoint<IProductCatalog> ();}}}

This leads to methods in the microservice :

http://localhost:5001/gethttp://localhost:5001/get/<productid>

Method UseWebApiEndpoint can take the optional parameter root
If we connect the endpoint in this way :

 

app.UseWebApiEndpoint(“products”)
then the endpoint of the microservice will look like this :
http://localhost:5001/products/get

This can be useful if we need to connect several interfaces to the microservice.
That’s all you need to do. You can run the microservice and test its methods.
The rest of the code in Setup configures and connects additional features.
The services.AddCors() / app.UseCors(…) pair enables cross-domain queries in the project. This is necessary when redirecting requests from the UI side.
The services.AddCorrelationToken() / app.UseCorrelationToken() pair enables the use of correlation tokens when logging queries, as described in Christian Horsdal’s book. We’ll discuss this further later.
Finally, the services.AddRequestLogging() / app.UseRequestLogging() pair connects the request logging from the Middleware project. We’ll come back to that later too.

3. ShoppingCart microservice

Bind the project to port 5002 in the same way as ProductCatalog.
Connect the Shed.CoreKit.WebApi package to the Nuget project and link to the Interfaces and Middleware projects.
We add an implementation of the IShoppingCart interface.

//// ShoppingCart/ShoppingCart.cs//using MicroCommerce.Models;using System;using System.Collections.Generic;using System.Linq;namespace MicroCommerce.ShoppingCart{public class ShoppingCartImpl : IShoppingCart{private static List<Order> _orders = new List<Order> ();private static List<CartEvent> _events = new List<CartEvent> ();private IProductCatalog _catalog;public ShoppingCartImpl(IProductCatalog catalog){_catalog = catalog;}public Cart AddOrder(Guid productId, int qty){var order = _orders.FirstOrDefault(i => i.Product.Id == productId);if(order != null){order.Quantity += qty;CreateEvent(CartEventTypeEnum.OrderChanged, order);}else{var product = _catalog.Get(productId);if (product != null){order = new Order{Id = Guid.NewGuid(), Product = product, Quantity = qty};_orders.Add(order);CreateEvent(CartEventTypeEnum.OrderAdded, order);}}return Get();}public Cart DeleteOrder(Guid orderId){var order = _orders.FirstOrDefault(i => i.Id == orderId);if(order != null){_orders.Remove(order);CreateEvent(CartEventTypeEnum.OrderRemoved, order);}return Get();}public Cart Get(){return new Cart{Orders = _orders};}public IEnumerable<CartEvent> GetCartEvents(long timestamp){return _events.Where(e => e.Timestamp > timestamp);}private void CreateEvent(CartEventTypeEnum type, Order order){_events.Add(new CartEvent{Timestamp = DateTime.Now.Ticks, Time = DateTime.Now, Order = order.Clone(), Type = type});}}}

Here, as in ProductCatalog, we use static fields as repositories. But this microservice still uses calls to ProductCatalog to get product information, so we pass reference to IProductCatalog to the constructor as a dependency.
Now this dependency needs to be defined in the DI, and we use the AddWebApiEndpoints extension method from the Shed.CoreKit.WebApi package to do so. This method registers the WebApi-client generator factory for the IProductCatalog interface to the DI.
The factory uses the System.Net.Http.HttpClient dependency when generating the WebApi-Client. If the application requires any special settings for the HttpClient (credentials, special headers/tokens), this can be done when registering the HttpClient in the DI.

//// ShoppingCart/Settings.cs//using MicroCommerce.Middleware;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using Shed.CoreKit.WebApi;using System.Net.Http;namespace MicroCommerce.ShoppingCart{public class Startup{public void ConfigureServices(IServiceCollection services){services.AddCorrelationToken();services.AddCors();services.AddTransient<IShoppingCart, ShoppingCartImpl> ();services.AddTransient<HttpClient> ();services.AddWebApiEndpoints(new WebApiEndpoint<IProductCatalog> (new System.Uri("http://localhost:5001")));services.AddLogging(builder => builder.AddConsole());services.AddRequestLogging();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseCorrelationToken();app.UseRequestLogging("getevents");app.UseCors(builder =>{builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();});app.UseWebApiEndpoint<IShoppingCart> ();}}}

The AddWebApiEndpoints method can take any number of parameters, so it is possible to configure all dependencies with a single call to this method.
All other settings are similar to ProductCatalog.

4. ActivityLogger microservice

Bind the project to port 5003 in the same way as ProductCatalog.
Connect the Shed.CoreKit.WebApi package to the Nuget project and link to the Interfaces and Middleware projects.
Add implementation of IActivityLogger interface.

//// ActivityLogger/ActivityLogger.cs//using MicroCommerce;using MicroCommerce.Models;using System.Collections.Generic;using System.Linq;namespace ActivityLogger{public class ActivityLoggerImpl : IActivityLogger{private IShoppingCart _shoppingCart;private static long timestamp;private static List<LogEvent> _log = new List<LogEvent> ();public ActivityLoggerImpl(IShoppingCart shoppingCart){_shoppingCart = shoppingCart;}public IEnumerable<LogEvent> Get(long timestamp){return _log.Where(i => i.Timestamp > timestamp);}public void ReceiveEvents(){var cartEvents = _shoppingCart.GetCartEvents(timestamp);if(cartEvents.Count() > 0){timestamp = cartEvents.Max(c => c.Timestamp);_log.AddRange(cartEvents.Select(e => new LogEvent{Description = $"{GetEventDesc(e.Type)}: '{e.Order.Product.Name} ({e.Order.Quantity})'"}));}}private string GetEventDesc(CartEventTypeEnum type){switch (type){case CartEventTypeEnum.OrderAdded: return "order added";case CartEventTypeEnum.OrderChanged: return "order changed";case CartEventTypeEnum.OrderRemoved: return "order removed";default: return "unknown operation";}}}}

Here we also use dependence on another microservice (IShoppingCart). But one of the tasks of this service is to listen to the events of other services, so we add an additional method ReceiveEvents(), which we will call from the scheduler. We will add it to the project additionally.

//// ActivityLogger/Scheduler.cs//using Microsoft.Extensions.Hosting;using System;using System.Threading;using System.Threading.Tasks;namespace ActivityLogger{public class Scheduler : BackgroundService{private IServiceProvider ServiceProvider;public Scheduler(IServiceProvider serviceProvider){ServiceProvider = serviceProvider;}protected override Task ExecuteAsync(CancellationToken stoppingToken){Timer timer = new Timer(new TimerCallback(PollEvents), stoppingToken, 2000, 2000);return Task.CompletedTask;}private void PollEvents(object state){try{var logger = ServiceProvider.GetService(typeof(MicroCommerce.IActivityLogger)) as ActivityLoggerImpl;logger.ReceiveEvents();}catch{}}}}

The project settings are similar to the previous item.
Additionally, you only need to connect the scheduler you added earlier.

//// ActivityLogger/Setup.cs//using System.Net.Http;using MicroCommerce;using MicroCommerce.Middleware;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using Shed.CoreKit.WebApi;namespace ActivityLogger{public class Startup{public void ConfigureServices(IServiceCollection services){services.AddCorrelationToken();services.AddCors();services.AddTransient<IActivityLogger, ActivityLoggerImpl> ();services.AddTransient<HttpClient> ();services.AddWebApiEndpoints(new WebApiEndpoint<IShoppingCart> (new System.Uri("http://localhost:5002")));// register the scheduler (it will start at the start of the application, you don't need to do anything else)services.AddHostedService<Scheduler> ();services.AddLogging(builder => builder.AddConsole());services.AddRequestLogging();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseCorrelationToken();app.UseRequestLogging("get");app.UseCors(builder =>{builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();});app.UseWebApiEndpoint<IActivityLogger> ();}}}

5. WebUI – user interface

Bind the project to port 5000 in the same way as ProductCatalog.
Connect the Shed.CoreKit.WebApi package to the Nuget project. Links to the Interfaces and Middleware projects need to be connected only if we are going to use calls to microservices in this project.
Strictly speaking, this is a normal ASP.NET project and we can use MVC in it, i.e. we can create controllers which use our microservices interfaces as dependencies to interact with UI. But it is more interesting and practical to leave only UI provisioning to this project and redirect all UI calls directly to microservices. The UseWebApiRedirect extension method from the Shed.CoreKit.WebApi:

//// WebUI/Setup.cs//using MicroCommerce.Interfaces;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting;using Shed.CoreKit.WebApi;using System.Net.Http;namespace MicroCommerce.Web{public class Startup{public void ConfigureServices(IServiceCollection services){}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.Use(async (context, next) =>{// when root calls, the start page will be returnedif(string.IsNullOrEmpty(context.Request.Path.Value.Trim('/'))){context.Request.Path = "/index.html";}await next();});app.UseStaticFiles(); // redirects to microservices app.UseWebApiRedirect("api/products" new WebApiEndpoint<IProductCatalog> (new System.Uri("http://localhost:5001"))); app.UseWebApiRedirect("api/orders" new WebApiEndpoint<IShoppingCart> (new System.Uri("http://localhost:5002"))); app.UseWebApiRedirect("api/logs" new WebApiEndpoint<IActivityLogger> (new System.Uri("http://localhost:5003"))); } } }

It’s all very simple. Now if a request to ‘http://localhost:5000/api/products/get’, for example, comes from the UI side, it will automatically be redirected to ‘http://localhost:5001/get’. Of course, microservices have to allow cross-domain requests to do that, but we allowed that earlier (see CORS in microservices implementation).
Now all that’s left is to develop the user interface, and the best way to do that is the Single Page Application. You can use Angular or React, but we will just create a small page using an out-of-the-box bootstrap theme and knockoutjs framework.

<!DOCTYPE html> <!-- WebUI/wwwroot/index.html --><html><head><meta charset="utf-8" /><title> </title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/4.5.0/materia/bootstrap.min.css" /> "<style type="text/css">body {background-color: #0094ff;}.panel {background-color: #FFFFFF;margin-top:20px;padding:10px;border-radius: 4px;}.table .desc {vertical-align: middle;font-weight:bold;}.table .actions {text-align:right;white-space:nowrap;width:40px;}</style><script src="https://code.jquery.com/jquery-3.5.1.min.js"integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="crossorigin="anonymous"> </script><script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"> </script><script src="../index.js"> </script></head><body><div class="container"><div class="row"><div class="col-12"><div class="panel panel-heading"><div class="panel-heading"><h1> MicroCommerce</h1></div></div></div><div class="col-xs-12 col-md-6"><div class="panel panel-default"><h2> All products</h2><table class="table table-bordered" data-bind="foreach:products"><tr><td data-bind="text:name"> </td><td class="actions"><a class="btn btn-primary" data-bind="click:function(){$parent.addorder(id, 1);}"> ADD</a></td></tr></table></div></div><div class="col-xs-12 col-md-6"><div class="panel panel-default" data-bind="visible:shoppingCart()"><h2> Shopping cart</h2><table class="table table-bordered" data-bind="foreach:shoppingCart().orders"><tr><td data-bind="text:product.name"> </td><td class="actions" data-bind="text:quantity"> </td><td class="actions"><a class="btn btn-primary" data-bind="click:function(){$parent.delorder(id);}"> DELETE</a></td></tr></table></div></div><div class="col-12"><div class="panel panel-default"><h2> Operations history</h2><!-- ko foreach:logs --><div class="log-item"><span data-bind="text:time"> </span><span data-bind="text:description"> </span></div><!-- /ko --></div></div></div></div><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"> </script><script>var model = new IndexModel();ko.applyBindings(model);</script></body></html>

//// WebUI/wwwroot/index.js//function request(url, method, data) {return $.ajax({cache: false, dataType: 'json', url: url, data: data ? JSON.stringify(data) : null, method: method, contentType: 'application/json'});}function IndexModel() {this.products = ko.observableArray([]);this.shoppingCart = ko.observableArray(null);this.logs = ko.observableArray([]);var _this = this;this.getproducts = function () {request('/api/products/get', 'GET').done(function (products) {_this.products(products);console.log("get products: ", products);}).fail(function (err) {console.log("get products error: ", err);});};this.getcart = function () {request('/api/orders/get', 'GET').done(function (cart) {_this.shoppingCart(cart);console.log("get cart: ", cart);}).fail(function (err) {console.log("get cart error: ", err);});};this.addorder = function (id, qty) {request(`/api/orders/addorder/${id}/${qty}`, 'PUT').done(function (cart) {_this.shoppingCart(cart);console.log("add order: ", cart);}).fail(function (err) {console.log("add order error: ", err);});};this.delorder = function (id) {request(`/api/orders/deleteorder?orderId=${id}`, 'DELETE').done(function (cart) {_this.shoppingCart(cart);console.log("del order: ", cart);}).fail(function (err) {console.log("del order error: ", err);});};this.timestamp = Number(0);this.updateLogsInProgress = false;this.updatelogs = function () {if (_this.updateLogsInProgress)return;_this.updateLogsInProgress = true;request(`/api/logs/get?timestamp=${_this.timestamp}`, 'GET').done(function (logs) {if (!logs.length) {return;}ko.utils.arrayForEach(logs, function (item) {_this.logs.push(item);_this.timestamp = Math.max(_this.timestamp, Number(item.timestamp));});console.log("update logs: ", logs, _this.timestamp);}).fail(function (err) {console.log("update logs error: ", err);}).always(function () { _this.updateLogsInProgress = false; });};this.getproducts();this.getcart();this.updatelogs();setInterval(() => _this.updatelogs(), 1000);}

I won’t explain the UI implementation in detail, because it’s outside the scope of this article, I’ll just say that the javascript model defines properties and collections to bind to the HTML markup, as well as functions that respond to button presses to call WebApi endpoints, which redirect to the appropriate microservices invisibly to the developer. We’ll look at how the UI looks like and how it works later in Testing the Application.

6. A few words about general functionality

We haven’t covered some other aspects of application development in this article, such as logging, health monitoring, authentication and authorization. These are all covered in detail in Christian Horsdal’s book and are quite applicable to the approach described above. However, these aspects are too specific for each application and it does not make sense to put them into a Nuget package, it is better to create a separate assembly within the application. We have created such an assembly – it is Middleware. As an example, let’s just add here the functionality for logging queries which we already plugged in when developing microservices (see steps 2-4).

//// Middleware/RequestLoggingExt.cs//using Microsoft.AspNetCore.Builder;using Microsoft.Extensions.DependencyInjection;namespace MicroCommerce.Middleware{public static class RequestLoggingExt{private static RequestLoggingOptions Options = new RequestLoggingOptions();public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder, params string[] exclude){Options.Exclude = exclude;return builder.UseMiddleware<RequestLoggingMiddleware> ();}public static IServiceCollection AddRequestLogging(this IServiceCollection services){return services.AddSingleton(Options);}}internal class RequestLoggingMiddleware{private readonly RequestDelegate _next;private readonly ILogger _logger;private RequestLoggingOptions _options;public RequestLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, RequestLoggingOptions options){_next = next;_options = options;_logger = loggerFactory.CreateLogger("LoggingMiddleware");}public async Task InvokeAsync(HttpContext context){if(_options.Exclude.Any(i => context.Request.Path.Value.Trim().ToLower().Contains(i))){await _next.Invoke(context);return;}var request = context.Request;_logger.LogInformation($"Incoming request: {request.Method}, {request.Path}, [{HeadersToString(request.Headers)}]");await _next.Invoke(context);var response = context.Response;_logger.LogInformation($"Outgoing response: {response.StatusCode}, [{HeadersToString(response.Headers)}]");}private string HeadersToString(IHeaderDictionary headers){var list = new List<string> ();foreach(var key in headers.Keys){list.Add($"'{key}':[{string.Join(';', headers[key])}]");}return string.Join(", ", list);}}internal class RequestLoggingOptions{public string[] Exclude = new string[] { };}}

The AddRequestLogging() / UseRequestLogging(…) method pair allows to enable logging of requests in a microservice. The UseRequestLogging method can also accept an arbitrary number of exclusion paths. We used it in ShoppingCart and ActivityLogger to exclude event polling from logging and avoid log overflow. But again, logging, as well as any other general functionality, is the sole responsibility of developers and is implemented within a specific project.

Application testing

Launching the solution, we see a list of products to add to cart on the left, an empty cart on the right, and a history of transactions at the bottom, also empty for now.
.NET Core: Microservices Interaction via Web api
In the microservices consoles, we see that the UI has already requested and received some data at startup. For example, a request was sent to get a list of products localhost :5000/api/products/get, which was redirected to localhost :5001/get.
.NET Core: Microservices Interaction via Web api
.NET Core: Microservices Interaction via Web api
When we click ADD, the corresponding product is added to the cart. If a product has already been added before, the quantity is simply increased.
.NET Core: Microservices Interaction via Web api
A request is sent to the ShoppingCart microservice localhost :5002/addorder/ .NET Core: Microservices Interaction via Web api
But since ShoppingCart doesn’t store the list of products, it gets the information about the ordered product from ProductCatalog.
.NET Core: Microservices Interaction via Web api
Note, a correlation token was assigned before sending the query to ProductCatalog. This allows you to trace the chain of related requests in case of failures.
When a transaction is completed, ShoppingCart publishes an event that is tracked and logged by ActivityLogger. In turn, the UI periodically polls this microservice and displays the received data in the history panel. Of course, the entries in the history appear with some delay, since this is a parallel mechanism, independent of the operation of adding a product.

Conclusion

Nuget package Shed.CoreKit.WebApi allows :

  • focus entirely on developing the application’s business logic, without putting extra effort into microservice interaction issues;
  • describe the structure of a microservice with a .NET interface and use it both when developing the microservice itself and for generating the Web client (the Web client for a microservice is generated by a factory method after the interface is registered in the DI and provided as a dependency);
  • register microservice interfaces as dependencies in Dependency Injection;
  • Organize redirection of requests from the Web UI side to microservices without additional effort in UI development.

Microcommerce.zip

You may also like