.NET Core: Взаимодействие микросервисов через Web api

    Введение


    Практически все, кто имел дело с микросервисами в .NET Core, наверняка знают книгу Кристиана Хорсдала “Микросервисы на платформе .NET”. Здесь прекрасно описаны подходы к построению приложения на основе микросервисов, подробно рассмотрены вопросы мониторинга, журналирования, управления доступом. Единственное, чего не хватает — это инструмента автоматизации взаимодействия между микросервисами.

    При обычном подходе при разработке микросервиса параллельно разрабатывается web-клиент для него. И каждый раз, когда меняется web-интерфейс микросервиса, приходится затрачивать дополнительные усилия для соответствующих изменений web-клиента. Идея генерировать пару web-api/web-клиент с использованием OpenApi тоже достаточно трудоемка, хотелось бы чего-то более прозрачного для разработчика.

    Итак, при альтернативном подходе в разработке нашего приложения хотелось бы:
    Структуру микросервиса описывать интерфейсом с использованием атрибутов, описывающих тип метода, маршрут и способ передачи параметров, как это делается в MVC.

    Функциональность микросервиса разрабатывается исключительно в классе,
    реализующим этот интерфейс. Публикация конечных точек микросервиса должна быть автоматической, не требующей сложных настроек.

    Web-клиент для микросервиса должен генерироваться автоматически на основе интерфейса и предоставляться через Dependency Injection.

    Должен быть механизм организации редиректов на конечные точки микросервисов из главного приложения, взаимодействующего с пользовательским интерфейсом.

    В соответствии с этим критериями разработан Nuget-пакет Shed.CoreKit.WebApi. В дополнение к нему создан вспомогательный пакет Shed.CoreKit.WebApi.Abstractions, содержащий атрибуты и классы, которые могут быть использованы при разработке общих проектов-сборок, где не требуется функциональность основного пакета.

    Ниже мы рассмотрим использование возможностей этих пакетов при разработке приложения MicroCommerce, описанного в вышеупомянутой книге Кристиана Хорсдала.

    Здесь и далее мы будем использовать следующую терминологию:

    Микросервис — приложение (проект) ASP.NET Core, которое может запускаться консольно, под Internet Information Services (IIS) или в Docker-контейнере.
    Интерфейс — сущность .NET, набор методов и свойств без реализации.
    Конечная точка — путь к корню приложения микросервиса или реализации интерфейса. Примеры: localhost:5001, localhost:5000/products
    Маршрут — путь к методу интерфейса от конечной точки. Может определяться по умолчанию так же как в MVC или устанавливаться при помощи атрибута.

    Структура приложения MicroCommerce


    1. ProductCatalog — микросервис, предоставляющий сведения о продуктах.
    2. ShoppingCart — микросервис, предоставляющий сведения о покупках пользователя, а также возможность добавлять/удалять покупки. При изменении состояния корзины пользователя генерируются события для уведомления других микросервисов.
    3. ActivityLogger — микросервис, собирающий сведения о событиях других микросервисов. Предоставляет конечную точку для получения логов.
    4. WebUI — Пользовательский интерфейс приложения, должен быть реализован в виде Single Page Application.
    5. Interfaces — интерфейсы микросервисов и классы-модели.
    6. Middleware — общая функциональность для всех микросервисов

    Разработка приложения MicroCommerce




    Создаем пустое решение .Net Core. Добавляем в него проект WebUI как пустой ASP.NET Core WebApplication. Далее добавляем проекты микросервисов ProductCatalog, ShoppingCart, ActivityLog, также как пустые проекты ASP.NET Core WebApplication. В заключение добавляем две библиотеки классов — Interfaces и Middleware.

    1. Interfaces — интерфейсы микросервисов и классы-модели


    Подключаем к проекту Nuget-пакет Shed.CoreKit.WebApi.Abstractions.

    Добавляем интерфейс 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, но нужно помнить, что этот атрибут должен быть из namespace Shed.CoreKit.WebApi, и никакого другого. То же самое касается атрибутов HttpGet, HttpPut, HttpPost, HttpPatch, HttpDelete, а также FromBody в случае их применения.

    Правила применения атрибутов типа 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, при опросе источника события слушатель передает последний полученный timestamp. К сожалению, тип long некорректно преобразуется в в тип Number javascript при больших значениях, поэтому мы используем некую хитрость — вычитаем timestamp базовой даты (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.

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

    Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi и ссылки на проекты Interfaces и Middleware. О 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);
            }
        }
    }
    

    Каталог продуктов храним в статическом поле, для упрощения примера. Конечно же, в реальном приложении нужно использовать какое-то другое хранилище, которое можно получить как зависимость через Dependency Injection.

    Теперь эту реализацию нужно подключить как конечную точку. Если бы мы использовали традиционный подход, мы должны были бы использовать инфраструктуру MVC, то есть создать контроллер, передать ему нашу реализацию как зависимость, настроить роутинг и т.д. С использованием Nuget-пакета Shed.CoreKit.WebApi это делается гораздо проще. Достаточно зарегистрировать нашу реализацию в Dependency Injection (services.AddTransient<IProductCatalog, ProductCatalogImpl>()), затем объявляем ее как конечную точку (app.UseWebApiEndpoint()) при помощи метода-расширителя UseWebApiEndpoint из пакета Shed.CoreKit.WebApi. Это делается в 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();
                // регистрируем реализацию как зависимость в контейнере IoC
                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();
                });
    
                // привязываем реализацию к конечной точке
                app.UseWebApiEndpoint<IProductCatalog>();
            }
        }
    }
    

    Это приводит к тому, что в микросервисе появляются методы:

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

    Метод UseWebApiEndpoint может принимать необязательный параметр root.

    Если мы подключим конечную точку таким образом:
    app.UseWebApiEndpoint(“products”)
    то конечная точка микросервиса будет выглядеть вот так:

    http://localhost:5001/products/get

    Это может быть полезно, если у нас появится необходимость подключить к микросервису несколько интерфейсов.

    Это все что нужно сделать. Можно запустить микросервис и протестировать его методы.

    Остальной код в Setup настраивает и подключает дополнительные возможности.

    Пара services.AddCors() / app.UseCors(...) разрешает использование кросс-доменных запросов в проекте. Это необходимо при редиректах запросов со стороны UI.

    Пара services.AddCorrelationToken() / app.UseCorrelationToken() подключает использование токенов корреляции при журналировании запросов, как это описано в книге Кристиана Хорсдала. Мы дополнительно обсудим это позже.

    И наконец, пара services.AddRequestLogging() / app.UseRequestLogging() подключает журналирование запросов из проекта Middleware. К этому тоже вернемся позже.

    3. Микросервис ShoppingCart


    Привязываем проект к порту 5002 аналогично ProductCatalog.

    Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi и ссылки на проекты Interfaces и Middleware.

    Добавляем реализацию интерфейса 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, используем статические поля как хранилища. Но этот микросервис еще использует вызовы к ProductCatalog для получения информации о продукте, поэтому ссылку на IProductCatalog передаем в конструктор как зависимость.

    Теперь эту зависимость нужно определить в DI, и мы используем для этого метод-расширитель AddWebApiEndpoints из пакета Shed.CoreKit.WebApi. Этот метод регистрирует в DI фабрику-генератор WebApi-клиентов для интерфейса IProductCatalog.

    При генерировании WebApi-клиента фабрика использует зависимость System.Net.Http.HttpClient. Если в приложении требуются какие-то специальные настройки для HttpClient (учетные данные, специальные заголовки/токены), это можно сделать при регистрации HttpClient в 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>();
            }
        }
    }
    

    Метод AddWebApiEndpoints может принимать произвольное количество параметров, поэтому возможно настроить все зависимости одним вызовом этого метода.

    В остальном все настройки аналогичны ProductCatalog.

    4. Микросервис ActivityLogger


    Привязываем проект к порту 5003 аналогично ProductCatalog.

    Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi и ссылки на проекты Interfaces и Middleware.

    Добавляем реализацию интерфейса IActivityLogger.

    //
    // 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";
                }
            }
        }
    }
    

    Здесь также используется зависимость от другого микросервиса (IShoppingCart). Но одна из задач этого сервиса - слушать события других сервисов, поэтому добавляем дополнительный метод ReceiveEvents(), который будем вызывать из планировщика. Мы его добавим к проекту дополнительно.
    //
    // 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
                {
    
                }
            }
        }
    }
    

    Настройки проекта аналогичны предыдущему пункту.

    Дополнительно нужно только подключить добавленный ранее планировщик.

    //
    // 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")));
                // регистрируем планировщик (запустится при старте приложения, больше ничего делать не нужно)
                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 - пользовательский интерфейс


    Привязываем проект к порту 5000 аналогично ProductCatalog.

    Подключаем к проекту Nuget- пакет Shed.CoreKit.WebApi. Cсылки на проекты Interfaces и Middleware нужно подключать только в том случае, если мы в этом проекте собираемся использовать вызовы к микросервисам.

    Строго говоря, это обычный ASP.NET проект и в нем возможно использование MVC, т.е. для взаимодействия с UI мы можем создать контроллеры, которые используют наши интерфейсы микросервисов как зависимости. Но интереснее и практичнее оставить за этим проектом только предоставление пользовательского интерфейса, а все обращения со стороны UI перенаправлять непосредственно микросервисам. Для этого используется метод-расширитель UseWebApiRedirect из пакета 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 returned
                    if(string.IsNullOrEmpty(context.Request.Path.Value.Trim('/')))
                    {
                        context.Request.Path = "/index.html";
                    }
    
                    await next();
                });
                app.UseStaticFiles();
                // редиректы на микросервисы
                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 в реализации микросервисов).

    Теперь осталось только разработать пользовательский интерфейс, и лучше всего для этого подходит Single Page Application. Можно использовать Angular или React, но мы просто создадим маленькую страничку с использованием готовой темы bootstrap и фреймворка knockoutjs.

    <!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-разметки, а также функции, реагирующие на нажатие кнопок для обращения к конечным точкам WebApi, которые незаметно для разработчика перенаправляются к соответствующим микросервисам. Как выглядит пользовательский интерфейс и как он работает мы рассмотрим позднее в разделе “Тестирование приложения”.

    6. Несколько слов об общей функциональности


    Мы не затронули в этой статье некоторые другие аспекты разработки приложения, такие как журналирование, мониторинг работоспособности, аутентификация и авторизация. Это все подробно рассмотрено в книге Кристиана Хорсдала и вполне применимо в рамках вышеописанного подхода. Вместе с тем эти аспекты слишком специфичны для для каждого конкретного приложения и не имеет смысла выносить их в Nuget-пакет, лучше просто создать отдельную сборку в рамках приложения. Мы такую сборку создали - это Middleware. Для примера просто добавим сюда функциональность для журналирования запросов, которую мы уже подключили при разработке микросервисов (см. пп. 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 кроме того может принимать произвольное количество путей-исключений. Мы воспользовались этим в ShoppingCart и в ActivityLogger чтобы исключить из журналирования опросы событий и избежать переполнения логов. Но повторюсь, журналирование, как и любая другай общая функциональность - это исключительно зона ответственности разработчиков и реализуется в рамках конкретного проекта.

    Тестирование приложения


    Запускаем решение, видим слева список продуктов для добавления в корзину, пустую корзину справа и историю операций снизу, тоже пока пустую.



    В консолях микросервисов мы видим, что при старте UI уже запросил и получил некоторые данные. Например, для получения списка продуктов был отправлен запрос localhost:5000/api/products/get, который был перенаправлен на localhost:5001/get.





    Когда мы нажимаем кнопку ADD, в корзину добавляется соответствующий продукт. Если продукт был уже ранее добавлен, просто увеличивается количество.



    Микросервису ShoppingCart отправляется запрос localhost:5002/addorder/

    Но поскольку ShoppingCart не хранит список продуктов, сведения о заказанном продукте он получает от ProductCatalog.



    Обратите внимание, перед отправкой запроса к ProductCatalog был назначен токен корреляции. Это позволяет отследить цепочки связанных запросов в случае сбоев.

    По завершении операции ShoppingCart публикует событие, которое отслеживает и регистрирует ActivityLogger. В свою очередь, UI периодически опрашивает этот микросервис и отображает полученные данные на панели истории операций. Конечно же, записи в истории появляются с некоторой задержкой, т.к. это параллельный, не зависящий от операции добавления продукта механизм.

    Заключение


    Nuget-пакет Shed.CoreKit.WebApi позволяет:

    • полностью сосредоточиться на разработке бизнес-логики приложения, не прилагая дополнительных усилий на вопросы взаимодействия микросервисов;
    • описывать структуру микросервиса интерфейсом .NET и использовать его как при разработке самого микросервиса, так и для генерации Web-клиента (Web-клиент для микросервиса генерируется фабричным методом после регистрации интерфейса в DI и предоставляется как зависимость);
    • регистрировать интерфейсы микросервисов как зависимости в Dependency Injection;
    • организовать перенаправление запросов со стороны Web UI к микросервисам без дополнительных усилий при разработке UI.


    Microcommerce.zip
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 24

      +2
      Поставил плюс за основательную статью.

      Но из самой статьи не понятен профит разбивания коризны/каталога/шины событий на микросервисы. Будущие проблемы видны, выгоды не видны.
        0

        Очевидно, что разбивание было показано исключительно в познавательных целях. Разве что в будущем эти сервисы будут весьма толстыми, и на каждом будет сидеть своя команда, но это дискутируемо.

          0
          Например один общий микросервис корзины используют приложения на ios, android и сайт
          +4
          Сегодня статья была про 6 грехов программиста, вот у вас тут как минимум 2 — оверинжиниринг и велосипед. Велосипед потому, что вы игнорируете общепринятые стандартные фреймворки для микросервисов: OpenApi для веб и grpc для внутреннего взаимодействия. Они обеспечивают валидацию, транспорт и документирование апи, создают из декларативного описания сервисов код клиента и заглушки для конечных точек (grpc к тому же обеспечивает двунаправленный стриминг) — всё это в вашем случае придётся выполнять вручную. И вы блин такое рекомендуете другим? не смешите. У вас всё взаимодействие сервисов сводится к редиректу запроса — вы серьёзно считаете, что это исчерпывающий кейс?

          Кроме того, ваши измышления противоречат 12 factor apps в части логгирования. К вашему сведению, логи пишутся в консоль и никуда более, и далее транслируются в систему типа ELC. Иные решения, в стиле вашего ActivityLogger — грех.
            +3
            Очевидно, Вы невнимательно прочитали статью. Я ни в коем случае не игнорирую общепринятые фреймворки, по крайней мере OpenApi упомянут, и я с ним немало работал. Мне очень нравится swagger, но не нравится идея генерировать «болванку» для сервиса на основе yml и клиента как отдельную сборку. Предлагаемое в статье решение — просто альтернатива, и разумеется не панацея. Хорошо подходит для web-приложений, где нужно предоставлять большое количество разнородных и часто меняющих структуру данных.
            И насчет логирования (пишется с одним «г» :-). Я прекрасно знаю как логируются данные и что такое ELK (не ELC). Сервис ActivityLogger — не более чем пример опроса событий. Если Вы знакомы с упоминаемой книгой Хорсдала, Вы поймете, почему я привел именно этот пример.
              0
              «OpenApi упомянут» — не нашёл. Поиск по тексту в браузере ctrl+F «OpenApi» «Open Api» «swagger» и «сваггер» так же не дал результатов.

              «не нравится идея генерировать «болванку» для сервиса на основе yml и клиента как отдельную сборку» — хотелось бы аргументов

              «Хорошо подходит для web-приложений, где нужно предоставлять большое количество разнородных и часто меняющих структуру данных» — чем же для этого плох сваггер? Он как бы считается пром стандартом, и структур данных в спеке можно много наобъявлять, и менять их легко. Но в отличие от вашего решения сваггер унифицирует апи и предоставляет бесплатно транспорт, валидацию и документацию.

              «Если Вы знакомы с упоминаемой книгой Хорсдала, Вы поймете, почему я привел именно этот пример.» — знаком. Но там имхо существует путаница между журналированием и логированием (да, спасибо за поправку). Если ваш мета сервис именно _журналирует_, то вопросов нет. Но у вас же сервис назван Logger-ом — логично предположить из названия, что он «логирует»

              Кроме того остался вопрос межсервисной коммуникации — как в вашей архитектуре её осуществлять, вручную? но это не правильно.

                0
                «OpenApi упомянут» — не нашёл. Да, видимо ошибся в именовании, уже исправил.

                И насчет сваггера — я не говорю что он плох. Но давайте рассмотрим процесс разработки чего-то (назовем Service) с использованием OpenApi:
                1. Описать структуру сервиса и моделей данных.
                2. Сгенерировать ServiceApi, реализовать в нем нужную функциональность (лучше конечно функциональность вынести в отдельный сервис и внедрить его в ServiceApi как зависимость)
                3. Сгенерировать ServiceApiClient.
                4. Сконфигурировать использование ServiceApiClient во всех нужных местах приложения.
                5. Если планируем обращаться к ServiceApi из UI, то нужно либо как-то передать в UI конфигурации конечных точек, либо организовать редиректы, сконфигурировать CORS и т.д.

                Хотя признаю, бесплатное, из коробки, документирование, валидация и другие плюшки это приятно :-)

                Теперь с использованием описанного в статье подхода:
                1. Разработать интерфейс IService
                2. В микросервисе разработать реализацию этого сервиса и опубликовать как конечную точку (пара строк в Setup)
                3. Зарегистрировать IService как зависимость во всех нужных местах и использовать ее привычным образом (в общем-то для ServiceApiClient нужно сделать ровно то же самое, но в отличие от OpenApi мы можем все интерфейсы собрать в одну сборку)
                4. Ну с собственно редиректы на микросервисы из главного приложения (если нужно).

                Наше приложение Microcommerce с использованием OpenApi получается несколько более громоздким, не так ли?
                  0
                  Разработать интерфейс IService
                  Зарегистрировать IService как зависимость во всех нужных местах и использовать ее привычным образом


                  Тут на сцену выходит .NET Remoting, оказывается MS уже в 2000 году придумала фреймворк для микросервисов в внедрила его в рантайм .NET по самые помидоры.

                  Ох, история развития ПО действительно идёт по спирали.
                    0
                    оказывается MS уже в 2000 году придумала фреймворк для микросервисов — .NET Remoting в то время позиционировался как средство междоменного взаимодействия, но по сути это то же самое. Но собственно идея генерировать прокси по интерфейсу не так уж плоха, я бы даже сказал весьма продуктивна. Главное — чтобы это было просто и гибко. Согласитесь, это гораздо менее трудоемко, чем расписать структуру в yml, сгенерировать по ней заготовку сервиса и клиента, наполнить сервис функционалом и т.д.
                    +1
                    Так ведь нет, не получается.

                    Разрабатывать интерфейс IService не нужно, он генерируется из спеки и является своего рода низкоуровневой деталью реализации апи, используемой фреймворком.

                    Публиковать реализацию так же не нужно, она сама «публикуется».

                    ServiceApi и ServiceApiClient генерирует тулза — бесплатно, для этого не нужно прилагать умственных усилий. Вся конфигурация сервера сводится к запуску хттп листенера с указанием урла и прописывания миделварей точно так же как у вас (CORS и т.п ), ни какие редиректы не нужны. Банально прописывается реализация для сгенерированных заглушек и всё. При этом архитектура самого сервера не завязана на DI, а доменную модель можно делать как вам больше нравится — на DI или глобальных переменных или как-то ещё.

                    Что касается клиента, то ServiceApiClient на C# в данном случае (я пишу на другом ЯП, раньше писал на C#) нужен исключительно для интеграционных и функциональных тестов на апи. И его конфигурация так же сводится к указанию корневого урла сервера.

                    Далее, если мы не планируем обращаться к ServiceApi из веб приложения браузера, а планируем его юзать из внутренних сервисов, мобильных клиентов и, например, дектопа — то нам нужен не сваггер, а grpc как более универсальная, мощная, надёжная и простая технология. Сваггер — только для веба.

                    Конкретно для веба генерируется fetch-клиент и типы структур данных сервиса на typescript. Поэтому фронтенд программисту не нужно думать о конечных точках, обработке ошибок хттп и т.п. вещам касательно транспорта. Он получает полностью типизированный и рабочий апи, сгенерированный из спеки OpenApi, так же как и ServiceApiClient для тестов — ServiceApiClient.Foo(Bar). Фронтенд программист так же участвует в разработке спеки, внося в неё требуемые ему фичи. В вашем же случае типизация апи на стороне клиента и транспорт делаются вручную и поддерживаются в актуальном состоянии через боль и слёзы.

                      0
                      В вашем же случае типизация апи на стороне клиента и транспорт делаются вручную и поддерживаются в актуальном состоянии через боль и слёзы.

                      Ну на самом деле не все так печально с типизацией на стороне клиента
                      habr.com/ru/post/266899
                        0
                        Это не слишком впечатляет, поскольку фронтенд программисту всё ещё нужен готовый вызов сервиса с актуальными эндпойнтами, валидацией и обработкой ошибок хттп. А не только серверные структуры данных.

                        Вы попробуйте всё же в rpc фреймворки — сразу оцените насколько они упрощают жизнь
                0

                вы просто взрыв из прошлого, удивлен что не вспомнили генерации прокси из webservice в VS2008

                  0
                  OpenApi, в 2015 появившаяся в виде стандарта — взрыв из прошлого, а писать всё руками — модно и современно? ну ну

                  Вы забыли упомянуть про WCF — какое это старое г-но. grpc делает в сущности примерно то же самое и поэтому не нужен по вашей логике
                0
                На проекте которым я сейчас занимаюсь более 200 рабочих сервисов. Они одинаковые, но каждый занимается обработкой своей группы данных. Так как это все крутится на выделенных серверах, иногда возникает ситуация когда некоторые сервисы сгруппированные на одном сервере необходимо перемещать на менее загруженные сервера или резервный сервер. Ситуация редкая но встречается.

                Есть ситуация когда управляющий сервис, а с ним общается вся пачка из 200-т воркеров на постоянной основе, надо обновлять не останавливая работу. Мы поднимаем обновленный сервис и переводим на него работу всех воркеров.

                Таже самая ситуация в обратную сторону, при переносе сервиса-воркера на другой сервер или виртуалку сервисы работающие с фермой должны отслеживать их перемещение. Если это один сервис, то еще ничего, а если их десяток?

                У вас есть N ДЦ, в которых подняты идентичные системы, но обслуживают разные регионы. Например Европа-США-Азия, очень распространенный вариант размещения для увеличения отзывчивости. Предположим падает сервис или хостер уронил виртуалку или произошел непредвиденный сбой. Самый простой способ чтоб на время сбоя сервисы поднятые в других ДЦ перехватили работу павшего собрата, до момента его подъема.

                А как Вы решаете проблему передачи данных на несколько сервисов, со сложным роутингом, размещением сервисов в региональных ДЦ? В случае постройки работы проекта на WebApi разворот потоков данных фермы достаточно не простая задача.
                  +1
                  Для такой системы взаимодействие через WebApi плохо подходит. В такой архитектуре нужно заботиться о предоставлении конфигураций каждому микросервису, плюс к тому же нужно как-то решать вопрос балансировки нагрузки.

                  Мне кажется в вашем случае нужна звездообразная архитектура. Как вариант — использование какого-либо MQ-сервера, RabbitMq или ApachMq. Я разрабатывал подобные системы взаимодействия приложений, и даже есть Nuget-пакет для этого, хоть меня и критикуют за велосипеды :-). Если Вам интересно — пишите в личку, я поделюсь наработками.
                    0
                    Поэтому и спросил как Вы решаете подобный вопрос используя WebApi. В частности я для данной задачи использую NATS, он подошел лучше всего по производительности.
                      0
                      Опыта использования NATS у меня нет. Я немного почитал доки по Вашей ссылке, насколько я понял это message oriented система. Собственно, MQ-сервисы тоже из этой линейки, и да, я абсолютно согласен что такое решение хорошо решает проблему балансировки нагрузки.
                  0
                  Получается микросервисы завязаны на один общий пакет но при этом общаются между собой через http? А подход с API Gateway чем не устроил?
                    0
                    Эта статья — дополнение к книге Хорсдала, упомянутой во введении. Рассмотрен единственный вопрос — упрощение разработки микросервисов. В книге описывается использование Nancy, я предлагаю альтернативный подход, позволяющий оставить вопросы предоставления конечных точек, генерирования web-клиентов, переадресации «под капотом».
                    0

                    Мне иетересно, пробовали ли вы Microsoft Orleans? Как на меня отличнейшее решение для 90% случаев. Но тут я пока только как теоретик, пару раз кластер подымал — проще ничего нету.
                    Первое что в голову приходит


                    • Строгая типизация реквестов через интерфейсы со скоростной сериализацией
                    • Автоматическая балансировка вплоть до миграции воркера на другую машину с состоянием (естественно надо persistanse писать)
                    • Автоматический failover
                    • Поддержка кластерного singleton по умолчанию. Удобно, знеаете ли, знать что три машины не колбасят ту же задачу или данные.
                    • Distributed cache в силу того что работает предыдущий пункт
                    • Встроенные события/потоки/таймеры/remainders
                    • Discovery

                    Можно еще покопать, но как на меня профит очевиден: пишите себе на чистом .NET забыв о swagger. Расслабляетесь от балансировки "микросервисов", от толпы не нужных инстансов. Не конфигурируете кеши, очереди (ну без них не всегда можно).


                    Сумбурно как-то так

                      0
                      Нет, это я не пробовал. Для балансировки нагрузки и масштабирования я имею опыт использования ApacheMq. В общем-то те же фичи что описаны Вами для Microsoft Orleans, но плюс еще кросс-платформенность
                        0
                        Для балансировки нагрузки и масштабирования я имею опыт использования ApacheMq

                        Посмотрел, что-то больно на Java мир завязано. И разве это не брокер сообщений?
                        Я бы его больше к этому проекту сравнивал: https://github.com/dapr/dapr


                        В общем-то те же фичи что описаны Вами для Microsoft Orleans, но плюс еще кросс-платформенность

                        .NET Core давно кросплатформенный как бы.

                          0
                          Это действительно брокер сообщений, но особенности amq протокола позволяют организовать весьма причудливые сценарии взаимодействия, в т.ч. обработку очереди несколькими сервисами для распараллеливания обработки.
                          А насчет кросс-платформенности — я имел в виду взаимодействие с приложениями на java, php и т.д. Хотя Вы правы, лучше использовать чистый .Net Core, если нет каких-то экстраординарных кейсов.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое