.NET Core:通过Web API进行微服务交互

介绍 (Introduction)

Almost everyone who has worked with microservices in the .NET Core probably knows the book of Christian Horsdal, “Microservices in .NET Core: with examples in Nancy” The ways of building an application based on microservices are well described here, monitoring, logging, and access control are discussed in detail. The only thing that is missing is a tool for automating the interaction between microservices.

几乎所有在.NET Core中使用微服务的人都可能知道Christian Horsdal的书“ .NET Core中的微服务:Nancy中的示例”。在这里很好地描述了基于微服务构建应用程序的方法,监视,记录,和访问控制进行了详细讨论。 唯一缺少的是使微服务之间的交互自动化的工具。

In the usual approach, when developing a microservice, a web client for it is being developed in parallel. And every time the web interface of the microservice changes, additional efforts have to be expended for the corresponding changes in the web client. The idea of ​​generating a pair of web-api / web-client using OpenNET is also quite laborious, I would like something more transparent for the developer.

在通常的方法中,开发微服务时,将同时开发用于该微服务的Web客户端。 而且每次微服务的Web界面更改时,都必须为Web客户端中的相应更改花费更多的精力。 使用OpenNET生成一对web-api / web-client的想法也很费力,我希望对开发人员来说更透明。

So, with an alternative approach to the development of our application, I would like:

因此,采用一种替代方法来开发应用程序,我想:

  • The microservice structure is described by the .NET interface using attributes that describe the type of method, route and way of passing parameters, as is done in MVC.

    .NET接口使用属性来描述微服务结构,这些属性描述了方法的类型,路由和传递参数的方式,就像在MVC中所做的那样。
  • Microservice functionality is developed exclusively in the .NET class, implementing this interface. The publication of microservice endpoints should be automatic, not requiring complex settings.

    微服务功能是在.NET类中专门开发的,实现了此接口。 微服务端点的发布应该是自动的,不需要复杂的设置。
  • The web client for the microservice should be generated automatically based on the interface and provided through IoC container.

    微服务的Web客户端应基于该界面自动生成,并通过IoC容器提供。
  • There should be a mechanism for organizing redirects to the endpoints of microservices from the main application interacting with the user interface.

    应该有一种机制,用于组织从与用户界面交互的主应用程序到微服务端点的重定向。

In accordance with these criteria, the Nuget Shed.CoreKit.WebApi package was developed. In addition to this, the auxiliary package Shed.CoreKit.WebApi.Abstractions was created, containing attributes and classes that can be used to develop common assembly projects where the functionality of the main package is not required.

根据这些条件,开发了Nuget Shed.CoreKit.WebApi软件包。 除此之外,还创建了辅助软件包Shed.CoreKit.WebApi.Abstractions ,其中包含可用于开发不需要主软件包功能的通用装配项目的属性和类。

Below, we look at the use of the features of these packages in the development of the MicroCommerce application described in the aforementioned book of Christian Horsdal.

下面,我们将在前面提到的克里斯蒂安·霍斯达尔(Christian Horsdal)所描述的MicroCommerce应用程序的开发中使用这些包的功能。

命名协议 (Naming Agreement)

Hereinafter, we use the following terminology:

在下文中,我们使用以下术语:

  • The microservice is an ASP.NET Core application (project) that can be run console-based, by Internet Information Services (IIS) or in a Docker container.

    微服务是一个ASP.NET Core应用程序(项目),可以通过Internet信息服务(IIS)或在Docker容器中基于控制台运行。

  • The interface is a .NET entity, a set of methods and properties without implementation.

    该接口是一个.NET实体,是一组没有实现的方法和属性。

  • The endpoint is the path to the root of the microservice application or interface implementation. Examples: http://localhost:5001, http://localhost:5000/products

    端点是到微服务应用程序或接口实现的根的路径。 示例: http:// localhost:5001,http:// localhost:5000 / products

  • The route is the path to the interface method from the endpoint. It can be defined by default in the same way as in MVC or set using the attribute.

    路由是从端点到接口方法的路径。 默认情况下,可以使用与MVC中相同的方式定义它,也可以使用属性进行设置。

微商应用程序结构 (MicroCommerce Application Structure)

  • ProductCatalog is a microservice that provides product information.

    ProductCatalog是提供产品信息的微服务。

  • ShoppingCart is a microservice that provides information about the user's purchases, as well as the ability to add/remove purchases. When the state of the user's basket changes, events are generated to notify other microservices.

    ShoppingCart是一种微服务,提供有关用户购买的信息以及添加/删除购买的功能。 当用户的购物篮状态更改时,将生成事件以通知其他微服务。

  • ActivityLogger is a microservice that collects information about events of other microservices. Provides an endpoint for receiving logs.

    ActivityLogger是一个微服务,可收集有关其他微服务的事件的信息。 提供用于接收日志的端点。

  • WebUI is a user interface of the application that should be implemented as a Single Page Application.

    WebUI是应作为单页应用程序实现的应用程序的用户界面。

  • Interfaces - microservice interfaces and model classes

    Interfaces -微服务接口和模型类

  • Middleware - common functionality for all microservices

    Middleware -所有微服务的通用功能

电子商务应用开发 (MicroCommerce Application Development)

Image 1
Create an empty .NET Core solution. Add the WebUI project as an empty ASP.NET Core WebApplication. Next, add ProductCatalog, ShoppingCart, ActivityLog microservice projects, as well as empty ASP.NET Core WebApplication projects. Finally, we add two class libraries - Interfaces and Middleware.

创建一个空的.NET Core解决方案。 将WebUI项目添加为空的ASP.NET Core WebApplication。 接下来,添加ProductCatalogShoppingCartActivityLog微服务项目以及空的ASP.NET Core WebApplication项目。 最后,我们添加两个类库-接口和中间件。

1.接口项目。 微服务接口和模型类 (1. Interfaces Project. Microservice Interfaces and Model Classes)

Install the Shed.CoreKit.WebApi.Abstractions nuget package.

安装Shed.CoreKit.WebApi.Abstractions nuget软件包。

Сreate the IProductCatalog interface and models for it:

创建IProductCatalog接口和模型:

//
// 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 from that in ASP.NET Core MVC, but remember that this attribute must be from namespace Shed.CoreKit.WebApi, and no other. The same applies to the HttpGet, HttpPut, HttpPost, HttpPatch, HttpDelete, and FromBody attributes if used.

使用Route属性与ASP.NET Core MVC中的属性没有什么不同,但请记住,此属性必须来自命名空间Shed.CoreKit.WebApi ,并且不能来自其他名称。 如果使用HttpGetHttpPutHttpPostHttpPatchHttpDeleteFromBody属性,则同样适用。

The rules for using attributes of the Http [Methodname] type are the same as in MVC, that is, if the prefix of the interface method name matches the name of the required Http method, then you do not need to additionally define it, otherwise we will use the corresponding attribute.

使用Http [Methodname]类型的属性的规则与MVC中的规则相同,即,如果接口方法名称的前缀与所需Http方法的名称匹配,则无需额外定义它,否则我们将使用相应的属性。

The FromBody attribute is applied to a method parameter if this parameter is to be retrieved from the request body. I note that like ASP.NET Core MVC, it must always be specified, there are no default rules. And in the method parameters, there can be only one parameter with this attribute.

如果要从请求正文中检索此参数,则FromBody属性将应用于方法参数。 我注意到,像ASP.NET Core MVC一样, 必须始终指定它,没有默认规则 。 在方法参数中,只能有一个具有此属性的参数。

Сreate the IShoppingCart interface and models for it:

创建IShoppingCart 界面和模型:

//
// 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 few 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 sends the last received timestamp. Unfortunately, the long type is incorrectly converted to the Number JavaScript type 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 absolutely unimportant.

关于事件基本类型EventBase几句话。 发布事件时,我们使用本书中描述的方法,即任何事件都包含Timestamp ,当轮询事件的源时,侦听器将发送最后收到的时间戳。 不幸的是,长类型错误地转换为数值较大的Number JavaScript类型,因此我们使用了一些技巧,我们减去了基准日期的Timestamp = Time.Ticks - TimestampBase ( Timestamp = Time.Ticks - TimestampBase )。 基本日期的具体值绝对不重要。

Сreate the IActivityLogger interface and models for it:

创建IActivityLogger接口并为其建模:

//
// 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项目 (2. ProductCatalog Project)

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

打开Properties / launchSettings.json ,将项目绑定到端口5001。

//
// Properties/launchSettings.json
//

{
  "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"
    }
  }
}

Install Shed.CoreKit.WebApi nuget package to the project and add links to Interfaces and Middleware projects. Middleware project will be described in more detail below.

Shed.CoreKit.WebApi nuget软件包安装到项目中,并添加到InterfacesMiddleware链接 项目。 Middleware项目将在下面更详细地描述。

Create the IProductCatalog interface implementation:

创建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);
        }
    }
}

The product catalog is stored in a static field, to simplify the example. Of course, in a real application, you need to use some other storage, which can be provided as a dependency through Dependency Injection.

为了简化示例, product目录存储在静态字段中。 当然,在实际的应用程序中,您需要使用其他一些存储,可以通过依赖注入将其作为依赖提供。

Now this implementation needs to be connected as an endpoint. If we used the traditional approach, we would have to use the MVC infrastructure, that is, create a controller, pass our implementation to it as a dependency, configure routing, etc. Using the Shed.CoreKit.WebApi Nuget package makes this much easier. It is enough to register our implementation in Dependency Injection, then declare it as the endpoint using the UseWebApiEndpoint extender method from the Shed.CoreKit.WebApi package. We do it in the Setup:

现在,此实现需要连接为端点。 如果使用传统方法,则必须使用MVC基础结构,即创建一个控制器,将我们的实现作为依赖项传递给它,配置路由等。使用Shed.CoreKit.WebApi Nuget包会使此过程变得更加容易。 只需在Dependency Injection中注册我们的实现,然后使用Shed.CoreKit.WebApi包中的UseWebApiEndpoint扩展器方法将其声明为端点Shed.CoreKit.WebApi 。 我们在安装程序中执行此操作:

//
// 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();
            // register the implementation as dependency
            services.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 registered implementation to the endpoint
            app.UseWebApiEndpoint<IProductCatalog>();
        }
    }
}

This leads to the fact that methods appear in the microservice: http://localhost: 5001/get http://localhost: 5001/get/<productid>

这导致以下事实:方法出现在微服务中: http:// localhost:5001 / get http:// localhost:5001 / get / <productid>

The UseWebApiEndpoint method may accept an optional root parameter. If we connect the endpoint this way: app.UseWebApiEndpoint<IProductCatalog>(“products”), then the microservice endpoint will look like this: http://localhost:5001/products/get This can be useful if we need to connect several interfaces to the microservice.

UseWebApiEndpoint方法可以接受可选的根参数。 如果我们以这种方式连接端点: app.UseWebApiEndpoint<IProductCatalog>(“products”) ,那么微服务端点将看起来像这样: http:// localhost:5001 / products / get如果需要连接多个端点,这将非常有用与微服务的接口。

This is all you need to do. You can start the microservice and test its methods.

这就是您需要做的。 您可以启动微服务并测试其方法。

The rest of the code in Setup configures and enables additional features. A pair of services.AddCors() / app.UseCors(...) allows the use of cross-domain requests in the project. This is necessary when redirecting requests from the UI. A pair of services.AddCorrelationToken() / app.UseCorrelationToken() enables the use of correlation tokens when logging requests, as described in the book of Christian Horsdal. We will discuss this later. Finally, a pair of services.AddRequestLogging() / app.UseRequestLogging() enables request logging from a Middleware project. We will return to this later too.

Setup的其余代码将配置并启用其他功能。 一对services.AddCors() / app.UseCors(...)允许在项目中使用跨域请求。 从用户界面重定向请求时,这是必需的。 一对services.AddCorrelationToken() app.UseCorrelationToken() services.AddCorrelationToken() / app.UseCorrelationToken()允许在记录请求时使用相关令牌,如Christian Horsdal的书中所述。 我们将在后面讨论。 最后,一对services.AddRequestLogging() / app.UseRequestLogging()启用来自中间件项目的请求日志记录。 我们也将在稍后返回。

3. ShoppingCart项目 (3. ShoppingCart Project)

Bind the project to port 5002 in the same way as ProductCatalog.

以与ProductCatalog相同的方式将项目绑定到端口5002。

Add to the project the Shed.CoreKit.WebApi nuget package and links to Interfaces and Middleware projects.

Shed.CoreKit.WebApi nuget包添加到项目中,并链接到InterfacesMiddleware项目。

Create the IShoppingCart interface implementation.

创建IShoppingCart接口实现。

//
// 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 storage. But this microservice still uses calls to ProductCatalog to get information about the product, so we pass the link to IProductCatalog to the constructor as a dependency.

在这里,与ProductCatalog ,我们使用static字段作为存储。 但是,此微服务仍使用对ProductCatalog调用来获取有关产品的信息,因此我们将IProductCatalog的链接作为依赖项IProductCatalog给构造函数。

Now this dependency needs to be defined in DI, and for this, we use the AddWebApiEndpoints extender method from the Shed.CoreKit.WebApi package. This method registers the WebApi client generator as factory method for the IProductCatalog interface in the IoC container. When generating a WebApi client, the factory uses the dependency System.Net.Http.HttpClient. If the application requires some special settings for HttpClient (credentials, special headers / tokens, etc.), this should be done when registering HttpClient in the IoC container.

现在,需要在DI中定义此依赖关系,为此,我们使用Shed.CoreKit.WebApi包中的AddWebApiEndpoints扩展器方法。 此方法将WebApi客户端生成器注册为IoC容器中IProductCatalog接口的工厂方法。 生成WebApi客户端时,工厂使用依赖项System.Net.Http.HttpClient 。 如果应用程序需要HttpClient某些特殊设置(凭据,特殊的标头/令牌等),则应在IoC容器中注册HttpClient时执行此操作。

//
// 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>();
            // register a dependency binded to the endpoint
            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 an arbitrary number of parameters, so it is possible to configure all dependencies with a single call to this method. Otherwise, all settings are similar to ProductCatalog.

AddWebApiEndpoints方法可以采用任意数量的参数,因此可以通过对该方法的一次调用来配置所有依赖项。 否则,所有设置都类似于ProductCatalog

4. ActivityLogger项目 (4. ActivityLogger Project)

Bind the project to port 5003 in the same way as ProductCatalog.

以与ProductCatalog相同的方式将项目绑定到端口5003。

Install the Shed.CoreKit.WebApi nuget package to the project and add links to Interfaces and Middleware projects.

Shed.CoreKit.WebApi nuget软件包安装到项目中,并添加到InterfacesMiddleware项目的链接。

Create the IActivityLogger interface implementation.

创建IActivityLogger接口实现。

//
// ActivityLogger/ActivityLogger.cs
//

using MicroCommerce;
using MicroCommerce.Models;
using System.Collections.Generic;
using System.Linq;

namespace MicroCommerce.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";
            }
        }
    }
}

It also uses a dependency 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 ReceiveEvents() method, which we will call from the scheduler. We will add it to the project additionally.

它还使用了对另一个微服务的依赖( IShoppingCart )。 但是此服务的任务之一是监听其他服务的事件,因此我们添加了一个额外的ReceiveEvents() 方法,我们将从调度程序中调用该方法。 我们将其另外添加到项目中。

//
// ActivityLogger/Scheduler.cs
//

using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace MicroCommerce.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
            {

            }
        }
    }
}

Project settings are similar to the previous paragraph. Additionally, we only need to add the previously developed scheduler.

项目设置与上一段相似。 另外,我们只需要添加以前开发的调度程序。

//
// 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 MicroCommerce.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
            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项目。 用户界面 (5. WebUI Project. The User Interface)

Bind the project to port 5000 in the same way as ProductCatalog.

以与ProductCatalog相同的方式将项目绑定到端口5000。

Install the Shed.CoreKit.WebApi nuget package to the project. Links to Interfaces and Middleware projects are needed only if we going to use calls to microservices inside this project.

Shed.CoreKit.WebApi nuget软件包安装到项目中。 仅当我们要在该项目中使用对微服务的调用时,才需要链接到InterfacesMiddleware项目。

In fact, this is a usual ASP.NET project and we can use MVC in it, i.e., to interact with the UI, we can create controllers that use our microservice interfaces as dependencies. But it’s more interesting and practical to leave only the user interface behind this project, and redirect all calls from the UI directly to microservices. For this, the UseWebApiRedirect extender method from the Shed.CoreKit.WebApi package is used.

实际上,这是一个普通的ASP.NET项目,我们可以在其中使用MVC,即与UI交互,我们可以创建将微服务接口用作依赖项的控制器。 但是,将用户界面仅留在该项目之后,将所有调用从UI直接重定向到微服务,这是更有趣和实用的。 为此,使用了Shed.CoreKit.WebApi包中的UseWebApiRedirect扩展方法。

//
// 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 returned
                if(string.IsNullOrEmpty(context.Request.Path.Value.Trim('/')))
                {
                    context.Request.Path = "/index.html";
                }

                await next();
            });
            app.UseStaticFiles();
            // add 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")));
        }
    }
}

Everything is very simple. Now, if, for example, a request to ‘http://localhost:5000/api/products/get’ comes from the UI, it will be automatically redirected to ‘http://localhost:5001/get’. Of course, for this, microservices must allow cross-domain requests, but we have allowed this earlier (see CORS in the implementation of microservices).

一切都非常简单。 现在,例如,如果UI发出了对“ http:// localhost:5000 / api / products / get ”的请求,它将自动重定向到“ http:// localhost:5001 / get ”。 当然,为此,微服务必须允许跨域请求,但是我们早些时候已允许这样做(请参阅CORS在微服务的实现中)。

Now all that remains is to develop a user interface, and Single Page Application is best suited for this. You can use Angular or React, but we just create a small page using the ready-made bootstrap theme and the knockoutjs framework.

现在剩下的就是开发用户界面了,单页应用程序最适合于此。 您可以使用Angular或React,但是我们只是使用现成的bootstrap主题和kickoutjs框架来创建一个小页面。

<!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 will not explain in detail the implementation of the UI, as this is beyond the scope of the article’s topic, I’ll just say that the JavaScript model defines properties and collections for binding from the HTML markup, as well as functions that respond to button clicks for accessing WebApi endpoints that are redirected to the corresponding microservices. What the user interface looks like and how it works, we will discuss later in the section “Testing the application”.

我将不详细解释UI的实现,因为这超出了本文主题的范围,我只想说JavaScript模型定义了用于从HTML标记进行绑定的属性和集合,以及响应单击按钮可访问重定向到相应微服务的WebApi端点。 用户界面的外观及其工作方式,我们将在后面的“测试应用程序”部分中讨论。

7.关于通用功能的几句话 (7. A Few Words About the General Functionality)

In this article, we did not touch on some other aspects of application development, such as journaling, health monitoring, authentication, and authorization. This is all considered in detail in the book of Christian Horsdahl and is quite applicable within the frame of the above approach. However, these aspects are too specific for each application and it makes no sense to put them in a Nuget package, it is better to just create a separate assembly within the application. We have created such an assembly - this is Middleware. For example, just add here the functionality for query logging, which we already linked when developing microservices (see paragraphs 2-4).

在本文中,我们没有涉及应用程序开发的其他方面,例如日志记录,运行状况监视,身份验证和授权。 在克里斯蒂安·霍斯达尔(Christian Horsdahl)的书中都对此进行了详细的考虑,并且在上述方法的框架内非常适用。 但是,这些方面对于每个应用程序来说都是太具体了,将它们放在Nuget包中没有意义,最好只在应用程序中创建一个单独的程序集。 我们已经创建了这样的程序集-这就是中间件 。 例如,仅在此处添加查询日志记录功能,我们在开发微服务时已将其链接(请参阅第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[] { };
    }
}

A pair of AddRequestLogging() / UseRequestLogging(...) methods allows us to enable query logging in the microservice. The UseRequestLogging method can also take an arbitrary number of exception paths. We used this in ShoppingCart and in ActivityLogger to exclude event polls from logging and to avoid log overflows. But again, journaling, like any other common functionality, is an exclusive responsibility of developers and is implemented within the frame of a specific project.

一对AddRequestLogging() / UseRequestLogging(...)方法使我们能够在微服务中启用查询日志记录。 UseRequestLogging方法还可以采用任意数量的异常路径。 我们在ShoppingCartActivityLogger 从记录中排除事件轮询并避免日志溢出。 但是,与其他任何常见功能一样,日记功能也是开发人员的专有责任,并且是在特定项目的框架内实现的。

应用测试 (Testing of the Application)

We launch the solution, we see on the left a list of products to add to the basket, an empty basket on the right and the history of operations below, also empty so far.

我们启动该解决方案,在左侧看到要添加到购物篮中的产品列表,在右侧看到一个空购物篮,并在下面看到操作历史记录,到目前为止也是空的。

Image 2

In the consoles of microservices, we see that at startup, the UI has already requested and received some data. For example, to get a list of products, a request was sent http://localhost:5000/api/products/get, which was redirected to http://localhost:5001/get.

在微服务的控制台中,我们看到在启动时,UI已经请求并接收了一些数据。 例如,要获取产品列表,发送了一个请求http:// localhost:5000 / api / products / get ,该请求已重定向到http:// localhost:5001 / get

Image 3

Similarly, the UI received the status of an order basket from ShoppingCart.

同样,UI从ShoppingCart接收了订单篮的状态。

Image 4

When click the ADD button, the product is added to the basket. If this product has already been added, the quantity increases.

单击添加按钮时,产品将添加到购物篮。 如果已添加此产品,则数量会增加。

Image 5

The microservice ShoppingCart is sent a request http://localhost:5002/addorder/<productid>.

向微服务ShoppingCart发送请求http:// localhost:5002 / addorder / <productid>

Image 6

But since ShoppingCart does not store the list of products, it receives information about the ordered product from ProductCatalog.

但是,由于ShoppingCart不存储产品列表,因此它从ProductCatalog接收有关订购产品的信息。

Image 7

Please note that before sending a request to ProductCatalog, a correlation token was assigned. This allows us to track the chain of related queries in case of a failure.

请注意,在向ProductCatalog发送请求之前,已分配了关联令牌。 这使我们能够在发生故障时跟踪相关查询的链。

Upon completion of the operation, ShoppingCart publishes an event that tracks and logs the ActivityLogger. In turn, the UI periodically polls this microservice and displays the received data in the operation history panel. Of course, the entries in the history appear with some delay, because it is a parallel mechanism that does not depend on the operation of adding a product.

操作完成后, ShoppingCart发布一个事件,该事件跟踪并记录ActivityLogger 。 反过来,UI会定期轮询此微服务,并在操作历史记录面板中显示接收到的数据。 当然,历史记录中的条目会延迟出现,因为它是一种并行机制,不依赖于添加产品的操作。

结论 (Conclusion)

Nuget package Shed.CoreKit.WebApi allows us to:

Nuget包Shed.CoreKit.WebApi使我们能够:

  • fully focus on developing the business logic of the application, without making additional efforts on the issues of microservices interaction;

    完全专注于开发应用程序的业务逻辑,而无需在微服务交互问题上做出额外的努力;
  • describe the structure of the microservice with the .NET interface and use it both in the development of the microservice itself and for generating the Web client (the Web client for the microservice is generated by the factory method after registering the interface in the IoC container and is provided as a dependency);

    用.NET接口描述微服务的结构,并在微服务本身的开发和用于生成Web客户端中使用它(用于微服务的Web客户端是在将接口注册到IoC容器后通过工厂方法生成的,作为依赖提供);
  • Register microservice interfaces as dependencies in the IoC container;

    在IoC容器中将微服务接口注册为依赖项;
  • organize redirection of requests from the Web UI to microservices without additional efforts in the development of UI.

    无需重新开发UI,就可以将请求从Web UI重定向到微服务。

缺点 (Drawbacks)

The idea of ​​interacting via Web api is quite attractive, but there are some problems:

通过Web api进行交互的想法非常吸引人,但是存在一些问题:

  • we must take care of providing microservice configurations on which our microservice depends;

    我们必须注意提供微服务所依赖的微服务配置;
  • if our microservice is overloaded, we would like to launch several additional instances of this microservice to reduce the load.

    如果我们的微服务超载,我们想启动该微服务的其他几个实例以减少负载。

To handle this, we need another architecture, namely star-shaped architecture, that is, interaction through a common bus.

为了解决这个问题,我们需要另一种架构,即星形架构,即通过公共总线进行交互。

How to organize interaction through a common bus like MQ service, we will consider in the next article

如何通过诸如MQ服务之类的通用总线来组织交互,我们将在下一篇文章中考虑

历史 (History)

  • 14th June, 2020: Initial version

    2020年6月14 :初始版本

翻译自: https://www.codeproject.com/Articles/5271067/NET-Core-Interaction-of-Microservices-via-Web-API

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值