Merge branch 'reviews/rabbitmq'
This commit is contained in:
commit
46a030b095
@ -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,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";
|
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(string connectionString)
|
public EventBusRabbitMQ(IRabbitMQPersisterConnection persisterConnection, ILogger<EventBusRabbitMQ> logger)
|
||||||
{
|
{
|
||||||
_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)
|
||||||
channel.QueueBind(queue: _queueName,
|
{
|
||||||
exchange: _brokerName,
|
_persisterConnection.TryConnect();
|
||||||
routingKey: eventName);
|
}
|
||||||
|
|
||||||
_handlers.Add(eventName, new List<IIntegrationEventHandler>());
|
using (var channel = _persisterConnection.CreateModel())
|
||||||
_handlers[eventName].Add(handler);
|
{
|
||||||
_eventTypes.Add(typeof(T));
|
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;
|
_eventTypes.Remove(eventType);
|
||||||
_model.Dispose();
|
|
||||||
_connection.Dispose();
|
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()
|
channel.ExchangeDeclare(exchange: BROKER_NAME,
|
||||||
{
|
type: "direct");
|
||||||
var factory = new ConnectionFactory() { HostName = _connectionString };
|
|
||||||
var con = factory.CreateConnection();
|
|
||||||
var channel = con.CreateModel();
|
|
||||||
|
|
||||||
channel.ExchangeDeclare(exchange: _brokerName,
|
_queueName = channel.QueueDeclare().QueueName;
|
||||||
type: "direct");
|
|
||||||
if (string.IsNullOrEmpty(_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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,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.
|
// TODO: Need to make this more robust. ConnectionMultiplexer.ConnectAsync doesn't like domain names or IPv6 addresses.
|
||||||
var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString);
|
if (IPAddress.TryParse(_settings.ConnectionString, out var ip))
|
||||||
_logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}");
|
{
|
||||||
_redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString());
|
_redis = await ConnectionMultiplexer.ConnectAsync(ip.ToString());
|
||||||
|
_logger.LogInformation($"Connecting to database at {_settings.ConnectionString}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// workaround for https://github.com/StackExchange/StackExchange.Redis/issues/410
|
||||||
|
var ips = await Dns.GetHostAddressesAsync(_settings.ConnectionString);
|
||||||
|
_logger.LogInformation($"Connecting to database {_settings.ConnectionString} at IP {ips.First().ToString()}");
|
||||||
|
_redis = await ConnectionMultiplexer.ConnectAsync(ips.First().ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
using System.Linq;
|
using Basket.API.Infrastructure.Filters;
|
||||||
|
using Basket.API.IntegrationEvents.EventHandling;
|
||||||
|
using Basket.API.IntegrationEvents.Events;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
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]
|
||||||
@ -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
|
//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;
|
||||||
_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
|
//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,
|
Description = product.Description,
|
||||||
Description = product.Description,
|
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
|
||||||
@ -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) {
|
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
|
||||||
{
|
{
|
||||||
@ -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));
|
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,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 {
|
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 =>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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"],
|
options.UseSqlServer(Configuration["ConnectionString"],
|
||||||
sqlServerOptionsAction: sqlOptions =>
|
sqlServerOptionsAction: sqlOptions =>
|
||||||
{
|
{
|
||||||
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
|
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
|
||||||
sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
|
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)
|
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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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