diff --git a/README.md b/README.md index ad3de086b..07c9b3691 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**ALPHA state** - VS 2017 and CLI environments compatible) +# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**BETA state** - Visual Studio 2017 and CLI environments compatible) Sample .NET Core reference application, powered by Microsoft, based on a simplified microservices architecture and Docker containers.

> ### DISCLAIMER -> **IMPORTANT:** The current state of this sample application is **ALPHA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.** +> **IMPORTANT:** The current state of this sample application is **BETA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.** > -> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. However, this reference application it is not trying to solve all the problems in a large and mission-critical distributed system, it is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core. +> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. The chosen domain is an eShop/eCommerce but simply because it is a well-know domain by most people/developers. +However, this sample application should not be considered as an "eCommerce reference model", at all. The implemented business domain might not be ideal from an eCommerce business point of view. It is neither trying to solve all the problems in a large, scalable and mission-critical distributed system. It is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core. >

For example, the next step (still not covered in eShopOnContainers) after understanding Docker containers and microservices development with .NET Core, is to select a microservice cluster/orchestrator like Docker Swarm, Kubernetes or DC/OS (in Azure Container Service) or Azure Service Fabric which in most of the cases will require additional partial changes to your application's configuration (although the present architecture should work on most orchestrators with small changes). -> Or moving your databases to HA cloud services, or implementing your EventBus with Azure Service Bus or any other production ready Service Bus in the market. +> Additional steps would be to move your databases to HA cloud services, or to implement your EventBus with Azure Service Bus or any other production ready Service Bus in the market. >

In the future we might fork this project and make multiple versions targeting specific microservice cluster/orchestrators plus using additional cloud infrastructure.

> > Read the planned Roadmap and Milestones for future releases of eShopOnContainers within the Wiki for further info about possible new implementations and provide feedback at the ISSUES section if you'd like to see any specific scenario implemented or improved. Also, feel free to discuss on any current issue. @@ -32,7 +33,7 @@ Additional miroservice styles with other frameworks and No-SQL databases will be >

However, in a real production environment it is recommended to have your databases (SQL Server and Redis, in this case) in HA (High Available) services like Azure SQL Database, Redis as a service or any other clustering system. If you want to change to a production configuration, you'll just need to change the connection strings once you have set up the servers in a HA cloud or on-premises. ## Related documentation and guidance -While developing this reference application, we are creating a reference Guide/eBook named "Architecting and Developing Containerized and Microservice based .NET Applications" which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers. +While developing this reference application, we've been creating a reference Guide/eBook focusing on architecting and developing containerized and microservice based .NET Applications (download link available below) which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers.

There are also additional eBooks focusing on Containers/Docker lifecycle (DevOps, CI/CD, etc.) with Microsoft Tools, already published plus an additional eBook focusing on Enterprise Apps Patterns with Xamarin.Forms. You can download them and start reviewing these Guides/eBooks here: @@ -43,7 +44,7 @@ You can download them and start reviewing these Guides/eBooks here: | | | | | **Download** (Early DRAFT, still work in progress) | **Download** (First Edition from late 2016) | **Download** (Early DRAFT, still work in progress) | -Send feedback to [cesardl@microsoft.com](cesardl@microsoft.com) +Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)

However, we encourage to download and review the "Architecting & Developing eBook" because the architectural styles and architectural patterns and technologies explained in the guidance are using this reference application when explaining many pattern implementations, so you'll understand much better the context, design and decisions taken in the current architecture and internal designs. diff --git a/img/ebook_arch_dev_microservices_containers_cover.png b/img/ebook_arch_dev_microservices_containers_cover.png index 1837354b5..caf16c665 100644 Binary files a/img/ebook_arch_dev_microservices_containers_cover.png and b/img/ebook_arch_dev_microservices_containers_cover.png differ diff --git a/img/ebook_arch_dev_microservices_containers_cover_LARGE.png b/img/ebook_arch_dev_microservices_containers_cover_LARGE.png new file mode 100644 index 000000000..f18ec386f Binary files /dev/null and b/img/ebook_arch_dev_microservices_containers_cover_LARGE.png differ diff --git a/img/ebook_arch_dev_microservices_containers_cover_OLD.png b/img/ebook_arch_dev_microservices_containers_cover_OLD.png new file mode 100644 index 000000000..1837354b5 Binary files /dev/null and b/img/ebook_arch_dev_microservices_containers_cover_OLD.png differ diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs index af6ee1aa7..63f9f1b99 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions { diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs index d755bc066..828aed26a 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions diff --git a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs index c9e60a0cf..e01a7aaa8 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events { diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs new file mode 100644 index 000000000..894afb4e4 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs @@ -0,0 +1,130 @@ +using Microsoft.Extensions.Logging; +using Polly; +using Polly.Retry; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; +using System; +using System.IO; +using System.Net.Sockets; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ +{ + public class DefaultRabbitMQPersisterConnection + : IRabbitMQPersisterConnection + { + private readonly IConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + IConnection _connection; + bool _disposed; + + object sync_root = new object(); + + public DefaultRabbitMQPersisterConnection(IConnectionFactory connectionFactory,ILogger logger) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public bool IsConnected + { + get + { + return _connection != null && _connection.IsOpen && !_disposed; + } + } + + public IModel CreateModel() + { + if (!IsConnected) + { + throw new InvalidOperationException("No RabbitMQ connections are available to perform this action"); + } + + return _connection.CreateModel(); + } + + public void Dispose() + { + if (_disposed) return; + + _disposed = true; + + try + { + _connection.Dispose(); + } + catch (IOException ex) + { + _logger.LogCritical(ex.ToString()); + } + } + + public bool TryConnect() + { + _logger.LogInformation("RabbitMQ Client is trying to connect"); + + lock (sync_root) + { + var policy = RetryPolicy.Handle() + .Or() + .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + } + ); + + policy.Execute(() => + { + _connection = _connectionFactory + .CreateConnection(); + }); + + if (IsConnected) + { + _connection.ConnectionShutdown += OnConnectionShutdown; + _connection.CallbackException += OnCallbackException; + _connection.ConnectionBlocked += OnConnectionBlocked; + + _logger.LogInformation($"RabbitMQ persister connection acquire a connection {_connection.Endpoint.HostName} and is subscribed to failure events"); + + return true; + } + else + { + _logger.LogCritical("FATAL ERROR: RabbitMQ connections can't be created and opened"); + + return false; + } + } + } + + private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect..."); + + TryConnect(); + } + + void OnCallbackException(object sender, CallbackExceptionEventArgs e) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect..."); + + TryConnect(); + } + + void OnConnectionShutdown(object sender, ShutdownEventArgs reason) + { + if (_disposed) return; + + _logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect..."); + + TryConnect(); + } + } +} diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index 3388875ab..e7a493c10 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -1,13 +1,16 @@ - -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Polly; +using Polly.Retry; using RabbitMQ.Client; using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; using System; -using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Net.Sockets; using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -16,68 +19,98 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ { public class EventBusRabbitMQ : IEventBus, IDisposable { - private readonly string _brokerName = "eshop_event_bus"; - private readonly string _connectionString; - private readonly Dictionary> _handlers; - private readonly List _eventTypes; + const string BROKER_NAME = "eshop_event_bus"; - private IModel _model; - private IConnection _connection; + private readonly IRabbitMQPersisterConnection _persisterConnection; + private readonly ILogger _logger; + + private readonly Dictionary> _handlers + = new Dictionary>(); + + private readonly List _eventTypes + = new List(); + + private IModel _consumerChannel; private string _queueName; - - public EventBusRabbitMQ(string connectionString) + public EventBusRabbitMQ(IRabbitMQPersisterConnection persisterConnection, ILogger logger) { - _connectionString = connectionString; - _handlers = new Dictionary>(); - _eventTypes = new List(); + _persisterConnection = persisterConnection ?? throw new ArgumentNullException(nameof(persisterConnection)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _consumerChannel = CreateConsumerChannel(); } + public void Publish(IntegrationEvent @event) { - var eventName = @event.GetType().Name; - var factory = new ConnectionFactory() { HostName = _connectionString }; - using (var connection = factory.CreateConnection()) - using (var channel = connection.CreateModel()) + if (!_persisterConnection.IsConnected) { - channel.ExchangeDeclare(exchange: _brokerName, + _persisterConnection.TryConnect(); + } + + var policy = RetryPolicy.Handle() + .Or() + .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + }); + + using (var channel = _persisterConnection.CreateModel()) + { + var eventName = @event.GetType() + .Name; + + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); - string message = JsonConvert.SerializeObject(@event); + var message = JsonConvert.SerializeObject(@event); var body = Encoding.UTF8.GetBytes(message); - channel.BasicPublish(exchange: _brokerName, + policy.Execute(() => + { + channel.BasicPublish(exchange: BROKER_NAME, routingKey: eventName, basicProperties: null, - body: body); + body: body); + }); } - } public void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { var eventName = typeof(T).Name; - if (_handlers.ContainsKey(eventName)) + + if (_handlers.ContainsKey(eventName)) { _handlers[eventName].Add(handler); } else { - var channel = GetChannel(); - channel.QueueBind(queue: _queueName, - exchange: _brokerName, - routingKey: eventName); - - _handlers.Add(eventName, new List()); - _handlers[eventName].Add(handler); - _eventTypes.Add(typeof(T)); + if (!_persisterConnection.IsConnected) + { + _persisterConnection.TryConnect(); + } + + using (var channel = _persisterConnection.CreateModel()) + { + channel.QueueBind(queue: _queueName, + exchange: BROKER_NAME, + routingKey: eventName); + + _handlers.Add(eventName, new List()); + _handlers[eventName].Add(handler); + _eventTypes.Add(typeof(T)); + } + } - + } public void Unsubscribe(IIntegrationEventHandler handler) where T : IntegrationEvent { var eventName = typeof(T).Name; + if (_handlers.ContainsKey(eventName) && _handlers[eventName].Contains(handler)) { _handlers[eventName].Remove(handler); @@ -85,56 +118,59 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ if (_handlers[eventName].Count == 0) { _handlers.Remove(eventName); - var eventType = _eventTypes.Single(e => e.Name == eventName); - _eventTypes.Remove(eventType); - _model.QueueUnbind(queue: _queueName, - exchange: _brokerName, - routingKey: eventName); - if (_handlers.Keys.Count == 0) + var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); + + if (eventType != null) { - _queueName = string.Empty; - _model.Dispose(); - _connection.Dispose(); + _eventTypes.Remove(eventType); + + if (!_persisterConnection.IsConnected) + { + _persisterConnection.TryConnect(); + } + + using (var channel = _persisterConnection.CreateModel()) + { + channel.QueueUnbind(queue: _queueName, + exchange: BROKER_NAME, + routingKey: eventName); + + if (_handlers.Keys.Count == 0) + { + _queueName = string.Empty; + + _consumerChannel.Close(); + } + } } - } } } public void Dispose() { + if (_consumerChannel != null) + { + _consumerChannel.Dispose(); + } + _handlers.Clear(); - _model?.Dispose(); - _connection?.Dispose(); } - private IModel GetChannel() + private IModel CreateConsumerChannel() { - if (_model != null) + if (!_persisterConnection.IsConnected) { - return _model; + _persisterConnection.TryConnect(); } - else - { - (_model, _connection) = CreateConnection(); - return _model; - } - } + var channel = _persisterConnection.CreateModel(); - private (IModel model, IConnection connection) CreateConnection() - { - var factory = new ConnectionFactory() { HostName = _connectionString }; - var con = factory.CreateConnection(); - var channel = con.CreateModel(); + channel.ExchangeDeclare(exchange: BROKER_NAME, + type: "direct"); - channel.ExchangeDeclare(exchange: _brokerName, - type: "direct"); - if (string.IsNullOrEmpty(_queueName)) - { - _queueName = channel.QueueDeclare().QueueName; - } + _queueName = channel.QueueDeclare().QueueName; var consumer = new EventingBasicConsumer(channel); consumer.Received += async (model, ea) => @@ -144,11 +180,18 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ await ProcessEvent(eventName, message); }; + channel.BasicConsume(queue: _queueName, noAck: true, consumer: consumer); - return (channel, con); + channel.CallbackException += (sender, ea) => + { + _consumerChannel.Dispose(); + _consumerChannel = CreateConsumerChannel(); + }; + + return channel; } private async Task ProcessEvent(string eventName, string message) @@ -156,7 +199,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ if (_handlers.ContainsKey(eventName)) { Type eventType = _eventTypes.Single(t => t.Name == eventName); - var integrationEvent = JsonConvert.DeserializeObject(message, eventType); + var integrationEvent = JsonConvert.DeserializeObject(message, eventType); var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); var handlers = _handlers[eventName]; @@ -166,6 +209,5 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ } } } - } } diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj index cf36a2222..023a5d5ec 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj @@ -7,7 +7,9 @@ + + diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs new file mode 100644 index 000000000..b9debe743 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs @@ -0,0 +1,16 @@ +using RabbitMQ.Client; +using System; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ +{ + + public interface IRabbitMQPersisterConnection + : IDisposable + { + bool IsConnected { get; } + + bool TryConnect(); + + IModel CreateModel(); + } +} diff --git a/src/Services/Basket/Basket.API/BasketSettings.cs b/src/Services/Basket/Basket.API/BasketSettings.cs index 6aae45015..9d143545a 100644 --- a/src/Services/Basket/Basket.API/BasketSettings.cs +++ b/src/Services/Basket/Basket.API/BasketSettings.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Basket.API +namespace Microsoft.eShopOnContainers.Services.Basket.API { public class BasketSettings { diff --git a/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs index 2ec3727a6..a0b988156 100644 --- a/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs +++ b/src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -1,9 +1,5 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Basket.API.Infrastructure.ActionResults { diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs index e35badc64..19ae1b594 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs @@ -2,8 +2,6 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Basket.API.IntegrationEvents.EventHandling @@ -11,9 +9,10 @@ namespace Basket.API.IntegrationEvents.EventHandling public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler { private readonly IBasketRepository _repository; + public OrderStartedIntegrationEventHandler(IBasketRepository repository) { - _repository = repository; + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } public async Task Handle(OrderStartedIntegrationEvent @event) diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs index 50d6b9e25..88244404a 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs @@ -1,6 +1,7 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Basket.API.Model; +using System; using System.Linq; using System.Threading.Tasks; @@ -9,17 +10,20 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Even public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler { private readonly IBasketRepository _repository; + public ProductPriceChangedIntegrationEventHandler(IBasketRepository repository) { - _repository = repository; + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } public async Task Handle(ProductPriceChangedIntegrationEvent @event) { - var userIds = await _repository.GetUsers(); + var userIds = await _repository.GetUsersAsync(); + foreach (var id in userIds) { var basket = await _repository.GetBasketAsync(id); + await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, @event.OldPrice, basket); } } @@ -27,6 +31,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Even private async Task UpdatePriceInBasketItems(int productId, decimal newPrice, decimal oldPrice, CustomerBasket basket) { var itemsToUpdate = basket?.Items?.Where(x => int.Parse(x.ProductId) == productId).ToList(); + if (itemsToUpdate != null) { foreach (var item in itemsToUpdate) diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs index 3b5726e83..a32ad0beb 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs @@ -1,8 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Basket.API.IntegrationEvents.Events { diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs index 87d2e9e81..6f51010be 100644 --- a/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events { diff --git a/src/Services/Basket/Basket.API/Model/IBasketRepository.cs b/src/Services/Basket/Basket.API/Model/IBasketRepository.cs index 7f75ea342..fcdc69faa 100644 --- a/src/Services/Basket/Basket.API/Model/IBasketRepository.cs +++ b/src/Services/Basket/Basket.API/Model/IBasketRepository.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Basket.API.Model @@ -8,7 +6,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model public interface IBasketRepository { Task GetBasketAsync(string customerId); - Task> GetUsers(); + Task> GetUsersAsync(); Task UpdateBasketAsync(CustomerBasket basket); Task DeleteBasketAsync(string id); } diff --git a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs index 59ca0da03..01a8b4728 100644 --- a/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs +++ b/src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using StackExchange.Redis; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; -using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Basket.API.Model { @@ -31,7 +30,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model return await database.KeyDeleteAsync(id.ToString()); } - public async Task> GetUsers() + public async Task> GetUsersAsync() { var server = await GetServer(); @@ -63,11 +62,12 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model var created = await database.StringSetAsync(basket.BuyerId, JsonConvert.SerializeObject(basket)); if (!created) { - _logger.LogInformation("Problem persisting the item"); + _logger.LogInformation("Problem occur persisting the item."); return null; } - _logger.LogInformation("basket item persisted succesfully"); + _logger.LogInformation("Basket item persisted succesfully."); + return await GetBasketAsync(basket.BuyerId); } @@ -94,13 +94,21 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model private async Task ConnectToRedisAsync() { - //TODO: Need to make this more robust. Also want to understand why the static connection method cannot accept dns names. - var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString); - _logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}"); - _redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString()); + // TODO: Need to make this more robust. ConnectionMultiplexer.ConnectAsync doesn't like domain names or IPv6 addresses. + if (IPAddress.TryParse(_settings.ConnectionString, out var ip)) + { + _redis = await ConnectionMultiplexer.ConnectAsync(ip.ToString()); + _logger.LogInformation($"Connecting to database at {_settings.ConnectionString}"); + } + else + { + // workaround for https://github.com/StackExchange/StackExchange.Redis/issues/410 + var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString); + _logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}"); + _redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString()); + } } - - + } } diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs index 11a6ebc97..60fc46de2 100644 --- a/src/Services/Basket/Basket.API/Startup.cs +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -1,24 +1,24 @@ -using System.Linq; +using Basket.API.Infrastructure.Filters; +using Basket.API.IntegrationEvents.EventHandling; +using Basket.API.IntegrationEvents.Events; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +using Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; +using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; +using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; -using Microsoft.eShopOnContainers.Services.Basket.API.Model; -using StackExchange.Redis; using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using StackExchange.Redis; +using System.Linq; using System.Net; -using Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; -using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; -using System; -using Microsoft.Extensions.HealthChecks; using System.Threading.Tasks; -using Basket.API.Infrastructure.Filters; -using Basket.API.IntegrationEvents.Events; -using Basket.API.IntegrationEvents.EventHandling; namespace Microsoft.eShopOnContainers.Services.Basket.API { @@ -59,17 +59,33 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API //and then creating the connection it seems reasonable to move //that cost to startup instead of having the first request pay the //penalty. - services.AddSingleton((sp) => { - var config = sp.GetRequiredService>().Value; - var ips = Dns.GetHostAddressesAsync(config.ConnectionString).Result; + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var ips = Dns.GetHostAddressesAsync(settings.ConnectionString).Result; + return ConnectionMultiplexer.Connect(ips.First().ToString()); }); + + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); + var factory = new ConnectionFactory() + { + HostName = settings.EventBusConnection + }; + + return new DefaultRabbitMQPersisterConnection(factory, logger); + }); + + services.AddSingleton(); + services.AddSwaggerGen(); - //var sch = new IdentitySecurityScheme(); + services.ConfigureSwaggerGen(options => { - //options.AddSecurityDefinition("IdentityServer", sch); options.OperationFilter(); options.DescribeAllEnumsAsStrings(); options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info() @@ -95,9 +111,6 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API services.AddTransient, ProductPriceChangedIntegrationEventHandler>(); services.AddTransient, OrderStartedIntegrationEventHandler>(); - var serviceProvider = services.BuildServiceProvider(); - var configuration = serviceProvider.GetRequiredService>().Value; - services.AddSingleton(provider => new EventBusRabbitMQ(configuration.EventBusConnection)); } @@ -119,11 +132,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API app.UseSwagger() .UseSwaggerUi(); - var catalogPriceHandler = app.ApplicationServices.GetService>(); - var orderStartedHandler = app.ApplicationServices.GetService>(); - var eventBus = app.ApplicationServices.GetRequiredService(); - eventBus.Subscribe(catalogPriceHandler); - eventBus.Subscribe(orderStartedHandler); + ConfigureEventBus(app); } @@ -136,6 +145,21 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API ScopeName = "basket", RequireHttpsMetadata = false }); - } + } + + protected virtual void ConfigureEventBus(IApplicationBuilder app) + { + var catalogPriceHandler = app.ApplicationServices + .GetService>(); + + var orderStartedHandler = app.ApplicationServices + .GetService>(); + + var eventBus = app.ApplicationServices + .GetRequiredService(); + + eventBus.Subscribe(catalogPriceHandler); + eventBus.Subscribe(orderStartedHandler); + } } } diff --git a/src/Services/Catalog/Catalog.API/CatalogSettings.cs b/src/Services/Catalog/Catalog.API/CatalogSettings.cs new file mode 100644 index 000000000..af6e0ab13 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/CatalogSettings.cs @@ -0,0 +1,9 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API +{ + public class CatalogSettings + { + public string ExternalCatalogBaseUrl {get;set;} + + public string EventBusConnection { get; set; } + } +} diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index 83a2de02d..374d8ec7c 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -1,9 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Catalog.API.IntegrationEvents; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; -using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Catalog.API.Model; @@ -11,12 +8,8 @@ using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.Data.Common; using System.Linq; using System.Threading.Tasks; -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; -using Catalog.API.IntegrationEvents; namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { @@ -24,16 +17,16 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers public class CatalogController : ControllerBase { private readonly CatalogContext _catalogContext; - private readonly IOptionsSnapshot _settings; + private readonly CatalogSettings _settings; private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; - public CatalogController(CatalogContext Context, IOptionsSnapshot settings, ICatalogIntegrationEventService catalogIntegrationEventService) + public CatalogController(CatalogContext context, IOptionsSnapshot settings, ICatalogIntegrationEventService catalogIntegrationEventService) { - _catalogContext = Context; - _catalogIntegrationEventService = catalogIntegrationEventService; - _settings = settings; + _catalogContext = context ?? throw new ArgumentNullException(nameof(context)); + _catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); - ((DbContext)Context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + _settings = settings.Value; + ((DbContext)context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] @@ -46,19 +39,37 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers .LongCountAsync(); var itemsOnPage = await _catalogContext.CatalogItems - .OrderBy(c=>c.Name) + .OrderBy(c => c.Name) .Skip(pageSize * pageIndex) .Take(pageSize) .ToListAsync(); - itemsOnPage = ComposePicUri(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel( - pageIndex, pageSize, totalItems, itemsOnPage); + pageIndex, pageSize, totalItems, itemsOnPage); return Ok(model); } + [HttpGet] + [Route("items/{id:int}")] + public async Task GetItemById(int id) + { + if (id <= 0) + { + return BadRequest(); + } + + var item = await _catalogContext.CatalogItems.SingleOrDefaultAsync(ci => ci.Id == id); + if (item != null) + { + return Ok(item); + } + + return NotFound(); + } + // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10] [HttpGet] [Route("[action]/withname/{name:minlength(1)}")] @@ -75,7 +86,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers .Take(pageSize) .ToListAsync(); - itemsOnPage = ComposePicUri(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel( pageIndex, pageSize, totalItems, itemsOnPage); @@ -108,7 +119,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers .Take(pageSize) .ToListAsync(); - itemsOnPage = ComposePicUri(itemsOnPage); + itemsOnPage = ChangeUriPlaceholder(itemsOnPage); var model = new PaginatedItemsViewModel( pageIndex, pageSize, totalItems, itemsOnPage); @@ -138,16 +149,23 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers return Ok(items); } - //POST api/v1/[controller]/update - [Route("update")] - [HttpPost] + //PUT api/v1/[controller]/items + [Route("items")] + [HttpPut] public async Task UpdateProduct([FromBody]CatalogItem productToUpdate) { - var catalogItem = await _catalogContext.CatalogItems.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); - if (catalogItem == null) return NotFound(); - var raiseProductPriceChangedEvent = catalogItem.Price != productToUpdate.Price; + var catalogItem = await _catalogContext.CatalogItems + .SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); + + if (catalogItem == null) + { + return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); + } + var oldPrice = catalogItem.Price; - + var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; + + // Update current product catalogItem = productToUpdate; _catalogContext.CatalogItems.Update(catalogItem); @@ -156,40 +174,40 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { //Create Integration Event to be published through the Event Bus var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); - + // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); - + // Publish through the Event Bus and mark the saved event as published await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); } else // Save updated product { await _catalogContext.SaveChangesAsync(); - } + } - return Ok(); + return CreatedAtAction(nameof(GetItemById), new { id = productToUpdate.Id }, null); } - //POST api/v1/[controller]/create - [Route("create")] + //POST api/v1/[controller]/items + [Route("items")] [HttpPost] public async Task CreateProduct([FromBody]CatalogItem product) { - _catalogContext.CatalogItems.Add( - new CatalogItem - { - CatalogBrandId = product.CatalogBrandId, - CatalogTypeId = product.CatalogTypeId, - Description = product.Description, - Name = product.Name, - PictureUri = product.PictureUri, - Price = product.Price - }); + var item = new CatalogItem + { + CatalogBrandId = product.CatalogBrandId, + CatalogTypeId = product.CatalogTypeId, + Description = product.Description, + Name = product.Name, + PictureUri = product.PictureUri, + Price = product.Price + }; + _catalogContext.CatalogItems.Add(item); await _catalogContext.SaveChangesAsync(); - return Ok(); + return CreatedAtAction(nameof(GetItemById), new { id = item.Id }, null); } //DELETE api/v1/[controller]/id @@ -202,16 +220,19 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers if (product == null) { return NotFound(); - } + } _catalogContext.CatalogItems.Remove(product); + await _catalogContext.SaveChangesAsync(); - return Ok(); + return NoContent(); } - private List ComposePicUri(List items) { - var baseUri = _settings.Value.ExternalCatalogBaseUrl; + private List ChangeUriPlaceholder(List items) + { + var baseUri = _settings.ExternalCatalogBaseUrl; + items.ForEach(x => { x.PictureUri = x.PictureUri.Replace("http://externalcatalogbaseurltobereplaced", baseUri); diff --git a/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs index e6ade8cc5..9401b56f9 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/HomeController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 diff --git a/src/Services/Catalog/Catalog.API/Controllers/PicController.cs b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs index fa6b4ec94..8d8aaf9f2 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/PicController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/PicController.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using System.IO; -using Microsoft.AspNetCore.Hosting; // For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 @@ -25,8 +21,10 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { var webRoot = _env.WebRootPath; var path = Path.Combine(webRoot, id + ".png"); - Byte[] b = System.IO.File.ReadAllBytes(path); - return File(b, "image/png"); + + var buffer = System.IO.File.ReadAllBytes(path); + + return File(buffer, "image/png"); } } } diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs index 3ac3d0f78..a6138b476 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -1,9 +1,5 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Catalog.API.Infrastructure.ActionResults { diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs b/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs index 0b27131cf..45295994e 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Catalog.API.Infrastructure.Exceptions { diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs index e6e48c54b..1b82251e3 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs @@ -30,6 +30,7 @@ namespace Catalog.API.IntegrationEvents public async Task PublishThroughEventBusAsync(IntegrationEvent evt) { _eventBus.Publish(evt); + await _eventLogService.MarkEventAsPublishedAsync(evt); } diff --git a/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs index bb958eeaa..4d87e8e94 100644 --- a/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs +++ b/src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs @@ -1,7 +1,4 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Catalog.API.IntegrationEvents diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs b/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs index eb1fabf7d..84d72899e 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs @@ -1,11 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - - public class CatalogBrand { public int Id { get; set; } diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogType.cs b/src/Services/Catalog/Catalog.API/Model/CatalogType.cs index ac71914ff..0bc640dee 100644 --- a/src/Services/Catalog/Catalog.API/Model/CatalogType.cs +++ b/src/Services/Catalog/Catalog.API/Model/CatalogType.cs @@ -1,10 +1,5 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - public class CatalogType { public int Id { get; set; } diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index f3f0735b2..c13ac2d1b 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -1,6 +1,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API { using global::Catalog.API.Infrastructure.Filters; + using global::Catalog.API.IntegrationEvents; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -15,13 +16,11 @@ using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using RabbitMQ.Client; using System; - using System.Data.SqlClient; - using System.IO; using System.Data.Common; + using System.Data.SqlClient; using System.Reflection; - using global::Catalog.API.IntegrationEvents; - using System.Threading.Tasks; public class Startup { @@ -47,7 +46,7 @@ public void ConfigureServices(IServiceCollection services) { // Add framework services. - + services.AddHealthChecks(checks => { checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"]); @@ -62,18 +61,19 @@ { options.UseSqlServer(Configuration["ConnectionString"], sqlServerOptionsAction: sqlOptions => - { + { sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); }); + // Changing default behavior when client evaluation occurs to throw. // Default in EF Core would be to log a warning when client evaluation is performed. options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval }); - services.Configure(Configuration); + services.Configure(Configuration); // Add framework services. services.AddSwaggerGen(); @@ -99,11 +99,23 @@ }); services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - var serviceProvider = services.BuildServiceProvider(); - var configuration = serviceProvider.GetRequiredService>().Value; + sp => (DbConnection c) => new IntegrationEventLogService(c)); + services.AddTransient(); - services.AddSingleton(new EventBusRabbitMQ(configuration.EventBusConnection)); + + services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>().Value; + var logger = sp.GetRequiredService>(); + var factory = new ConnectionFactory() + { + HostName = settings.EventBusConnection + }; + + return new DefaultRabbitMQPersisterConnection(factory, logger); + }); + + services.AddSingleton(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) @@ -124,25 +136,28 @@ .ApplicationServices.GetService(typeof(CatalogContext)); WaitForSqlAvailability(context, loggerFactory); + //Seed Data CatalogContextSeed.SeedAsync(app, loggerFactory) - .Wait(); + .Wait(); var integrationEventLogContext = new IntegrationEventLogContext( new DbContextOptionsBuilder() .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API")) .Options); + integrationEventLogContext.Database.Migrate(); } private void WaitForSqlAvailability(CatalogContext ctx, ILoggerFactory loggerFactory, int? retry = 0) { - int retryForAvailability = retry.Value; + int retryForAvailability = retry.Value; + try { ctx.Database.OpenConnection(); } - catch(SqlException ex) + catch (SqlException ex) { if (retryForAvailability < 10) { @@ -152,11 +167,10 @@ WaitForSqlAvailability(ctx, loggerFactory, retryForAvailability); } } - finally { - ctx.Database.CloseConnection(); + finally + { + ctx.Database.CloseConnection(); } - - } } } diff --git a/src/Services/Catalog/Catalog.API/settings.cs b/src/Services/Catalog/Catalog.API/settings.cs deleted file mode 100644 index a6e959552..000000000 --- a/src/Services/Catalog/Catalog.API/settings.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Catalog.API -{ - // TODO: Rename CatalogSettings for consistency? - public class Settings - { - public string ExternalCatalogBaseUrl {get;set;} - public string EventBusConnection { get; set; } - } -} diff --git a/src/Services/Identity/Identity.API/Startup.cs b/src/Services/Identity/Identity.API/Startup.cs index c652de6a9..b47f0535d 100644 --- a/src/Services/Identity/Identity.API/Startup.cs +++ b/src/Services/Identity/Identity.API/Startup.cs @@ -57,6 +57,11 @@ namespace eShopOnContainers.Identity services.Configure(Configuration); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.identity"; + }); + services.AddMvc(); services.AddHealthChecks(checks => diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs index de7dc4fea..529c1aa05 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs @@ -45,9 +45,11 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands return CreateResultForDuplicateRequest(); } else - { - var result = await _mediator.SendAsync(message.Command); + { await _requestManager.CreateRequestForCommandAsync(message.Id); + + var result = await _mediator.SendAsync(message.Command); + return result; } } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs index f12a9e418..8d78524ea 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs @@ -4,10 +4,10 @@ public interface IOrderQueries { - Task GetOrder(int id); + Task GetOrderAsync(int id); - Task GetOrders(); + Task GetOrdersAsync(); - Task GetCardTypes(); + Task GetCardTypesAsync(); } } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index f10f76273..9d909e254 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -19,7 +19,7 @@ } - public async Task GetOrder(int id) + public async Task GetOrderAsync(int id) { using (var connection = new SqlConnection(_connectionString)) { @@ -44,7 +44,7 @@ } } - public async Task GetOrders() + public async Task GetOrdersAsync() { using (var connection = new SqlConnection(_connectionString)) { @@ -58,7 +58,7 @@ } } - public async Task GetCardTypes() + public async Task GetCardTypesAsync() { using (var connection = new SqlConnection(_connectionString)) { diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs index 449f95df5..e6bbdd4d9 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs @@ -1,10 +1,8 @@ using FluentValidation; -using MediatR; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; namespace Ordering.API.Application.Validations @@ -13,17 +11,17 @@ namespace Ordering.API.Application.Validations { public CreateOrderCommandValidator() { - RuleFor(order => order.City).NotEmpty(); - RuleFor(order => order.Street).NotEmpty(); - RuleFor(order => order.State).NotEmpty(); - RuleFor(order => order.Country).NotEmpty(); - RuleFor(order => order.ZipCode).NotEmpty(); - RuleFor(order => order.CardNumber).NotEmpty().Length(12, 19); - RuleFor(order => order.CardHolderName).NotEmpty(); - RuleFor(order => order.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date"); - RuleFor(order => order.CardSecurityNumber).NotEmpty().Length(3); - RuleFor(order => order.CardTypeId).NotEmpty(); - RuleFor(order => order.OrderItems).Must(ContainOrderItems).WithMessage("No order items found"); + RuleFor(command => command.City).NotEmpty(); + RuleFor(command => command.Street).NotEmpty(); + RuleFor(command => command.State).NotEmpty(); + RuleFor(command => command.Country).NotEmpty(); + RuleFor(command => command.ZipCode).NotEmpty(); + RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19); + RuleFor(command => command.CardHolderName).NotEmpty(); + RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date"); + RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3); + RuleFor(command => command.CardTypeId).NotEmpty(); + RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found"); } private bool BeValidExpirationDate(DateTime dateTime) diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs index 44b374ee6..e1482287f 100644 --- a/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs +++ b/src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs @@ -1,9 +1,5 @@ using FluentValidation; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Ordering.API.Application.Validations { @@ -11,7 +7,7 @@ namespace Ordering.API.Application.Validations { public IdentifierCommandValidator() { - RuleFor(customer => customer.Id).NotEmpty(); + RuleFor(command => command.Id).NotEmpty(); } } } diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index 4121e3214..d9a3752ed 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -57,7 +57,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers { try { - var order = await _orderQueries.GetOrder(orderId); + var order = await _orderQueries + .GetOrderAsync(orderId); + return Ok(order); } catch (KeyNotFoundException) @@ -70,7 +72,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers [HttpGet] public async Task GetOrders() { - var orders = await _orderQueries.GetOrders(); + var orders = await _orderQueries + .GetOrdersAsync(); return Ok(orders); } @@ -79,7 +82,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers [HttpGet] public async Task GetCardTypes() { - var cardTypes = await _orderQueries.GetCardTypes(); + var cardTypes = await _orderQueries + .GetCardTypesAsync(); return Ok(cardTypes); } diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs index 7d19815b9..69faf8e42 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -41,12 +41,12 @@ { var json = new JsonErrorResponse { - Messages = new[] { "An error ocurr.Try it again." } + Messages = new[] { "An error occur.Try it again." } }; if (env.IsDevelopment()) { - json.DeveloperMeesage = context.Exception; + json.DeveloperMessage = context.Exception; } // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 @@ -61,7 +61,7 @@ { public string[] Messages { get; set; } - public object DeveloperMeesage { get; set; } + public object DeveloperMessage { get; set; } } } } diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 6b120eb78..0d6e222b6 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; using Ordering.Infrastructure; + using RabbitMQ.Client; using System; using System.Data.Common; using System.Reflection; @@ -65,14 +66,14 @@ services.AddEntityFrameworkSqlServer() .AddDbContext(options => { - options.UseSqlServer(Configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); - sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - }, - ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) + options.UseSqlServer(Configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }, + ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) ); services.AddSwaggerGen(); @@ -105,7 +106,21 @@ sp => (DbConnection c) => new IntegrationEventLogService(c)); var serviceProvider = services.BuildServiceProvider(); services.AddTransient(); - services.AddSingleton(new EventBusRabbitMQ(Configuration["EventBusConnection"])); + + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = Configuration["EventBusConnection"] + }; + + return new DefaultRabbitMQPersisterConnection(factory, logger); + }); + + services.AddSingleton(); + services.AddOptions(); //configure autofac diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs index 84040d8a1..c806cebeb 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs @@ -16,6 +16,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B public IEnumerable PaymentMethods => _paymentMethods.AsReadOnly(); protected Buyer() { + _paymentMethods = new List(); } @@ -34,6 +35,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B if (existingPayment != null) { AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, existingPayment, orderId)); + return existingPayment; } else @@ -41,7 +43,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B var payment = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration); _paymentMethods.Add(payment); + AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, payment, orderId)); + return payment; } } diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs index a35188d8f..d7346ee4f 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs @@ -9,9 +9,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O public interface IOrderRepository : IRepository { Order Add(Order order); + + void Update(Order order); Task GetAsync(int orderId); - - void Update(Order order); } } diff --git a/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs b/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs index fd74ab1df..ec8dfd968 100644 --- a/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs +++ b/src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs @@ -71,17 +71,17 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork public static T FromValue(int value) where T : Enumeration, new() { - var matchingItem = parse(value, "value", item => item.Id == value); + var matchingItem = Parse(value, "value", item => item.Id == value); return matchingItem; } public static T FromDisplayName(string displayName) where T : Enumeration, new() { - var matchingItem = parse(displayName, "display name", item => item.Name == displayName); + var matchingItem = Parse(displayName, "display name", item => item.Name == displayName); return matchingItem; } - private static T parse(K value, string description, Func predicate) where T : Enumeration, new() + private static T Parse(K value, string description, Func predicate) where T : Enumeration, new() { var matchingItem = GetAll().FirstOrDefault(predicate); diff --git a/src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs similarity index 80% rename from src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs rename to src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs index 47a401aab..7ca49fa41 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; -namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure +namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency { public class ClientRequest { diff --git a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs index a8a02f8ca..d38c23e09 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency @@ -8,6 +6,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten public interface IRequestManager { Task ExistAsync(Guid id); + Task CreateRequestForCommandAsync(Guid id); } } diff --git a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs index 0ef005161..6b6a96579 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs @@ -1,8 +1,5 @@ -using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; -using Ordering.Domain.Exceptions; +using Ordering.Domain.Exceptions; using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency @@ -10,22 +7,25 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten public class RequestManager : IRequestManager { private readonly OrderingContext _context; - public RequestManager(OrderingContext ctx) + + public RequestManager(OrderingContext context) { - _context = ctx; + _context = context ?? throw new ArgumentNullException(nameof(context)); } public async Task ExistAsync(Guid id) { - var request = await _context.FindAsync(id); + var request = await _context. + FindAsync(id); + return request != null; } public async Task CreateRequestForCommandAsync(Guid id) - { - + { var exists = await ExistAsync(id); + var request = exists ? throw new OrderingDomainException($"Request with {id} already exists") : new ClientRequest() @@ -36,8 +36,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten }; _context.Add(request); + await _context.SaveChangesAsync(); } - } } diff --git a/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs b/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs index 99e94e086..9135188cf 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs @@ -6,13 +6,20 @@ using System.Threading.Tasks; namespace Ordering.Infrastructure { - public static class MediatorExtension + static class MediatorExtension { public static async Task DispatchDomainEventsAsync(this IMediator mediator, OrderingContext ctx) { - var domainEntities = ctx.ChangeTracker.Entries().Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); - var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList(); - domainEntities.ToList().ForEach(entity => entity.Entity.DomainEvents.Clear()); + var domainEntities = ctx.ChangeTracker + .Entries() + .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); + + var domainEvents = domainEntities + .SelectMany(x => x.Entity.DomainEvents) + .ToList(); + + domainEntities.ToList() + .ForEach(entity => entity.Entity.DomainEvents.Clear()); var tasks = domainEvents .Select(async (domainEvent) => { diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index 70bb8a51a..5b95ee23c 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; +using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; using Ordering.Infrastructure; using System; using System.Threading; @@ -34,7 +35,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure public OrderingContext(DbContextOptions options, IMediator mediator) : base(options) { - _mediator = mediator; + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index 5a2f0d8fe..e86c88c04 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -44,6 +44,11 @@ namespace Microsoft.eShopOnContainers.WebMVC // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webmvc"; + }); + services.AddMvc(); services.Configure(Configuration); diff --git a/src/Web/WebSPA/Client/modules/app.module.ts b/src/Web/WebSPA/Client/modules/app.module.ts index 997eacbfb..f05d466d8 100644 --- a/src/Web/WebSPA/Client/modules/app.module.ts +++ b/src/Web/WebSPA/Client/modules/app.module.ts @@ -2,7 +2,7 @@ import { NgModule, NgModuleFactoryLoader } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; // import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; -import { RouterModule } from '@angular/Router'; +import { RouterModule } from '@angular/router'; import { routing } from './app.routes'; import { AppService } from './app.service'; diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index 8a4a125c2..a572f7961 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -51,6 +51,11 @@ namespace eShopConContainers.WebSPA services.Configure(Configuration); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webspa"; + }); + services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); services.AddMvc() diff --git a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs index e81840bb3..8c7659862 100644 --- a/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs +++ b/test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs @@ -60,7 +60,7 @@ namespace UnitTest.Ordering.Application { //Arrange var fakeDynamicResult = new Object(); - _orderQueriesMock.Setup(x => x.GetOrders()) + _orderQueriesMock.Setup(x => x.GetOrdersAsync()) .Returns(Task.FromResult(fakeDynamicResult)); //Act @@ -77,7 +77,7 @@ namespace UnitTest.Ordering.Application //Arrange var fakeOrderId = 123; var fakeDynamicResult = new Object(); - _orderQueriesMock.Setup(x => x.GetOrder(It.IsAny())) + _orderQueriesMock.Setup(x => x.GetOrderAsync(It.IsAny())) .Returns(Task.FromResult(fakeDynamicResult)); //Act @@ -93,7 +93,7 @@ namespace UnitTest.Ordering.Application { //Arrange var fakeDynamicResult = new Object(); - _orderQueriesMock.Setup(x => x.GetCardTypes()) + _orderQueriesMock.Setup(x => x.GetCardTypesAsync()) .Returns(Task.FromResult(fakeDynamicResult)); //Act