Browse Source

Merge branch 'reviews/rabbitmq'

pull/223/head
Eduard Tomas 7 years ago
parent
commit
9d0e1e0e64
48 changed files with 567 additions and 324 deletions
  1. +0
    -3
      src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs
  2. +0
    -3
      src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs
  3. +0
    -2
      src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs
  4. +130
    -0
      src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs
  5. +110
    -68
      src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs
  6. +2
    -0
      src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj
  7. +16
    -0
      src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs
  8. +1
    -6
      src/Services/Basket/Basket.API/BasketSettings.cs
  9. +0
    -4
      src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs
  10. +2
    -3
      src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs
  11. +7
    -2
      src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs
  12. +0
    -4
      src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs
  13. +0
    -3
      src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs
  14. +2
    -4
      src/Services/Basket/Basket.API/Model/IBasketRepository.cs
  15. +23
    -15
      src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs
  16. +51
    -27
      src/Services/Basket/Basket.API/Startup.cs
  17. +9
    -0
      src/Services/Catalog/Catalog.API/CatalogSettings.cs
  18. +69
    -48
      src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs
  19. +1
    -5
      src/Services/Catalog/Catalog.API/Controllers/HomeController.cs
  20. +5
    -7
      src/Services/Catalog/Catalog.API/Controllers/PicController.cs
  21. +0
    -4
      src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs
  22. +0
    -3
      src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs
  23. +1
    -0
      src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs
  24. +0
    -3
      src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs
  25. +0
    -6
      src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs
  26. +0
    -5
      src/Services/Catalog/Catalog.API/Model/CatalogType.cs
  27. +32
    -18
      src/Services/Catalog/Catalog.API/Startup.cs
  28. +0
    -14
      src/Services/Catalog/Catalog.API/settings.cs
  29. +5
    -0
      src/Services/Identity/Identity.API/Startup.cs
  30. +4
    -2
      src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs
  31. +3
    -3
      src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs
  32. +3
    -3
      src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs
  33. +11
    -13
      src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs
  34. +1
    -5
      src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs
  35. +7
    -3
      src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs
  36. +24
    -9
      src/Services/Ordering/Ordering.API/Startup.cs
  37. +4
    -0
      src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs
  38. +2
    -2
      src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs
  39. +3
    -3
      src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs
  40. +1
    -3
      src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs
  41. +1
    -2
      src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs
  42. +10
    -10
      src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs
  43. +11
    -4
      src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs
  44. +2
    -1
      src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs
  45. +5
    -0
      src/Web/WebMVC/Startup.cs
  46. +1
    -1
      src/Web/WebSPA/Client/modules/app.module.ts
  47. +5
    -0
      src/Web/WebSPA/Startup.cs
  48. +3
    -3
      test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs

+ 0
- 3
src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs View File

@ -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
{


+ 0
- 3
src/BuildingBlocks/EventBus/EventBus/Abstractions/IIntegrationEventHandler.cs View File

@ -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


+ 0
- 2
src/BuildingBlocks/EventBus/EventBus/Events/IntegrationEvent.cs View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events
{


+ 130
- 0
src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs View File

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

+ 110
- 68
src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs View File

@ -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
}
}
}
}
}

+ 2
- 0
src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj View File

@ -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>


+ 16
- 0
src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs View File

@ -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
- 6
src/Services/Basket/Basket.API/BasketSettings.cs View File

@ -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
{


+ 0
- 4
src/Services/Basket/Basket.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs View File

@ -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
- 3
src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs View File

@ -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)


+ 7
- 2
src/Services/Basket/Basket.API/IntegrationEvents/EventHandling/ProductPriceChangedIntegrationEventHandler.cs View File

@ -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)


+ 0
- 4
src/Services/Basket/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs View File

@ -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
{


+ 0
- 3
src/Services/Basket/Basket.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs View File

@ -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
{


+ 2
- 4
src/Services/Basket/Basket.API/Model/IBasketRepository.cs View File

@ -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);
}


+ 23
- 15
src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs View File

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

+ 51
- 27
src/Services/Basket/Basket.API/Startup.cs View File

@ -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<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
- 0
src/Services/Catalog/Catalog.API/CatalogSettings.cs View File

@ -0,0 +1,9 @@
namespace Microsoft.eShopOnContainers.Services.Catalog.API
{
public class CatalogSettings
{
public string ExternalCatalogBaseUrl {get;set;}
public string EventBusConnection { get; set; }
}
}

+ 69
- 48
src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs View File

@ -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
- 5
src/Services/Catalog/Catalog.API/Controllers/HomeController.cs View File

@ -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


+ 5
- 7
src/Services/Catalog/Catalog.API/Controllers/PicController.cs View File

@ -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");
}
}
}

+ 0
- 4
src/Services/Catalog/Catalog.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs View File

@ -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
{


+ 0
- 3
src/Services/Catalog/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Catalog.API.Infrastructure.Exceptions
{


+ 1
- 0
src/Services/Catalog/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs View File

@ -30,6 +30,7 @@ namespace Catalog.API.IntegrationEvents
public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
{
_eventBus.Publish(evt);
await _eventLogService.MarkEventAsPublishedAsync(evt);
}


+ 0
- 3
src/Services/Catalog/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs View File

@ -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


+ 0
- 6
src/Services/Catalog/Catalog.API/Model/CatalogBrand.cs View File

@ -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; }


+ 0
- 5
src/Services/Catalog/Catalog.API/Model/CatalogType.cs View File

@ -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; }


+ 32
- 18
src/Services/Catalog/Catalog.API/Startup.cs View File

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

+ 0
- 14
src/Services/Catalog/Catalog.API/settings.cs View File

@ -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; }
}
}

+ 5
- 0
src/Services/Identity/Identity.API/Startup.cs View File

@ -57,6 +57,11 @@ namespace eShopOnContainers.Identity
services.Configure<AppSettings>(Configuration);
services.AddDataProtection(opts =>
{
opts.ApplicationDiscriminator = "eshop.identity";
});
services.AddMvc();
services.AddHealthChecks(checks =>


+ 4
- 2
src/Services/Ordering/Ordering.API/Application/Commands/IdentifierCommandHandler.cs View File

@ -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;
}
}


+ 3
- 3
src/Services/Ordering/Ordering.API/Application/Queries/IOrderQueries.cs View File

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

+ 3
- 3
src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs View File

@ -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))
{


+ 11
- 13
src/Services/Ordering/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs View File

@ -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
- 5
src/Services/Ordering/Ordering.API/Application/Validations/IdentifierCommandValidator.cs View File

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

+ 7
- 3
src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs View File

@ -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);
}


+ 24
- 9
src/Services/Ordering/Ordering.API/Startup.cs View File

@ -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


+ 4
- 0
src/Services/Ordering/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs View File

@ -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;
}
}


+ 2
- 2
src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs View File

@ -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);
}
}

+ 3
- 3
src/Services/Ordering/Ordering.Domain/SeedWork/Enumeration.cs View File

@ -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);


src/Services/Ordering/Ordering.Infrastructure/ClientRequest.cs → src/Services/Ordering/Ordering.Infrastructure/Idempotency/ClientRequest.cs View File

@ -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
- 2
src/Services/Ordering/Ordering.Infrastructure/Idempotency/IRequestManager.cs View File

@ -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);
}
}

+ 10
- 10
src/Services/Ordering/Ordering.Infrastructure/Idempotency/RequestManager.cs View File

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

+ 11
- 4
src/Services/Ordering/Ordering.Infrastructure/MediatorExtension.cs View File

@ -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) => {


+ 2
- 1
src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs View File

@ -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)


+ 5
- 0
src/Web/WebMVC/Startup.cs View File

@ -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);


+ 1
- 1
src/Web/WebSPA/Client/modules/app.module.ts View File

@ -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';


+ 5
- 0
src/Web/WebSPA/Startup.cs View File

@ -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()


+ 3
- 3
test/Services/UnitTest/Ordering/Application/OrdersWebApiTest.cs View File

@ -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…
Cancel
Save