Browse Source

Merge branch 'master' into xamarin

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

+ 7
- 6
README.md View File

@ -1,12 +1,13 @@
# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**ALPHA state** - VS 2017 and CLI environments compatible)
# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**BETA state** - Visual Studio 2017 and CLI environments compatible)
Sample .NET Core reference application, powered by Microsoft, based on a simplified microservices architecture and Docker containers. <p> Sample .NET Core reference application, powered by Microsoft, based on a simplified microservices architecture and Docker containers. <p>
> ### DISCLAIMER > ### DISCLAIMER
> **IMPORTANT:** The current state of this sample application is **ALPHA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.**
> **IMPORTANT:** The current state of this sample application is **BETA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.**
> >
> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. However, this reference application it is not trying to solve all the problems in a large and mission-critical distributed system, it is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core.
> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. The chosen domain is an eShop/eCommerce but simply because it is a well-know domain by most people/developers.
However, this sample application should not be considered as an "eCommerce reference model", at all. The implemented business domain might not be ideal from an eCommerce business point of view. It is neither trying to solve all the problems in a large, scalable and mission-critical distributed system. It is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core.
> <p>For example, the next step (still not covered in eShopOnContainers) after understanding Docker containers and microservices development with .NET Core, is to select a microservice cluster/orchestrator like Docker Swarm, Kubernetes or DC/OS (in Azure Container Service) or Azure Service Fabric which in most of the cases will require additional partial changes to your application's configuration (although the present architecture should work on most orchestrators with small changes). > <p>For example, the next step (still not covered in eShopOnContainers) after understanding Docker containers and microservices development with .NET Core, is to select a microservice cluster/orchestrator like Docker Swarm, Kubernetes or DC/OS (in Azure Container Service) or Azure Service Fabric which in most of the cases will require additional partial changes to your application's configuration (although the present architecture should work on most orchestrators with small changes).
> Or moving your databases to HA cloud services, or implementing your EventBus with Azure Service Bus or any other production ready Service Bus in the market.
> Additional steps would be to move your databases to HA cloud services, or to implement your EventBus with Azure Service Bus or any other production ready Service Bus in the market.
> <p> In the future we might fork this project and make multiple versions targeting specific microservice cluster/orchestrators plus using additional cloud infrastructure. <p> > <p> In the future we might fork this project and make multiple versions targeting specific microservice cluster/orchestrators plus using additional cloud infrastructure. <p>
> <img src="img/exploring-to-production-ready.png"> > <img src="img/exploring-to-production-ready.png">
> Read the planned <a href='https://github.com/dotnet/eShopOnContainers/wiki/01.-Roadmap-and-Milestones-for-future-releases'>Roadmap and Milestones for future releases of eShopOnContainers</a> within the Wiki for further info about possible new implementations and provide feedback at the <a href='https://github.com/dotnet/eShopOnContainers/issues'>ISSUES section</a> if you'd like to see any specific scenario implemented or improved. Also, feel free to discuss on any current issue. > Read the planned <a href='https://github.com/dotnet/eShopOnContainers/wiki/01.-Roadmap-and-Milestones-for-future-releases'>Roadmap and Milestones for future releases of eShopOnContainers</a> within the Wiki for further info about possible new implementations and provide feedback at the <a href='https://github.com/dotnet/eShopOnContainers/issues'>ISSUES section</a> if you'd like to see any specific scenario implemented or improved. Also, feel free to discuss on any current issue.
@ -32,7 +33,7 @@ Additional miroservice styles with other frameworks and No-SQL databases will be
> <p> However, in a real production environment it is recommended to have your databases (SQL Server and Redis, in this case) in HA (High Available) services like Azure SQL Database, Redis as a service or any other clustering system. If you want to change to a production configuration, you'll just need to change the connection strings once you have set up the servers in a HA cloud or on-premises. > <p> However, in a real production environment it is recommended to have your databases (SQL Server and Redis, in this case) in HA (High Available) services like Azure SQL Database, Redis as a service or any other clustering system. If you want to change to a production configuration, you'll just need to change the connection strings once you have set up the servers in a HA cloud or on-premises.
## Related documentation and guidance ## Related documentation and guidance
While developing this reference application, we are creating a reference Guide/eBook named <b>"Architecting and Developing Containerized and Microservice based .NET Applications"</b> which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers.
While developing this reference application, we've been creating a reference <b>Guide/eBook</b> focusing on <b>architecting and developing containerized and microservice based .NET Applications</b> (download link available below) which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers.
<p> <p>
There are also additional eBooks focusing on Containers/Docker lifecycle (DevOps, CI/CD, etc.) with Microsoft Tools, already published plus an additional eBook focusing on Enterprise Apps Patterns with Xamarin.Forms. There are also additional eBooks focusing on Containers/Docker lifecycle (DevOps, CI/CD, etc.) with Microsoft Tools, already published plus an additional eBook focusing on Enterprise Apps Patterns with Xamarin.Forms.
You can download them and start reviewing these Guides/eBooks here: You can download them and start reviewing these Guides/eBooks here:
@ -43,7 +44,7 @@ You can download them and start reviewing these Guides/eBooks here:
| <a href='https://aka.ms/microservicesebook'><img src="img/ebook_arch_dev_microservices_containers_cover.png"> </a> | <a href='https://aka.ms/dockerlifecycleebook'> <img src="img/ebook_containers_lifecycle.png"> </a> | <a href='https://aka.ms/xamarinpatternsebook'> <img src="img/xamarin-enterprise-patterns-ebook-cover-small.png"> </a> | | <a href='https://aka.ms/microservicesebook'><img src="img/ebook_arch_dev_microservices_containers_cover.png"> </a> | <a href='https://aka.ms/dockerlifecycleebook'> <img src="img/ebook_containers_lifecycle.png"> </a> | <a href='https://aka.ms/xamarinpatternsebook'> <img src="img/xamarin-enterprise-patterns-ebook-cover-small.png"> </a> |
| <sup> <a href='https://aka.ms/microservicesebook'>**Download** (Early DRAFT, still work in progress)</a> </sup> | <sup> <a href='https://aka.ms/dockerlifecycleebook'>**Download** (First Edition from late 2016) </a> </sup> | <sup> <a href='https://aka.ms/xamarinpatternsebook'>**Download** (Early DRAFT, still work in progress) </a> </sup> | | <sup> <a href='https://aka.ms/microservicesebook'>**Download** (Early DRAFT, still work in progress)</a> </sup> | <sup> <a href='https://aka.ms/dockerlifecycleebook'>**Download** (First Edition from late 2016) </a> </sup> | <sup> <a href='https://aka.ms/xamarinpatternsebook'>**Download** (Early DRAFT, still work in progress) </a> </sup> |
Send feedback to [cesardl@microsoft.com](cesardl@microsoft.com)
Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)
<p> <p>
However, we encourage to download and review the "Architecting & Developing eBook" because the architectural styles and architectural patterns and technologies explained in the guidance are using this reference application when explaining many pattern implementations, so you'll understand much better the context, design and decisions taken in the current architecture and internal designs. However, we encourage to download and review the "Architecting & Developing eBook" because the architectural styles and architectural patterns and technologies explained in the guidance are using this reference application when explaining many pattern implementations, so you'll understand much better the context, design and decisions taken in the current architecture and internal designs.


BIN
img/ebook_arch_dev_microservices_containers_cover.png View File

Before After
Width: 257  |  Height: 336  |  Size: 31 KiB Width: 260  |  Height: 336  |  Size: 38 KiB

BIN
img/ebook_arch_dev_microservices_containers_cover_LARGE.png View File

Before After
Width: 1139  |  Height: 1469  |  Size: 242 KiB

BIN
img/ebook_arch_dev_microservices_containers_cover_OLD.png View File

Before After
Width: 257  |  Height: 336  |  Size: 31 KiB

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

@ -1,7 +1,4 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions 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 Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions 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;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events 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.eShopOnContainers.BuildingBlocks.EventBus.Events;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Polly;
using Polly.Retry;
using RabbitMQ.Client; using RabbitMQ.Client;
using RabbitMQ.Client.Events; using RabbitMQ.Client.Events;
using RabbitMQ.Client.Exceptions;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Sockets;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,68 +19,98 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
{ {
public class EventBusRabbitMQ : IEventBus, IDisposable 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; 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) 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"); type: "direct");
string message = JsonConvert.SerializeObject(@event);
var message = JsonConvert.SerializeObject(@event);
var body = Encoding.UTF8.GetBytes(message); var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: _brokerName,
policy.Execute(() =>
{
channel.BasicPublish(exchange: BROKER_NAME,
routingKey: eventName, routingKey: eventName,
basicProperties: null, basicProperties: null,
body: body);
body: body);
});
} }
} }
public void Subscribe<T>(IIntegrationEventHandler<T> handler) where T : IntegrationEvent public void Subscribe<T>(IIntegrationEventHandler<T> handler) where T : IntegrationEvent
{ {
var eventName = typeof(T).Name; var eventName = typeof(T).Name;
if (_handlers.ContainsKey(eventName))
if (_handlers.ContainsKey(eventName))
{ {
_handlers[eventName].Add(handler); _handlers[eventName].Add(handler);
} }
else 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 public void Unsubscribe<T>(IIntegrationEventHandler<T> handler) where T : IntegrationEvent
{ {
var eventName = typeof(T).Name; var eventName = typeof(T).Name;
if (_handlers.ContainsKey(eventName) && _handlers[eventName].Contains(handler)) if (_handlers.ContainsKey(eventName) && _handlers[eventName].Contains(handler))
{ {
_handlers[eventName].Remove(handler); _handlers[eventName].Remove(handler);
@ -85,56 +118,59 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
if (_handlers[eventName].Count == 0) if (_handlers[eventName].Count == 0)
{ {
_handlers.Remove(eventName); _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() public void Dispose()
{ {
if (_consumerChannel != null)
{
_consumerChannel.Dispose();
}
_handlers.Clear(); _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); var consumer = new EventingBasicConsumer(channel);
consumer.Received += async (model, ea) => consumer.Received += async (model, ea) =>
@ -144,11 +180,18 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
await ProcessEvent(eventName, message); await ProcessEvent(eventName, message);
}; };
channel.BasicConsume(queue: _queueName, channel.BasicConsume(queue: _queueName,
noAck: true, noAck: true,
consumer: consumer); consumer: consumer);
return (channel, con);
channel.CallbackException += (sender, ea) =>
{
_consumerChannel.Dispose();
_consumerChannel = CreateConsumerChannel();
};
return channel;
} }
private async Task ProcessEvent(string eventName, string message) private async Task ProcessEvent(string eventName, string message)
@ -156,7 +199,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
if (_handlers.ContainsKey(eventName)) if (_handlers.ContainsKey(eventName))
{ {
Type eventType = _eventTypes.Single(t => t.Name == 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 concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
var handlers = _handlers[eventName]; 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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.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="RabbitMQ.Client" Version="4.1.1" />
<PackageReference Include="System.ValueTuple" Version="4.3.0" /> <PackageReference Include="System.ValueTuple" Version="4.3.0" />
</ItemGroup> </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 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.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Basket.API.Infrastructure.ActionResults 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.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.eShopOnContainers.Services.Basket.API.Model;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Basket.API.IntegrationEvents.EventHandling namespace Basket.API.IntegrationEvents.EventHandling
@ -11,9 +9,10 @@ namespace Basket.API.IntegrationEvents.EventHandling
public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler<OrderStartedIntegrationEvent> public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler<OrderStartedIntegrationEvent>
{ {
private readonly IBasketRepository _repository; private readonly IBasketRepository _repository;
public OrderStartedIntegrationEventHandler(IBasketRepository repository) public OrderStartedIntegrationEventHandler(IBasketRepository repository)
{ {
_repository = repository;
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
} }
public async Task Handle(OrderStartedIntegrationEvent @event) 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.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.eShopOnContainers.Services.Basket.API.Model;
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,17 +10,20 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Even
public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent> public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
{ {
private readonly IBasketRepository _repository; private readonly IBasketRepository _repository;
public ProductPriceChangedIntegrationEventHandler(IBasketRepository repository) public ProductPriceChangedIntegrationEventHandler(IBasketRepository repository)
{ {
_repository = repository;
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
} }
public async Task Handle(ProductPriceChangedIntegrationEvent @event) public async Task Handle(ProductPriceChangedIntegrationEvent @event)
{ {
var userIds = await _repository.GetUsers();
var userIds = await _repository.GetUsersAsync();
foreach (var id in userIds) foreach (var id in userIds)
{ {
var basket = await _repository.GetBasketAsync(id); var basket = await _repository.GetBasketAsync(id);
await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, @event.OldPrice, basket); 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) private async Task UpdatePriceInBasketItems(int productId, decimal newPrice, decimal oldPrice, CustomerBasket basket)
{ {
var itemsToUpdate = basket?.Items?.Where(x => int.Parse(x.ProductId) == productId).ToList(); var itemsToUpdate = basket?.Items?.Where(x => int.Parse(x.ProductId) == productId).ToList();
if (itemsToUpdate != null) if (itemsToUpdate != null)
{ {
foreach (var item in itemsToUpdate) 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 Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Basket.API.IntegrationEvents.Events 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 Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events 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; using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
@ -8,7 +6,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
public interface IBasketRepository public interface IBasketRepository
{ {
Task<CustomerBasket> GetBasketAsync(string customerId); Task<CustomerBasket> GetBasketAsync(string customerId);
Task<IEnumerable<string>> GetUsers();
Task<IEnumerable<string>> GetUsersAsync();
Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket); Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket);
Task<bool> DeleteBasketAsync(string id); 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 Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
{ {
@ -31,7 +30,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
return await database.KeyDeleteAsync(id.ToString()); return await database.KeyDeleteAsync(id.ToString());
} }
public async Task<IEnumerable<string>> GetUsers()
public async Task<IEnumerable<string>> GetUsersAsync()
{ {
var server = await GetServer(); 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)); var created = await database.StringSetAsync(basket.BuyerId, JsonConvert.SerializeObject(basket));
if (!created) if (!created)
{ {
_logger.LogInformation("Problem persisting the item");
_logger.LogInformation("Problem occur persisting the item.");
return null; return null;
} }
_logger.LogInformation("basket item persisted succesfully");
_logger.LogInformation("Basket item persisted succesfully.");
return await GetBasketAsync(basket.BuyerId); return await GetBasketAsync(basket.BuyerId);
} }
@ -94,13 +94,21 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
private async Task ConnectToRedisAsync() 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.Builder;
using Microsoft.AspNetCore.Hosting; 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.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.HealthChecks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
using StackExchange.Redis;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using StackExchange.Redis;
using System.Linq;
using System.Net; 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 System.Threading.Tasks;
using Basket.API.Infrastructure.Filters;
using Basket.API.IntegrationEvents.Events;
using Basket.API.IntegrationEvents.EventHandling;
namespace Microsoft.eShopOnContainers.Services.Basket.API 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 //and then creating the connection it seems reasonable to move
//that cost to startup instead of having the first request pay the //that cost to startup instead of having the first request pay the
//penalty. //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()); 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(); services.AddSwaggerGen();
//var sch = new IdentitySecurityScheme();
services.ConfigureSwaggerGen(options => services.ConfigureSwaggerGen(options =>
{ {
//options.AddSecurityDefinition("IdentityServer", sch);
options.OperationFilter<AuthorizationHeaderParameterOperationFilter>(); options.OperationFilter<AuthorizationHeaderParameterOperationFilter>();
options.DescribeAllEnumsAsStrings(); options.DescribeAllEnumsAsStrings();
options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info() 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<ProductPriceChangedIntegrationEvent>, ProductPriceChangedIntegrationEventHandler>();
services.AddTransient<IIntegrationEventHandler<OrderStartedIntegrationEvent>, OrderStartedIntegrationEventHandler>(); 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() app.UseSwagger()
.UseSwaggerUi(); .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", ScopeName = "basket",
RequireHttpsMetadata = false 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;
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.Infrastructure;
using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.Services.Catalog.API.Model; using Microsoft.eShopOnContainers.Services.Catalog.API.Model;
@ -11,12 +8,8 @@ using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Common;
using System.Linq; using System.Linq;
using System.Threading.Tasks; 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 namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
{ {
@ -24,16 +17,16 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
public class CatalogController : ControllerBase public class CatalogController : ControllerBase
{ {
private readonly CatalogContext _catalogContext; private readonly CatalogContext _catalogContext;
private readonly IOptionsSnapshot<Settings> _settings;
private readonly CatalogSettings _settings;
private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; 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] // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10]
@ -46,19 +39,37 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
.LongCountAsync(); .LongCountAsync();
var itemsOnPage = await _catalogContext.CatalogItems var itemsOnPage = await _catalogContext.CatalogItems
.OrderBy(c=>c.Name)
.OrderBy(c => c.Name)
.Skip(pageSize * pageIndex) .Skip(pageSize * pageIndex)
.Take(pageSize) .Take(pageSize)
.ToListAsync(); .ToListAsync();
itemsOnPage = ComposePicUri(itemsOnPage);
itemsOnPage = ChangeUriPlaceholder(itemsOnPage);
var model = new PaginatedItemsViewModel<CatalogItem>( var model = new PaginatedItemsViewModel<CatalogItem>(
pageIndex, pageSize, totalItems, itemsOnPage);
pageIndex, pageSize, totalItems, itemsOnPage);
return Ok(model); 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] // GET api/v1/[controller]/items/withname/samplename[?pageSize=3&pageIndex=10]
[HttpGet] [HttpGet]
[Route("[action]/withname/{name:minlength(1)}")] [Route("[action]/withname/{name:minlength(1)}")]
@ -75,7 +86,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
.Take(pageSize) .Take(pageSize)
.ToListAsync(); .ToListAsync();
itemsOnPage = ComposePicUri(itemsOnPage);
itemsOnPage = ChangeUriPlaceholder(itemsOnPage);
var model = new PaginatedItemsViewModel<CatalogItem>( var model = new PaginatedItemsViewModel<CatalogItem>(
pageIndex, pageSize, totalItems, itemsOnPage); pageIndex, pageSize, totalItems, itemsOnPage);
@ -108,7 +119,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
.Take(pageSize) .Take(pageSize)
.ToListAsync(); .ToListAsync();
itemsOnPage = ComposePicUri(itemsOnPage);
itemsOnPage = ChangeUriPlaceholder(itemsOnPage);
var model = new PaginatedItemsViewModel<CatalogItem>( var model = new PaginatedItemsViewModel<CatalogItem>(
pageIndex, pageSize, totalItems, itemsOnPage); pageIndex, pageSize, totalItems, itemsOnPage);
@ -138,16 +149,23 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
return Ok(items); 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) 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 oldPrice = catalogItem.Price;
var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price;
// Update current product // Update current product
catalogItem = productToUpdate; catalogItem = productToUpdate;
_catalogContext.CatalogItems.Update(catalogItem); _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 //Create Integration Event to be published through the Event Bus
var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice);
// Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction
await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent);
// Publish through the Event Bus and mark the saved event as published // Publish through the Event Bus and mark the saved event as published
await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent);
} }
else // Save updated product else // Save updated product
{ {
await _catalogContext.SaveChangesAsync(); 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] [HttpPost]
public async Task<IActionResult> CreateProduct([FromBody]CatalogItem product) 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(); await _catalogContext.SaveChangesAsync();
return Ok();
return CreatedAtAction(nameof(GetItemById), new { id = item.Id }, null);
} }
//DELETE api/v1/[controller]/id //DELETE api/v1/[controller]/id
@ -202,16 +220,19 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
if (product == null) if (product == null)
{ {
return NotFound(); return NotFound();
}
}
_catalogContext.CatalogItems.Remove(product); _catalogContext.CatalogItems.Remove(product);
await _catalogContext.SaveChangesAsync(); 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 => items.ForEach(x =>
{ {
x.PictureUri = x.PictureUri.Replace("http://externalcatalogbaseurltobereplaced", baseUri); 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 // 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 Microsoft.AspNetCore.Mvc;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Hosting;
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 // 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 webRoot = _env.WebRootPath;
var path = Path.Combine(webRoot, id + ".png"); 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.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Catalog.API.Infrastructure.ActionResults 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;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Catalog.API.Infrastructure.Exceptions 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) public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
{ {
_eventBus.Publish(evt); _eventBus.Publish(evt);
await _eventLogService.MarkEventAsPublishedAsync(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 Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Catalog.API.IntegrationEvents 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 namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model
{ {
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class CatalogBrand public class CatalogBrand
{ {
public int Id { get; set; } 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 namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model
{ {
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class CatalogType public class CatalogType
{ {
public int Id { get; set; } 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 namespace Microsoft.eShopOnContainers.Services.Catalog.API
{ {
using global::Catalog.API.Infrastructure.Filters; using global::Catalog.API.Infrastructure.Filters;
using global::Catalog.API.IntegrationEvents;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -15,13 +16,11 @@
using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.HealthChecks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using System; using System;
using System.Data.SqlClient;
using System.IO;
using System.Data.Common; using System.Data.Common;
using System.Data.SqlClient;
using System.Reflection; using System.Reflection;
using global::Catalog.API.IntegrationEvents;
using System.Threading.Tasks;
public class Startup public class Startup
{ {
@ -47,7 +46,7 @@
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// Add framework services. // Add framework services.
services.AddHealthChecks(checks => services.AddHealthChecks(checks =>
{ {
checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"]); checks.AddSqlCheck("CatalogDb", Configuration["ConnectionString"]);
@ -62,18 +61,19 @@
{ {
options.UseSqlServer(Configuration["ConnectionString"], options.UseSqlServer(Configuration["ConnectionString"],
sqlServerOptionsAction: sqlOptions => sqlServerOptionsAction: sqlOptions =>
{
{
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
}); });
// Changing default behavior when client evaluation occurs to throw. // Changing default behavior when client evaluation occurs to throw.
// Default in EF Core would be to log a warning when client evaluation is performed. // Default in EF Core would be to log a warning when client evaluation is performed.
options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
//Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval //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. // Add framework services.
services.AddSwaggerGen(); services.AddSwaggerGen();
@ -99,11 +99,23 @@
}); });
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>( 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.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) public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
@ -124,25 +136,28 @@
.ApplicationServices.GetService(typeof(CatalogContext)); .ApplicationServices.GetService(typeof(CatalogContext));
WaitForSqlAvailability(context, loggerFactory); WaitForSqlAvailability(context, loggerFactory);
//Seed Data //Seed Data
CatalogContextSeed.SeedAsync(app, loggerFactory) CatalogContextSeed.SeedAsync(app, loggerFactory)
.Wait();
.Wait();
var integrationEventLogContext = new IntegrationEventLogContext( var integrationEventLogContext = new IntegrationEventLogContext(
new DbContextOptionsBuilder<IntegrationEventLogContext>() new DbContextOptionsBuilder<IntegrationEventLogContext>()
.UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API")) .UseSqlServer(Configuration["ConnectionString"], b => b.MigrationsAssembly("Catalog.API"))
.Options); .Options);
integrationEventLogContext.Database.Migrate(); integrationEventLogContext.Database.Migrate();
} }
private void WaitForSqlAvailability(CatalogContext ctx, ILoggerFactory loggerFactory, int? retry = 0) private void WaitForSqlAvailability(CatalogContext ctx, ILoggerFactory loggerFactory, int? retry = 0)
{ {
int retryForAvailability = retry.Value;
int retryForAvailability = retry.Value;
try try
{ {
ctx.Database.OpenConnection(); ctx.Database.OpenConnection();
} }
catch(SqlException ex)
catch (SqlException ex)
{ {
if (retryForAvailability < 10) if (retryForAvailability < 10)
{ {
@ -152,11 +167,10 @@
WaitForSqlAvailability(ctx, loggerFactory, retryForAvailability); 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.Configure<AppSettings>(Configuration);
services.AddDataProtection(opts =>
{
opts.ApplicationDiscriminator = "eshop.identity";
});
services.AddMvc(); services.AddMvc();
services.AddHealthChecks(checks => 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(); return CreateResultForDuplicateRequest();
} }
else else
{
var result = await _mediator.SendAsync(message.Command);
{
await _requestManager.CreateRequestForCommandAsync<T>(message.Id); await _requestManager.CreateRequestForCommandAsync<T>(message.Id);
var result = await _mediator.SendAsync(message.Command);
return result; return result;
} }
} }


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

@ -4,10 +4,10 @@
public interface IOrderQueries 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)) 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)) 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)) 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 FluentValidation;
using MediatR;
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand;
namespace Ordering.API.Application.Validations namespace Ordering.API.Application.Validations
@ -13,17 +11,17 @@ namespace Ordering.API.Application.Validations
{ {
public CreateOrderCommandValidator() 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) 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 FluentValidation;
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; 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 namespace Ordering.API.Application.Validations
{ {
@ -11,7 +7,7 @@ namespace Ordering.API.Application.Validations
{ {
public IdentifierCommandValidator() 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 try
{ {
var order = await _orderQueries.GetOrder(orderId);
var order = await _orderQueries
.GetOrderAsync(orderId);
return Ok(order); return Ok(order);
} }
catch (KeyNotFoundException) catch (KeyNotFoundException)
@ -70,7 +72,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
[HttpGet] [HttpGet]
public async Task<IActionResult> GetOrders() public async Task<IActionResult> GetOrders()
{ {
var orders = await _orderQueries.GetOrders();
var orders = await _orderQueries
.GetOrdersAsync();
return Ok(orders); return Ok(orders);
} }
@ -79,7 +82,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
[HttpGet] [HttpGet]
public async Task<IActionResult> GetCardTypes() public async Task<IActionResult> GetCardTypes()
{ {
var cardTypes = await _orderQueries.GetCardTypes();
var cardTypes = await _orderQueries
.GetCardTypesAsync();
return Ok(cardTypes); return Ok(cardTypes);
} }


+ 3
- 3
src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs View File

@ -41,12 +41,12 @@
{ {
var json = new JsonErrorResponse var json = new JsonErrorResponse
{ {
Messages = new[] { "An error ocurr.Try it again." }
Messages = new[] { "An error occur.Try it again." }
}; };
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {
json.DeveloperMeesage = context.Exception;
json.DeveloperMessage = context.Exception;
} }
// Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1
@ -61,7 +61,7 @@
{ {
public string[] Messages { get; set; } public string[] Messages { get; set; }
public object DeveloperMeesage { get; set; }
public object DeveloperMessage { get; set; }
} }
} }
} }

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

@ -22,6 +22,7 @@
using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.HealthChecks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ordering.Infrastructure; using Ordering.Infrastructure;
using RabbitMQ.Client;
using System; using System;
using System.Data.Common; using System.Data.Common;
using System.Reflection; using System.Reflection;
@ -65,14 +66,14 @@
services.AddEntityFrameworkSqlServer() services.AddEntityFrameworkSqlServer()
.AddDbContext<OrderingContext>(options => .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(); services.AddSwaggerGen();
@ -105,7 +106,21 @@
sp => (DbConnection c) => new IntegrationEventLogService(c)); sp => (DbConnection c) => new IntegrationEventLogService(c));
var serviceProvider = services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
services.AddTransient<IOrderingIntegrationEventService, OrderingIntegrationEventService>(); 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(); services.AddOptions();
//configure autofac //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(); public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly();
protected Buyer() { protected Buyer() {
_paymentMethods = new List<PaymentMethod>(); _paymentMethods = new List<PaymentMethod>();
} }
@ -34,6 +35,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B
if (existingPayment != null) if (existingPayment != null)
{ {
AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, existingPayment, orderId)); AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, existingPayment, orderId));
return existingPayment; return existingPayment;
} }
else else
@ -41,7 +43,9 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.B
var payment = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration); var payment = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration);
_paymentMethods.Add(payment); _paymentMethods.Add(payment);
AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, payment, orderId)); AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, payment, orderId));
return payment; 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> public interface IOrderRepository : IRepository<Order>
{ {
Order Add(Order order); Order Add(Order order);
void Update(Order order);
Task<Order> GetAsync(int orderId); 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() 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; return matchingItem;
} }
public static T FromDisplayName<T>(string displayName) where T : Enumeration, new() 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; 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); 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;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency
{ {
public class ClientRequest public class ClientRequest
{ {

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

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency
@ -8,6 +6,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten
public interface IRequestManager public interface IRequestManager
{ {
Task<bool> ExistAsync(Guid id); Task<bool> ExistAsync(Guid id);
Task CreateRequestForCommandAsync<T>(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;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency
@ -10,22 +7,25 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten
public class RequestManager : IRequestManager public class RequestManager : IRequestManager
{ {
private readonly OrderingContext _context; 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) public async Task<bool> ExistAsync(Guid id)
{ {
var request = await _context.FindAsync<ClientRequest>(id);
var request = await _context.
FindAsync<ClientRequest>(id);
return request != null; return request != null;
} }
public async Task CreateRequestForCommandAsync<T>(Guid id) public async Task CreateRequestForCommandAsync<T>(Guid id)
{
{
var exists = await ExistAsync(id); var exists = await ExistAsync(id);
var request = exists ? var request = exists ?
throw new OrderingDomainException($"Request with {id} already exists") : throw new OrderingDomainException($"Request with {id} already exists") :
new ClientRequest() new ClientRequest()
@ -36,8 +36,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempoten
}; };
_context.Add(request); _context.Add(request);
await _context.SaveChangesAsync(); 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 namespace Ordering.Infrastructure
{ {
public static class MediatorExtension
static class MediatorExtension
{ {
public static async Task DispatchDomainEventsAsync(this IMediator mediator, OrderingContext ctx) 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 var tasks = domainEvents
.Select(async (domainEvent) => { .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.BuyerAggregate;
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency;
using Ordering.Infrastructure; using Ordering.Infrastructure;
using System; using System;
using System.Threading; using System.Threading;
@ -34,7 +35,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure
public OrderingContext(DbContextOptions options, IMediator mediator) : base(options) public OrderingContext(DbContextOptions options, IMediator mediator) : base(options)
{ {
_mediator = mediator;
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
} }
protected override void OnModelCreating(ModelBuilder modelBuilder) 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. // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddDataProtection(opts =>
{
opts.ApplicationDiscriminator = "eshop.webmvc";
});
services.AddMvc(); services.AddMvc();
services.Configure<AppSettings>(Configuration); 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 { BrowserModule } from '@angular/platform-browser';
// import { FormsModule } from '@angular/forms'; // import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http'; import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/Router';
import { RouterModule } from '@angular/router';
import { routing } from './app.routes'; import { routing } from './app.routes';
import { AppService } from './app.service'; 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.Configure<AppSettings>(Configuration);
services.AddDataProtection(opts =>
{
opts.ApplicationDiscriminator = "eshop.webspa";
});
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
services.AddMvc() services.AddMvc()


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

@ -60,7 +60,7 @@ namespace UnitTest.Ordering.Application
{ {
//Arrange //Arrange
var fakeDynamicResult = new Object(); var fakeDynamicResult = new Object();
_orderQueriesMock.Setup(x => x.GetOrders())
_orderQueriesMock.Setup(x => x.GetOrdersAsync())
.Returns(Task.FromResult(fakeDynamicResult)); .Returns(Task.FromResult(fakeDynamicResult));
//Act //Act
@ -77,7 +77,7 @@ namespace UnitTest.Ordering.Application
//Arrange //Arrange
var fakeOrderId = 123; var fakeOrderId = 123;
var fakeDynamicResult = new Object(); var fakeDynamicResult = new Object();
_orderQueriesMock.Setup(x => x.GetOrder(It.IsAny<int>()))
_orderQueriesMock.Setup(x => x.GetOrderAsync(It.IsAny<int>()))
.Returns(Task.FromResult(fakeDynamicResult)); .Returns(Task.FromResult(fakeDynamicResult));
//Act //Act
@ -93,7 +93,7 @@ namespace UnitTest.Ordering.Application
{ {
//Arrange //Arrange
var fakeDynamicResult = new Object(); var fakeDynamicResult = new Object();
_orderQueriesMock.Setup(x => x.GetCardTypes())
_orderQueriesMock.Setup(x => x.GetCardTypesAsync())
.Returns(Task.FromResult(fakeDynamicResult)); .Returns(Task.FromResult(fakeDynamicResult));
//Act //Act


Loading…
Cancel
Save