Merge branch 'reviews/rabbitmq'
This commit is contained in:
		
						commit
						9d0e1e0e64
					
				| @ -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 | ||||
| { | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -1,6 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events | ||||
| { | ||||
|  | ||||
| @ -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<DefaultRabbitMQPersisterConnection> _logger; | ||||
| 
 | ||||
|         IConnection _connection; | ||||
|         bool _disposed; | ||||
| 
 | ||||
|         object sync_root = new object(); | ||||
| 
 | ||||
|         public DefaultRabbitMQPersisterConnection(IConnectionFactory connectionFactory,ILogger<DefaultRabbitMQPersisterConnection> 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<SocketException>() | ||||
|                     .Or<BrokerUnreachableException>() | ||||
|                     .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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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<string, List<IIntegrationEventHandler>>  _handlers; | ||||
|         private readonly List<Type> _eventTypes; | ||||
|         const string BROKER_NAME = "eshop_event_bus"; | ||||
| 
 | ||||
|         private IModel _model; | ||||
|         private IConnection _connection; | ||||
|         private readonly IRabbitMQPersisterConnection _persisterConnection; | ||||
|         private readonly ILogger<EventBusRabbitMQ> _logger; | ||||
| 
 | ||||
|         private readonly Dictionary<string, List<IIntegrationEventHandler>> _handlers | ||||
|             = new Dictionary<string, List<IIntegrationEventHandler>>(); | ||||
| 
 | ||||
|         private readonly List<Type> _eventTypes | ||||
|             = new List<Type>(); | ||||
| 
 | ||||
|         private IModel _consumerChannel; | ||||
|         private string _queueName; | ||||
|          | ||||
| 
 | ||||
|         public EventBusRabbitMQ(string connectionString) | ||||
|         public EventBusRabbitMQ(IRabbitMQPersisterConnection persisterConnection, ILogger<EventBusRabbitMQ> logger) | ||||
|         { | ||||
|             _connectionString = connectionString; | ||||
|             _handlers = new Dictionary<string, List<IIntegrationEventHandler>>(); | ||||
|             _eventTypes = new List<Type>(); | ||||
|             _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<BrokerUnreachableException>() | ||||
|                 .Or<SocketException>() | ||||
|                 .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<T>(IIntegrationEventHandler<T> 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<IIntegrationEventHandler>()); | ||||
|                 _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<IIntegrationEventHandler>()); | ||||
|                     _handlers[eventName].Add(handler); | ||||
|                     _eventTypes.Add(typeof(T)); | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|              | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         public void Unsubscribe<T>(IIntegrationEventHandler<T> 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 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -7,7 +7,9 @@ | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" /> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="9.0.1" /> | ||||
|     <PackageReference Include="Polly" Version="5.0.6" /> | ||||
|     <PackageReference Include="RabbitMQ.Client" Version="4.1.1" /> | ||||
|     <PackageReference Include="System.ValueTuple" Version="4.3.0" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
| @ -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 | ||||
|     { | ||||
|  | ||||
| @ -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 | ||||
| { | ||||
|  | ||||
| @ -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<OrderStartedIntegrationEvent> | ||||
|     { | ||||
|         private readonly IBasketRepository _repository; | ||||
| 
 | ||||
|         public OrderStartedIntegrationEventHandler(IBasketRepository repository) | ||||
|         { | ||||
|             _repository = repository; | ||||
|             _repository = repository ?? throw new ArgumentNullException(nameof(repository)); | ||||
|         } | ||||
| 
 | ||||
|         public async Task Handle(OrderStartedIntegrationEvent @event) | ||||
|  | ||||
| @ -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<ProductPriceChangedIntegrationEvent> | ||||
|     { | ||||
|         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) | ||||
|  | ||||
| @ -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 | ||||
| { | ||||
|  | ||||
| @ -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 | ||||
| { | ||||
|  | ||||
| @ -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<CustomerBasket> GetBasketAsync(string customerId); | ||||
|         Task<IEnumerable<string>> GetUsers(); | ||||
|         Task<IEnumerable<string>> GetUsersAsync(); | ||||
|         Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket); | ||||
|         Task<bool> DeleteBasketAsync(string id); | ||||
|     } | ||||
|  | ||||
| @ -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<IEnumerable<string>> GetUsers() | ||||
|         public async Task<IEnumerable<string>> 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()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|          | ||||
|      | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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.Logging; | ||||
| using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
| using StackExchange.Redis; | ||||
| using Microsoft.Extensions.Options; | ||||
| 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 Microsoft.Extensions.Logging; | ||||
| using Microsoft.Extensions.Options; | ||||
| using RabbitMQ.Client; | ||||
| using StackExchange.Redis; | ||||
| using System.Linq; | ||||
| using System.Net; | ||||
| 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<ConnectionMultiplexer>((sp) => { | ||||
|                 var config = sp.GetRequiredService<IOptionsSnapshot<BasketSettings>>().Value; | ||||
|                 var ips = Dns.GetHostAddressesAsync(config.ConnectionString).Result; | ||||
|             services.AddSingleton<ConnectionMultiplexer>(sp => | ||||
|             { | ||||
|                 var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value; | ||||
|                 var ips = Dns.GetHostAddressesAsync(settings.ConnectionString).Result; | ||||
| 
 | ||||
|                 return ConnectionMultiplexer.Connect(ips.First().ToString()); | ||||
|             }); | ||||
| 
 | ||||
| 
 | ||||
|             services.AddSingleton<IRabbitMQPersisterConnection>(sp => | ||||
|             { | ||||
|                 var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value; | ||||
|                 var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersisterConnection>>(); | ||||
|                 var factory = new ConnectionFactory() | ||||
|                 { | ||||
|                     HostName = settings.EventBusConnection | ||||
|                 }; | ||||
| 
 | ||||
|                 return new DefaultRabbitMQPersisterConnection(factory, logger); | ||||
|             }); | ||||
| 
 | ||||
|             services.AddSingleton<IEventBus, EventBusRabbitMQ>(); | ||||
| 
 | ||||
|             services.AddSwaggerGen(); | ||||
|             //var sch = new IdentitySecurityScheme(); | ||||
| 
 | ||||
|             services.ConfigureSwaggerGen(options => | ||||
|             { | ||||
|                 //options.AddSecurityDefinition("IdentityServer", sch); | ||||
|                 options.OperationFilter<AuthorizationHeaderParameterOperationFilter>(); | ||||
|                 options.DescribeAllEnumsAsStrings(); | ||||
|                 options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info() | ||||
| @ -95,9 +111,6 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API | ||||
|             services.AddTransient<IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>, ProductPriceChangedIntegrationEventHandler>(); | ||||
|             services.AddTransient<IIntegrationEventHandler<OrderStartedIntegrationEvent>, OrderStartedIntegrationEventHandler>(); | ||||
| 
 | ||||
|             var serviceProvider = services.BuildServiceProvider(); | ||||
|             var configuration = serviceProvider.GetRequiredService<IOptionsSnapshot<BasketSettings>>().Value; | ||||
|             services.AddSingleton<IEventBus>(provider => new EventBusRabbitMQ(configuration.EventBusConnection)); | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
| @ -119,11 +132,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API | ||||
|             app.UseSwagger() | ||||
|                 .UseSwaggerUi(); | ||||
| 
 | ||||
|             var catalogPriceHandler = app.ApplicationServices.GetService<IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>>(); | ||||
|             var orderStartedHandler = app.ApplicationServices.GetService<IIntegrationEventHandler<OrderStartedIntegrationEvent>>(); | ||||
|             var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); | ||||
|             eventBus.Subscribe<ProductPriceChangedIntegrationEvent>(catalogPriceHandler); | ||||
|             eventBus.Subscribe<OrderStartedIntegrationEvent>(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<IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>>(); | ||||
| 
 | ||||
|             var orderStartedHandler = app.ApplicationServices | ||||
|                 .GetService<IIntegrationEventHandler<OrderStartedIntegrationEvent>>(); | ||||
| 
 | ||||
|             var eventBus = app.ApplicationServices | ||||
|                 .GetRequiredService<IEventBus>(); | ||||
| 
 | ||||
|             eventBus.Subscribe<ProductPriceChangedIntegrationEvent>(catalogPriceHandler); | ||||
|             eventBus.Subscribe<OrderStartedIntegrationEvent>(orderStartedHandler); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										9
									
								
								src/Services/Catalog/Catalog.API/CatalogSettings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/Services/Catalog/Catalog.API/CatalogSettings.cs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Catalog.API | ||||
| { | ||||
|     public class CatalogSettings | ||||
|     { | ||||
|         public string ExternalCatalogBaseUrl {get;set;} | ||||
| 
 | ||||
|         public string EventBusConnection { get; set; } | ||||
|     } | ||||
| } | ||||
| @ -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> _settings; | ||||
|         private readonly CatalogSettings _settings; | ||||
|         private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; | ||||
| 
 | ||||
|         public CatalogController(CatalogContext Context, IOptionsSnapshot<Settings> settings, ICatalogIntegrationEventService catalogIntegrationEventService) | ||||
|         public CatalogController(CatalogContext context, IOptionsSnapshot<CatalogSettings> 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<CatalogItem>( | ||||
|                 pageIndex, pageSize, totalItems, itemsOnPage);            | ||||
|                 pageIndex, pageSize, totalItems, itemsOnPage); | ||||
| 
 | ||||
|             return Ok(model); | ||||
|         } | ||||
| 
 | ||||
|         [HttpGet] | ||||
|         [Route("items/{id:int}")] | ||||
|         public async Task<IActionResult> 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<CatalogItem>( | ||||
|                 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<CatalogItem>( | ||||
|                 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<IActionResult> 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<IActionResult> 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<CatalogItem> ComposePicUri(List<CatalogItem> items) { | ||||
|             var baseUri = _settings.Value.ExternalCatalogBaseUrl; | ||||
|         private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items) | ||||
|         { | ||||
|             var baseUri = _settings.ExternalCatalogBaseUrl; | ||||
| 
 | ||||
|             items.ForEach(x => | ||||
|             { | ||||
|                 x.PictureUri = x.PictureUri.Replace("http://externalcatalogbaseurltobereplaced", baseUri); | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -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"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| { | ||||
|  | ||||
| @ -1,7 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| 
 | ||||
| namespace Catalog.API.Infrastructure.Exceptions | ||||
| { | ||||
|  | ||||
| @ -30,6 +30,7 @@ namespace Catalog.API.IntegrationEvents | ||||
|         public async Task PublishThroughEventBusAsync(IntegrationEvent evt) | ||||
|         { | ||||
|             _eventBus.Publish(evt); | ||||
| 
 | ||||
|             await _eventLogService.MarkEventAsPublishedAsync(evt); | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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; } | ||||
|  | ||||
| @ -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; } | ||||
|  | ||||
| @ -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<Settings>(Configuration); | ||||
|             services.Configure<CatalogSettings>(Configuration); | ||||
| 
 | ||||
|             // Add framework services. | ||||
|             services.AddSwaggerGen(); | ||||
| @ -99,11 +99,23 @@ | ||||
|             }); | ||||
| 
 | ||||
|             services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( | ||||
|                 sp => (DbConnection c) => new IntegrationEventLogService(c));             | ||||
|             var serviceProvider = services.BuildServiceProvider(); | ||||
|             var configuration = serviceProvider.GetRequiredService<IOptionsSnapshot<Settings>>().Value; | ||||
|                 sp => (DbConnection c) => new IntegrationEventLogService(c)); | ||||
| 
 | ||||
|             services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); | ||||
|             services.AddSingleton<IEventBus>(new EventBusRabbitMQ(configuration.EventBusConnection));             | ||||
| 
 | ||||
|             services.AddSingleton<IRabbitMQPersisterConnection>(sp => | ||||
|             { | ||||
|                 var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | ||||
|                 var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersisterConnection>>(); | ||||
|                 var factory = new ConnectionFactory() | ||||
|                 { | ||||
|                     HostName = settings.EventBusConnection | ||||
|                 }; | ||||
| 
 | ||||
|                 return new DefaultRabbitMQPersisterConnection(factory, logger); | ||||
|             }); | ||||
| 
 | ||||
|             services.AddSingleton<IEventBus, EventBusRabbitMQ>(); | ||||
|         } | ||||
| 
 | ||||
|         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<IntegrationEventLogContext>() | ||||
|                 .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(); | ||||
|             } | ||||
|              | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; } | ||||
|     } | ||||
| } | ||||
| @ -57,6 +57,11 @@ namespace eShopOnContainers.Identity | ||||
| 
 | ||||
|             services.Configure<AppSettings>(Configuration); | ||||
| 
 | ||||
|             services.AddDataProtection(opts => | ||||
|             { | ||||
|                 opts.ApplicationDiscriminator = "eshop.identity"; | ||||
|             }); | ||||
| 
 | ||||
|             services.AddMvc(); | ||||
| 
 | ||||
|             services.AddHealthChecks(checks => | ||||
|  | ||||
| @ -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<T>(message.Id); | ||||
| 
 | ||||
|                 var result = await _mediator.SendAsync(message.Command); | ||||
|                  | ||||
|                 return result; | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -4,10 +4,10 @@ | ||||
| 
 | ||||
|     public interface IOrderQueries | ||||
|     { | ||||
|         Task<dynamic> GetOrder(int id); | ||||
|         Task<dynamic> GetOrderAsync(int id); | ||||
| 
 | ||||
|         Task<dynamic> GetOrders(); | ||||
|         Task<dynamic> GetOrdersAsync(); | ||||
| 
 | ||||
|         Task<dynamic> GetCardTypes(); | ||||
|         Task<dynamic> GetCardTypesAsync(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -19,7 +19,7 @@ | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         public async Task<dynamic> GetOrder(int id) | ||||
|         public async Task<dynamic> GetOrderAsync(int id) | ||||
|         { | ||||
|             using (var connection = new SqlConnection(_connectionString)) | ||||
|             { | ||||
| @ -44,7 +44,7 @@ | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public async Task<dynamic> GetOrders() | ||||
|         public async Task<dynamic> GetOrdersAsync() | ||||
|         { | ||||
|             using (var connection = new SqlConnection(_connectionString)) | ||||
|             { | ||||
| @ -58,7 +58,7 @@ | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public async Task<dynamic> GetCardTypes() | ||||
|         public async Task<dynamic> GetCardTypesAsync() | ||||
|         { | ||||
|             using (var connection = new SqlConnection(_connectionString)) | ||||
|             { | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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();     | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<IActionResult> 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<IActionResult> GetCardTypes() | ||||
|         { | ||||
|             var cardTypes = await _orderQueries.GetCardTypes(); | ||||
|             var cardTypes = await _orderQueries | ||||
|                 .GetCardTypesAsync(); | ||||
| 
 | ||||
|             return Ok(cardTypes); | ||||
|         }         | ||||
|  | ||||
| @ -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<OrderingContext>(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<IOrderingIntegrationEventService, OrderingIntegrationEventService>(); | ||||
|             services.AddSingleton<IEventBus>(new EventBusRabbitMQ(Configuration["EventBusConnection"])); | ||||
| 
 | ||||
|             services.AddSingleton<IRabbitMQPersisterConnection>(sp => | ||||
|             { | ||||
|                 var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersisterConnection>>(); | ||||
| 
 | ||||
|                 var factory = new ConnectionFactory() | ||||
|                 { | ||||
|                     HostName = Configuration["EventBusConnection"] | ||||
|                 }; | ||||
| 
 | ||||
|                 return new DefaultRabbitMQPersisterConnection(factory, logger); | ||||
|             }); | ||||
| 
 | ||||
|             services.AddSingleton<IEventBus, EventBusRabbitMQ>(); | ||||
| 
 | ||||
|             services.AddOptions(); | ||||
| 
 | ||||
|             //configure autofac | ||||
|  | ||||
| @ -16,6 +16,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B | ||||
|         public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly(); | ||||
| 
 | ||||
|         protected Buyer() { | ||||
| 
 | ||||
|             _paymentMethods = new List<PaymentMethod>(); | ||||
|         } | ||||
| 
 | ||||
| @ -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; | ||||
|             } | ||||
|         }        | ||||
|  | ||||
| @ -9,9 +9,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O | ||||
|     public interface IOrderRepository : IRepository<Order> | ||||
|     { | ||||
|         Order Add(Order order); | ||||
|          | ||||
|         void Update(Order order); | ||||
| 
 | ||||
|         Task<Order> GetAsync(int orderId); | ||||
| 
 | ||||
|         void Update(Order order); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -71,17 +71,17 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork | ||||
| 
 | ||||
|         public static T FromValue<T>(int value) where T : Enumeration, new() | ||||
|         { | ||||
|             var matchingItem = parse<T, int>(value, "value", item => item.Id == value); | ||||
|             var matchingItem = Parse<T, int>(value, "value", item => item.Id == value); | ||||
|             return matchingItem; | ||||
|         } | ||||
| 
 | ||||
|         public static T FromDisplayName<T>(string displayName) where T : Enumeration, new() | ||||
|         { | ||||
|             var matchingItem = parse<T, string>(displayName, "display name", item => item.Name == displayName); | ||||
|             var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName); | ||||
|             return matchingItem; | ||||
|         } | ||||
| 
 | ||||
|         private static T parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration, new() | ||||
|         private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration, new() | ||||
|         { | ||||
|             var matchingItem = GetAll<T>().FirstOrDefault(predicate); | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|     { | ||||
| @ -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<bool> ExistAsync(Guid id); | ||||
| 
 | ||||
|         Task CreateRequestForCommandAsync<T>(Guid id); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<bool> ExistAsync(Guid id) | ||||
|         { | ||||
|             var request = await _context.FindAsync<ClientRequest>(id); | ||||
|             var request = await _context. | ||||
|                 FindAsync<ClientRequest>(id); | ||||
| 
 | ||||
|             return request != null; | ||||
|         } | ||||
| 
 | ||||
|         public async Task CreateRequestForCommandAsync<T>(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(); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<Entity>().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<Entity>() | ||||
|                 .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) => { | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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<AppSettings>(Configuration); | ||||
| 
 | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
| @ -51,6 +51,11 @@ namespace eShopConContainers.WebSPA | ||||
| 
 | ||||
|             services.Configure<AppSettings>(Configuration); | ||||
| 
 | ||||
|             services.AddDataProtection(opts => | ||||
|             { | ||||
|                 opts.ApplicationDiscriminator = "eshop.webspa"; | ||||
|             }); | ||||
| 
 | ||||
|             services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); | ||||
| 
 | ||||
|             services.AddMvc() | ||||
|  | ||||
| @ -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<int>())) | ||||
|             _orderQueriesMock.Setup(x => x.GetOrderAsync(It.IsAny<int>())) | ||||
|                 .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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user