Merge branch 'master' into xamarin
This commit is contained in:
commit
dfe45883b2
13
README.md
13
README.md
@ -1,12 +1,13 @@
|
|||||||
# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**ALPHA state** - VS 2017 and CLI environments compatible)
|
# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**BETA state** - Visual Studio 2017 and CLI environments compatible)
|
||||||
Sample .NET Core reference application, powered by Microsoft, based on a simplified microservices architecture and Docker containers. <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.
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 38 KiB |
BIN
img/ebook_arch_dev_microservices_containers_cover_LARGE.png
Normal file
BIN
img/ebook_arch_dev_microservices_containers_cover_LARGE.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 242 KiB |
BIN
img/ebook_arch_dev_microservices_containers_cover_OLD.png
Normal file
BIN
img/ebook_arch_dev_microservices_containers_cover_OLD.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Retry;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
using RabbitMQ.Client.Events;
|
||||||
|
using RabbitMQ.Client.Exceptions;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
||||||
|
{
|
||||||
|
public class DefaultRabbitMQPersisterConnection
|
||||||
|
: IRabbitMQPersisterConnection
|
||||||
|
{
|
||||||
|
private readonly IConnectionFactory _connectionFactory;
|
||||||
|
private readonly ILogger<DefaultRabbitMQPersisterConnection> _logger;
|
||||||
|
|
||||||
|
IConnection _connection;
|
||||||
|
bool _disposed;
|
||||||
|
|
||||||
|
object sync_root = new object();
|
||||||
|
|
||||||
|
public DefaultRabbitMQPersisterConnection(IConnectionFactory connectionFactory,ILogger<DefaultRabbitMQPersisterConnection> logger)
|
||||||
|
{
|
||||||
|
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _connection != null && _connection.IsOpen && !_disposed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IModel CreateModel()
|
||||||
|
{
|
||||||
|
if (!IsConnected)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No RabbitMQ connections are available to perform this action");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _connection.CreateModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryConnect()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("RabbitMQ Client is trying to connect");
|
||||||
|
|
||||||
|
lock (sync_root)
|
||||||
|
{
|
||||||
|
var policy = RetryPolicy.Handle<SocketException>()
|
||||||
|
.Or<BrokerUnreachableException>()
|
||||||
|
.WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex.ToString());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
policy.Execute(() =>
|
||||||
|
{
|
||||||
|
_connection = _connectionFactory
|
||||||
|
.CreateConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (IsConnected)
|
||||||
|
{
|
||||||
|
_connection.ConnectionShutdown += OnConnectionShutdown;
|
||||||
|
_connection.CallbackException += OnCallbackException;
|
||||||
|
_connection.ConnectionBlocked += OnConnectionBlocked;
|
||||||
|
|
||||||
|
_logger.LogInformation($"RabbitMQ persister connection acquire a connection {_connection.Endpoint.HostName} and is subscribed to failure events");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogCritical("FATAL ERROR: RabbitMQ connections can't be created and opened");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect...");
|
||||||
|
|
||||||
|
TryConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnCallbackException(object sender, CallbackExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect...");
|
||||||
|
|
||||||
|
TryConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnConnectionShutdown(object sender, ShutdownEventArgs reason)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect...");
|
||||||
|
|
||||||
|
TryConnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,16 @@
|
|||||||
|
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
using Microsoft.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,56 +19,83 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
|||||||
{
|
{
|
||||||
public class EventBusRabbitMQ : IEventBus, IDisposable
|
public class EventBusRabbitMQ : IEventBus, IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _brokerName = "eshop_event_bus";
|
const string BROKER_NAME = "eshop_event_bus";
|
||||||
private readonly string _connectionString;
|
|
||||||
private readonly Dictionary<string, List<IIntegrationEventHandler>> _handlers;
|
|
||||||
private readonly List<Type> _eventTypes;
|
|
||||||
|
|
||||||
private IModel _model;
|
private readonly IRabbitMQPersisterConnection _persisterConnection;
|
||||||
private IConnection _connection;
|
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(IRabbitMQPersisterConnection persisterConnection, ILogger<EventBusRabbitMQ> logger)
|
||||||
public EventBusRabbitMQ(string connectionString)
|
|
||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_persisterConnection = persisterConnection ?? throw new ArgumentNullException(nameof(persisterConnection));
|
||||||
_handlers = new Dictionary<string, List<IIntegrationEventHandler>>();
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_eventTypes = new List<Type>();
|
|
||||||
|
_consumerChannel = CreateConsumerChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Publish(IntegrationEvent @event)
|
public void Publish(IntegrationEvent @event)
|
||||||
{
|
{
|
||||||
var eventName = @event.GetType().Name;
|
if (!_persisterConnection.IsConnected)
|
||||||
var factory = new ConnectionFactory() { HostName = _connectionString };
|
|
||||||
using (var connection = factory.CreateConnection())
|
|
||||||
using (var channel = connection.CreateModel())
|
|
||||||
{
|
{
|
||||||
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();
|
if (!_persisterConnection.IsConnected)
|
||||||
|
{
|
||||||
|
_persisterConnection.TryConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var channel = _persisterConnection.CreateModel())
|
||||||
|
{
|
||||||
channel.QueueBind(queue: _queueName,
|
channel.QueueBind(queue: _queueName,
|
||||||
exchange: _brokerName,
|
exchange: BROKER_NAME,
|
||||||
routingKey: eventName);
|
routingKey: eventName);
|
||||||
|
|
||||||
_handlers.Add(eventName, new List<IIntegrationEventHandler>());
|
_handlers.Add(eventName, new List<IIntegrationEventHandler>());
|
||||||
@ -75,9 +105,12 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
|
var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName);
|
||||||
|
|
||||||
|
if (eventType != null)
|
||||||
|
{
|
||||||
_eventTypes.Remove(eventType);
|
_eventTypes.Remove(eventType);
|
||||||
_model.QueueUnbind(queue: _queueName,
|
|
||||||
exchange: _brokerName,
|
if (!_persisterConnection.IsConnected)
|
||||||
|
{
|
||||||
|
_persisterConnection.TryConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var channel = _persisterConnection.CreateModel())
|
||||||
|
{
|
||||||
|
channel.QueueUnbind(queue: _queueName,
|
||||||
|
exchange: BROKER_NAME,
|
||||||
routingKey: eventName);
|
routingKey: eventName);
|
||||||
|
|
||||||
if (_handlers.Keys.Count == 0)
|
if (_handlers.Keys.Count == 0)
|
||||||
{
|
{
|
||||||
_queueName = string.Empty;
|
_queueName = string.Empty;
|
||||||
_model.Dispose();
|
|
||||||
_connection.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
_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()
|
channel.ExchangeDeclare(exchange: BROKER_NAME,
|
||||||
{
|
|
||||||
var factory = new ConnectionFactory() { HostName = _connectionString };
|
|
||||||
var con = factory.CreateConnection();
|
|
||||||
var channel = con.CreateModel();
|
|
||||||
|
|
||||||
channel.ExchangeDeclare(exchange: _brokerName,
|
|
||||||
type: "direct");
|
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)
|
||||||
@ -166,6 +209,5 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
using RabbitMQ.Client;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ
|
||||||
|
{
|
||||||
|
|
||||||
|
public interface IRabbitMQPersisterConnection
|
||||||
|
: IDisposable
|
||||||
|
{
|
||||||
|
bool IsConnected { get; }
|
||||||
|
|
||||||
|
bool TryConnect();
|
||||||
|
|
||||||
|
IModel CreateModel();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,4 @@
|
|||||||
using System;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API
|
|
||||||
{
|
{
|
||||||
public class BasketSettings
|
public class BasketSettings
|
||||||
{
|
{
|
||||||
|
@ -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,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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
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,12 +94,20 @@ 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.
|
// 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);
|
var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString);
|
||||||
_logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}");
|
_logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}");
|
||||||
_redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString());
|
_redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
using System.Linq;
|
using Basket.API.Infrastructure.Filters;
|
||||||
|
using Basket.API.IntegrationEvents.EventHandling;
|
||||||
|
using Basket.API.IntegrationEvents.Events;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.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.Logging;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.Net;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
|
|
||||||
using System;
|
|
||||||
using Microsoft.Extensions.HealthChecks;
|
using Microsoft.Extensions.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using RabbitMQ.Client;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using 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) => {
|
services.AddSingleton<ConnectionMultiplexer>(sp =>
|
||||||
var config = sp.GetRequiredService<IOptionsSnapshot<BasketSettings>>().Value;
|
{
|
||||||
var ips = Dns.GetHostAddressesAsync(config.ConnectionString).Result;
|
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>>();
|
ConfigureEventBus(app);
|
||||||
var orderStartedHandler = app.ApplicationServices.GetService<IIntegrationEventHandler<OrderStartedIntegrationEvent>>();
|
|
||||||
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
|
|
||||||
eventBus.Subscribe<ProductPriceChangedIntegrationEvent>(catalogPriceHandler);
|
|
||||||
eventBus.Subscribe<OrderStartedIntegrationEvent>(orderStartedHandler);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,5 +146,20 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API
|
|||||||
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
src/Services/Catalog/Catalog.API/CatalogSettings.cs
Normal file
9
src/Services/Catalog/Catalog.API/CatalogSettings.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Microsoft.eShopOnContainers.Services.Catalog.API
|
||||||
|
{
|
||||||
|
public class CatalogSettings
|
||||||
|
{
|
||||||
|
public string ExternalCatalogBaseUrl {get;set;}
|
||||||
|
|
||||||
|
public string EventBusConnection { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Catalog.API.IntegrationEvents;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
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;
|
_catalogContext = context ?? throw new ArgumentNullException(nameof(context));
|
||||||
_catalogIntegrationEventService = catalogIntegrationEventService;
|
_catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService));
|
||||||
_settings = settings;
|
|
||||||
|
|
||||||
((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]
|
||||||
@ -51,7 +44,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);
|
||||||
@ -59,6 +52,24 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
|
|||||||
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,15 +149,22 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
|
|||||||
return Ok(items);
|
return Ok(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
//POST api/v1/[controller]/update
|
//PUT api/v1/[controller]/items
|
||||||
[Route("update")]
|
[Route("items")]
|
||||||
[HttpPost]
|
[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);
|
var catalogItem = await _catalogContext.CatalogItems
|
||||||
if (catalogItem == null) return NotFound();
|
.SingleOrDefaultAsync(i => i.Id == productToUpdate.Id);
|
||||||
var raiseProductPriceChangedEvent = catalogItem.Price != productToUpdate.Price;
|
|
||||||
|
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;
|
||||||
@ -168,16 +186,15 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
|
|||||||
await _catalogContext.SaveChangesAsync();
|
await _catalogContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return CreatedAtAction(nameof(GetItemById), new { id = productToUpdate.Id }, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
//POST api/v1/[controller]/create
|
//POST api/v1/[controller]/items
|
||||||
[Route("create")]
|
[Route("items")]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateProduct([FromBody]CatalogItem product)
|
public async Task<IActionResult> CreateProduct([FromBody]CatalogItem product)
|
||||||
{
|
{
|
||||||
_catalogContext.CatalogItems.Add(
|
var item = new CatalogItem
|
||||||
new CatalogItem
|
|
||||||
{
|
{
|
||||||
CatalogBrandId = product.CatalogBrandId,
|
CatalogBrandId = product.CatalogBrandId,
|
||||||
CatalogTypeId = product.CatalogTypeId,
|
CatalogTypeId = product.CatalogTypeId,
|
||||||
@ -185,11 +202,12 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
|
|||||||
Name = product.Name,
|
Name = product.Name,
|
||||||
PictureUri = product.PictureUri,
|
PictureUri = product.PictureUri,
|
||||||
Price = product.Price
|
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
|
||||||
@ -205,13 +223,16 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
_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) {
|
private List<CatalogItem> ChangeUriPlaceholder(List<CatalogItem> items)
|
||||||
var baseUri = _settings.Value.ExternalCatalogBaseUrl;
|
{
|
||||||
|
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,8 +1,4 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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; }
|
||||||
|
@ -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; }
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
@ -67,13 +66,14 @@
|
|||||||
//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();
|
||||||
@ -100,10 +100,22 @@
|
|||||||
|
|
||||||
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
|
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
|
||||||
sp => (DbConnection c) => new IntegrationEventLogService(c));
|
sp => (DbConnection c) => new IntegrationEventLogService(c));
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
|
||||||
var configuration = serviceProvider.GetRequiredService<IOptionsSnapshot<Settings>>().Value;
|
|
||||||
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,6 +136,7 @@
|
|||||||
.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();
|
||||||
@ -132,12 +145,14 @@
|
|||||||
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();
|
||||||
@ -152,11 +167,10 @@
|
|||||||
WaitForSqlAvailability(ctx, loggerFactory, retryForAvailability);
|
WaitForSqlAvailability(ctx, loggerFactory, retryForAvailability);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally
|
||||||
|
{
|
||||||
ctx.Database.CloseConnection();
|
ctx.Database.CloseConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Catalog.API
|
|
||||||
{
|
|
||||||
// TODO: Rename CatalogSettings for consistency?
|
|
||||||
public class Settings
|
|
||||||
{
|
|
||||||
public string ExternalCatalogBaseUrl {get;set;}
|
|
||||||
public string EventBusConnection { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -57,6 +57,11 @@ namespace eShopOnContainers.Identity
|
|||||||
|
|
||||||
services.Configure<AppSettings>(Configuration);
|
services.Configure<AppSettings>(Configuration);
|
||||||
|
|
||||||
|
services.AddDataProtection(opts =>
|
||||||
|
{
|
||||||
|
opts.ApplicationDiscriminator = "eshop.identity";
|
||||||
|
});
|
||||||
|
|
||||||
services.AddMvc();
|
services.AddMvc();
|
||||||
|
|
||||||
services.AddHealthChecks(checks =>
|
services.AddHealthChecks(checks =>
|
||||||
|
@ -46,8 +46,10 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands
|
|||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -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(command => command.City).NotEmpty();
|
||||||
RuleFor(order => order.Street).NotEmpty();
|
RuleFor(command => command.Street).NotEmpty();
|
||||||
RuleFor(order => order.State).NotEmpty();
|
RuleFor(command => command.State).NotEmpty();
|
||||||
RuleFor(order => order.Country).NotEmpty();
|
RuleFor(command => command.Country).NotEmpty();
|
||||||
RuleFor(order => order.ZipCode).NotEmpty();
|
RuleFor(command => command.ZipCode).NotEmpty();
|
||||||
RuleFor(order => order.CardNumber).NotEmpty().Length(12, 19);
|
RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19);
|
||||||
RuleFor(order => order.CardHolderName).NotEmpty();
|
RuleFor(command => command.CardHolderName).NotEmpty();
|
||||||
RuleFor(order => order.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date");
|
RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date");
|
||||||
RuleFor(order => order.CardSecurityNumber).NotEmpty().Length(3);
|
RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3);
|
||||||
RuleFor(order => order.CardTypeId).NotEmpty();
|
RuleFor(command => command.CardTypeId).NotEmpty();
|
||||||
RuleFor(order => order.OrderItems).Must(ContainOrderItems).WithMessage("No order items found");
|
RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found");
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool BeValidExpirationDate(DateTime dateTime)
|
private bool BeValidExpirationDate(DateTime dateTime)
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,8 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O
|
|||||||
{
|
{
|
||||||
Order Add(Order order);
|
Order Add(Order order);
|
||||||
|
|
||||||
Task<Order> GetAsync(int orderId);
|
|
||||||
|
|
||||||
void Update(Order order);
|
void Update(Order order);
|
||||||
|
|
||||||
|
Task<Order> GetAsync(int orderId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 domainEntities = ctx.ChangeTracker
|
||||||
var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList();
|
.Entries<Entity>()
|
||||||
domainEntities.ToList().ForEach(entity => entity.Entity.DomainEvents.Clear());
|
.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) => {
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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()
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user