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

目录

介绍

命名协议

MicroCommerce应用程序结构

MicroCommerce应用开发

1.接口项目,微服务接口和模型类

2. ProductCatalog项目

3. ShoppingCart项目

4. ActivityLogger项目

5. WebUI项目,用户界面

6.关于通用功能的几句话

应用测试

结论

缺点


介绍

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

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

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

  • .NET接口使用属性来描述微服务结构,这些属性描述了方法的类型、路由和传递参数的方式,就像在MVC中所做的那样。
  • 微服务功能是在.NET类中专门开发的,实现了此接口。微服务端点的发布应该是自动的,不需要复杂的设置。
  • 微服务的Web客户端应基于该接口自动生成,并通过IoC容器提供。
  • 应该有一种机制来组织从与用户界面交互的主应用程序到微服务端点的重定向。

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

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

命名协议

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

  • 微服务是一个ASP.NET Core应用程序(项目),可以通过IIS或在Docker容器中基于控制台运行。
  • 接口是一个.NET实体,是一组没有实现的方法和属性。
  • 端点是微服务应用程序或接口实现的根的路径。例如:http://localhost:5001, http://localhost:5000/products
  • 路由是从端点到接口方法的路径。默认情况下,可以使用与MVC中相同的方式定义它,也可以使用属性进行设置。

MicroCommerce应用程序结构

  • ProductCatalog 是提供产品信息的微服务。
  • ShoppingCart是一种可提供有关用户购买的信息以及添加/删除购买的功能的微服务。当用户的购物篮状态更改时,将生成事件以通知其他微服务。
  • ActivityLogger是一种微服务,用于收集有关其他微服务的事件的信息。提供用于接收日志的端点。
  • WebUI 是应用程序的一个用户界面,应该将其实现为单个页面应该程序。
  • Interfaces——微服务接口和模型类
  • Middleware——所有微服务的通用功能

MicroCommerce应用开发

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

1.接口项目,微服务接口和模型类

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

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

 

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

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

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

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

 

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

创建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项目

打开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"
    }
  }
}

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

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

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

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

//
// 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>();
        }
    }
}

这导致以下事实:方法出现在微服务中:

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

UseWebApiEndpoint方法可以接受可选的根参数。

如果我们通过以下方式连接端点:app.UseWebApiEndpoint<IProductCatalog>(“products”),则微服务端点将如下所示:

http://localhost:5001/products/get

如果需要将多个接口连接至微服务,这将很有用。

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

Setup中的其余代码将配置并启用其他功能。

一对services.AddCors()/app.UseCors(...)允许在项目中使用跨域请求。从用户界面重定向请求时,这是必需的。

如克里斯汀·霍斯达尔(Christian Horsdal)所述,一对services.AddCorrelationToken()/app.UseCorrelationToken()允许在记录请求时使用相关令牌。我们将在后面讨论。

最后,一对services.AddRequestLogging()/app.UseRequestLogging()启用来自中间件项目的请求日志记录。我们也将在稍后返回。

3. ShoppingCart项目

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

Shed.CoreKit.WebApi nuget软件包以及指向InterfacesMiddleware项目的链接添加到项目中。

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

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

现在,需要在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>();
        }
    }
}

AddWebApiEndpoints方法可以采用任意数量的参数,因此可以通过一次调用此方法来配置所有依赖项。

否则,所有设置与ProductCatalog相似。

4. ActivityLogger项目

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

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

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

它还使用了对另一个微服务(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
            {

            }
        }
    }
}

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

//
// 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项目,用户界面

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

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

实际上,这是一个普通的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")));
        }
    }
}

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

现在剩下的就是开发用户界面,单页应用程序最适合于此。您可以使用AngularReact,但是我们只是使用现成的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);
}

 

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

6.关于通用功能的几句话

在本文中,我们没有涉及应用程序开发的其他方面,例如日志记录、运行状况监视、身份验证和授权。在克里斯蒂安·霍斯达尔(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[] { };
    }
}

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

应用测试

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

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

同样,用户界面从ShoppingCart接收了订单篮的状态。

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

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

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

请注意,在向ProductCatalog发送请求之前,已分配了相关令牌。这使我们能够在失败的情况下跟踪相关查询的链。

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

结论

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

  • 完全专注于开发应用程序的业务逻辑,而无需在微服务交互问题上做出额外的努力;
  • 使用.NET接口描述微服务的结构,并将其用于微服务本身的开发以及用于生成Web客户端(用于微服务的Web客户端是通过在IoC容器中注册接口后通过工厂方法生成的,作为依赖提供);
  • IoC容器中将微服务接口注册为依赖项;
  • 无需重新开发UI,就可以将请求从Web UI重定向到微服务。

缺点

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

  • 我们必须注意提供微服务所依赖的微服务配置;
  • 如果我们的微服务超载,我们想启动该微服务的其他几个实例以减少负载。

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值