# Conflicts: # src/Services/Basket/Basket.API/Model/IBasketRepository.cs # src/Services/Basket/Basket.API/Model/RedisBasketRepository.cs # src/Services/Catalog/Catalog.API/Startup.cs # src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs # src/Services/Ordering/Ordering.Infrastructure/Repositories/OrderRepository.cs # test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cspull/119/merge
@ -0,0 +1,29 @@ | |||
#https://github.com/spring2/dockerfiles/tree/master/rabbitmq | |||
FROM microsoft/windowsservercore | |||
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] | |||
ENV chocolateyUseWindowsCompression false | |||
RUN iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1')); \ | |||
choco install -y curl; | |||
RUN choco install -y erlang | |||
ENV ERLANG_SERVICE_MANAGER_PATH="C:\Program Files\erl8.2\erts-8.2\bin" | |||
RUN choco install -y rabbitmq | |||
ENV RABBITMQ_SERVER="C:\Program Files\RabbitMQ Server\rabbitmq_server-3.6.5" | |||
ENV RABBITMQ_CONFIG_FILE="c:\rabbitmq" | |||
COPY rabbitmq.config C:/ | |||
COPY rabbitmq.config C:/Users/ContainerAdministrator/AppData/Roaming/RabbitMQ/ | |||
COPY enabled_plugins C:/Users/ContainerAdministrator/AppData/Roaming/RabbitMQ/ | |||
EXPOSE 4369 | |||
EXPOSE 5672 | |||
EXPOSE 5671 | |||
EXPOSE 15672 | |||
WORKDIR C:/Program\ Files/RabbitMQ\ Server/rabbitmq_server-3.6.5/sbin | |||
CMD .\rabbitmq-server.bat |
@ -0,0 +1 @@ | |||
[rabbitmq_amqp1_0,rabbitmq_management]. |
@ -0,0 +1 @@ | |||
[{rabbit, [{loopback_users, []}]}]. |
@ -0,0 +1,30 @@ | |||
# The MSI installs a service which is hard to override, so let's use a zip file. | |||
FROM microsoft/windowsservercore | |||
MAINTAINER alexellis2@gmail.com | |||
SHELL ["powershell"] | |||
RUN $ErrorActionPreference = 'Stop'; \ | |||
wget https://github.com/MSOpenTech/redis/releases/download/win-3.2.100/Redis-x64-3.2.100.zip -OutFile Redis-x64-3.2.100.zip ; \ | |||
Expand-Archive Redis-x64-3.2.100.zip -dest 'C:\\Program Files\\Redis\\' ; \ | |||
Remove-Item Redis-x64-3.2.100.zip -Force | |||
RUN setx PATH '%PATH%;C:\\Program Files\\Redis\\' | |||
WORKDIR 'C:\\Program Files\\Redis\\' | |||
RUN Get-Content redis.windows.conf | Where { $_ -notmatch 'bind 127.0.0.1' } | Set-Content redis.openport.conf ; \ | |||
Get-Content redis.openport.conf | Where { $_ -notmatch 'protected-mode yes' } | Set-Content redis.unprotected.conf ; \ | |||
Add-Content redis.unprotected.conf 'protected-mode no' ; \ | |||
Add-Content redis.unprotected.conf 'bind 0.0.0.0' ; \ | |||
Get-Content redis.unprotected.conf | |||
EXPOSE 6379 | |||
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord | |||
# Define our command to be run when launching the container | |||
CMD .\\redis-server.exe .\\redis.unprotected.conf --port 6379 ; \ | |||
Write-Host Redis Started... ; \ | |||
while ($true) { Start-Sleep -Seconds 3600 } |
@ -0,0 +1,79 @@ | |||
version: '2.1' | |||
# The default docker-compose.override file can use the "localhost" as the external name for testing web apps within the same dev machine. | |||
# The ESHOP_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: | |||
# ESHOP_EXTERNAL_DNS_NAME_OR_IP=localhost | |||
# but values present in the environment vars at runtime will always override those defined inside the .env file | |||
# An external IP or DNS name has to be used (instead localhost and the 10.0.75.1 IP) when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. | |||
services: | |||
basket.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:5103 | |||
- ConnectionString=basket.data | |||
- identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. | |||
- EventBusConnection=rabbitmq | |||
ports: | |||
- "5103:5103" | |||
catalog.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:5101 | |||
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word | |||
- ExternalCatalogBaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. | |||
- EventBusConnection=rabbitmq | |||
ports: | |||
- "5101:5101" | |||
identity.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:5105 | |||
- SpaClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5104 | |||
- ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word | |||
- MvcClient=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your local dev-machine firewall at range 5100-5105. | |||
ports: | |||
- "5105:5105" | |||
ordering.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:5102 | |||
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word | |||
- identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. | |||
- EventBusConnection=rabbitmq | |||
ports: | |||
- "5102:5102" | |||
webspa: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:5104 | |||
- CatalogUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 | |||
- OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 | |||
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. | |||
- BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 | |||
ports: | |||
- "5104:5104" | |||
webmvc: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Development | |||
- ASPNETCORE_URLS=http://0.0.0.0:5100 | |||
- CatalogUrl=http://catalog.api:5101 | |||
- OrderingUrl=http://ordering.api:5102 | |||
- BasketUrl=http://basket.api:5103 | |||
- IdentityUrl=http://10.0.75.1:5105 #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser. | |||
#Remote: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser. | |||
ports: | |||
- "5100:5100" | |||
sql.data: | |||
environment: | |||
- SA_PASSWORD=Pass@word | |||
- ACCEPT_EULA=Y | |||
ports: | |||
- "5433:1433" |
@ -0,0 +1,80 @@ | |||
version: '2.1' | |||
# The Production docker-compose file has to have the external/real IPs or DNS names for the services | |||
# The ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP environment variable is taken, by default, from the ".env" file defined like: | |||
# ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP=192.168.88.248 | |||
# but values present in the environment vars at runtime will always override those defined inside the .env file | |||
# An external IP or DNS name has to be used when testing the Web apps and the Xamarin apps from remote machines/devices using the same WiFi, for instance. | |||
# | |||
# Set ASPNETCORE_ENVIRONMENT=Development to get errors while testing. | |||
# | |||
# You need to start it with the following CLI command: | |||
# docker-compose -f docker-compose-windows.yml -f docker-compose-windows.prod.yml up -d | |||
services: | |||
basket.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Production | |||
- ASPNETCORE_URLS=http://0.0.0.0:5103 | |||
- ConnectionString=basket.data | |||
- identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. | |||
ports: | |||
- "5103:5103" | |||
catalog.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Production | |||
- ASPNETCORE_URLS=http://0.0.0.0:5101 | |||
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word | |||
- ExternalCatalogBaseUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. | |||
ports: | |||
- "5101:5101" | |||
identity.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Production | |||
- ASPNETCORE_URLS=http://0.0.0.0:5105 | |||
- SpaClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5104 | |||
- ConnectionStrings__DefaultConnection=Server=sql.data;Database=Microsoft.eShopOnContainers.Service.IdentityDb;User Id=sa;Password=Pass@word | |||
- MvcClient=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5100 #Local: You need to open your host's firewall at range 5100-5105. | |||
ports: | |||
- "5105:5105" | |||
ordering.api: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Production | |||
- ASPNETCORE_URLS=http://0.0.0.0:5102 | |||
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word | |||
- identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. | |||
ports: | |||
- "5102:5102" | |||
webspa: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Production | |||
- ASPNETCORE_URLS=http://0.0.0.0:5104 | |||
- CatalogUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5101 | |||
- OrderingUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5102 | |||
- IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. | |||
- BasketUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5103 | |||
ports: | |||
- "5104:5104" | |||
webmvc: | |||
environment: | |||
- ASPNETCORE_ENVIRONMENT=Production | |||
- ASPNETCORE_URLS=http://0.0.0.0:5100 | |||
- CatalogUrl=http://catalog.api:5101 | |||
- OrderingUrl=http://ordering.api:5102 | |||
- IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. | |||
- BasketUrl=http://basket.api:5103 | |||
ports: | |||
- "5100:5100" | |||
sql.data: | |||
environment: | |||
- SA_PASSWORD=Pass@word | |||
- ACCEPT_EULA=Y | |||
ports: | |||
- "5433:1433" |
@ -0,0 +1,80 @@ | |||
version: '2.1' | |||
services: | |||
basket.api: | |||
image: eshop/basket.api | |||
build: | |||
context: ./src/Services/Basket/Basket.API | |||
dockerfile: Dockerfile.nanowin | |||
depends_on: | |||
- basket.data | |||
- identity.api | |||
catalog.api: | |||
image: eshop/catalog.api | |||
build: | |||
context: ./src/Services/Catalog/Catalog.API | |||
dockerfile: Dockerfile.nanowin | |||
depends_on: | |||
- sql.data | |||
identity.api: | |||
image: eshop/identity.api | |||
build: | |||
context: ./src/Services/Identity/Identity.API | |||
dockerfile: Dockerfile.nanowin | |||
depends_on: | |||
- sql.data | |||
ordering.api: | |||
image: eshop/ordering.api | |||
build: | |||
context: ./src/Services/Ordering/Ordering.API | |||
dockerfile: Dockerfile.nanowin | |||
depends_on: | |||
- sql.data | |||
webspa: | |||
image: eshop/webspa | |||
build: | |||
context: ./src/Web/WebSPA | |||
dockerfile: Dockerfile.nanowin | |||
depends_on: | |||
- identity.api | |||
- basket.api | |||
webmvc: | |||
image: eshop/webmvc | |||
build: | |||
context: ./src/Web/WebMVC | |||
dockerfile: Dockerfile.nanowin | |||
depends_on: | |||
- catalog.api | |||
- ordering.api | |||
- identity.api | |||
- basket.api | |||
sql.data: | |||
image: microsoft/mssql-server-windows | |||
basket.data: | |||
image: redis | |||
build: | |||
context: ./_docker/redis | |||
dockerfile: Dockerfile.nanowin | |||
ports: | |||
- "6379:6379" | |||
rabbitmq: | |||
image: rabbitmq | |||
build: | |||
context: ./_docker/rabbitmq | |||
dockerfile: Dockerfile.nanowin | |||
ports: | |||
- "5672:5672" | |||
networks: | |||
default: | |||
external: | |||
name: nat | |||
@ -0,0 +1,14 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions | |||
{ | |||
public interface IEventBus | |||
{ | |||
void Subscribe<T>(IIntegrationEventHandler<T> handler) where T: IntegrationEvent; | |||
void Unsubscribe<T>(IIntegrationEventHandler<T> handler) where T : IntegrationEvent; | |||
void Publish(IntegrationEvent @event); | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions | |||
{ | |||
public interface IIntegrationEventHandler<in TIntegrationEvent> : IIntegrationEventHandler | |||
where TIntegrationEvent: IntegrationEvent | |||
{ | |||
Task Handle(TIntegrationEvent @event); | |||
} | |||
public interface IIntegrationEventHandler | |||
{ | |||
} | |||
} |
@ -0,0 +1,17 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp1.1</TargetFramework> | |||
<RuntimeFrameworkVersion>1.1.0</RuntimeFrameworkVersion> | |||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBus</RootNamespace> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Folder Include="Abstractions\" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,18 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events | |||
{ | |||
public class IntegrationEvent | |||
{ | |||
public IntegrationEvent() | |||
{ | |||
Id = Guid.NewGuid(); | |||
CreationDate = DateTime.UtcNow; | |||
} | |||
public Guid Id { get; } | |||
public DateTime CreationDate { get; } | |||
} | |||
} |
@ -0,0 +1,171 @@ | |||
| |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using Newtonsoft.Json; | |||
using RabbitMQ.Client; | |||
using RabbitMQ.Client.Events; | |||
using System; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ | |||
{ | |||
public class EventBusRabbitMQ : IEventBus, IDisposable | |||
{ | |||
private readonly string _brokerName = "eshop_event_bus"; | |||
private readonly string _connectionString; | |||
private readonly Dictionary<string, List<IIntegrationEventHandler>> _handlers; | |||
private readonly List<Type> _eventTypes; | |||
private IModel _model; | |||
private IConnection _connection; | |||
private string _queueName; | |||
public EventBusRabbitMQ(string connectionString) | |||
{ | |||
_connectionString = connectionString; | |||
_handlers = new Dictionary<string, List<IIntegrationEventHandler>>(); | |||
_eventTypes = new List<Type>(); | |||
} | |||
public void Publish(IntegrationEvent @event) | |||
{ | |||
var eventName = @event.GetType().Name; | |||
var factory = new ConnectionFactory() { HostName = _connectionString }; | |||
using (var connection = factory.CreateConnection()) | |||
using (var channel = connection.CreateModel()) | |||
{ | |||
channel.ExchangeDeclare(exchange: _brokerName, | |||
type: "direct"); | |||
string message = JsonConvert.SerializeObject(@event); | |||
var body = Encoding.UTF8.GetBytes(message); | |||
channel.BasicPublish(exchange: _brokerName, | |||
routingKey: eventName, | |||
basicProperties: null, | |||
body: body); | |||
} | |||
} | |||
public void Subscribe<T>(IIntegrationEventHandler<T> handler) where T : IntegrationEvent | |||
{ | |||
var eventName = typeof(T).Name; | |||
if (_handlers.ContainsKey(eventName)) | |||
{ | |||
_handlers[eventName].Add(handler); | |||
} | |||
else | |||
{ | |||
var channel = GetChannel(); | |||
channel.QueueBind(queue: _queueName, | |||
exchange: _brokerName, | |||
routingKey: eventName); | |||
_handlers.Add(eventName, new List<IIntegrationEventHandler>()); | |||
_handlers[eventName].Add(handler); | |||
_eventTypes.Add(typeof(T)); | |||
} | |||
} | |||
public void Unsubscribe<T>(IIntegrationEventHandler<T> handler) where T : IntegrationEvent | |||
{ | |||
var eventName = typeof(T).Name; | |||
if (_handlers.ContainsKey(eventName) && _handlers[eventName].Contains(handler)) | |||
{ | |||
_handlers[eventName].Remove(handler); | |||
if (_handlers[eventName].Count == 0) | |||
{ | |||
_handlers.Remove(eventName); | |||
var eventType = _eventTypes.Single(e => e.Name == eventName); | |||
_eventTypes.Remove(eventType); | |||
_model.QueueUnbind(queue: _queueName, | |||
exchange: _brokerName, | |||
routingKey: eventName); | |||
if (_handlers.Keys.Count == 0) | |||
{ | |||
_queueName = string.Empty; | |||
_model.Dispose(); | |||
_connection.Dispose(); | |||
} | |||
} | |||
} | |||
} | |||
public void Dispose() | |||
{ | |||
_handlers.Clear(); | |||
_model?.Dispose(); | |||
_connection?.Dispose(); | |||
} | |||
private IModel GetChannel() | |||
{ | |||
if (_model != null) | |||
{ | |||
return _model; | |||
} | |||
else | |||
{ | |||
(_model, _connection) = CreateConnection(); | |||
return _model; | |||
} | |||
} | |||
private (IModel model, IConnection connection) CreateConnection() | |||
{ | |||
var factory = new ConnectionFactory() { HostName = _connectionString }; | |||
var con = factory.CreateConnection(); | |||
var channel = con.CreateModel(); | |||
channel.ExchangeDeclare(exchange: _brokerName, | |||
type: "direct"); | |||
if (string.IsNullOrEmpty(_queueName)) | |||
{ | |||
_queueName = channel.QueueDeclare().QueueName; | |||
} | |||
var consumer = new EventingBasicConsumer(channel); | |||
consumer.Received += async (model, ea) => | |||
{ | |||
var eventName = ea.RoutingKey; | |||
var message = Encoding.UTF8.GetString(ea.Body); | |||
await ProcessEvent(eventName, message); | |||
}; | |||
channel.BasicConsume(queue: _queueName, | |||
noAck: true, | |||
consumer: consumer); | |||
return (channel, con); | |||
} | |||
private async Task ProcessEvent(string eventName, string message) | |||
{ | |||
if (_handlers.ContainsKey(eventName)) | |||
{ | |||
Type eventType = _eventTypes.Single(t => t.Name == eventName); | |||
var integrationEvent = JsonConvert.DeserializeObject(message, eventType); | |||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); | |||
var handlers = _handlers[eventName]; | |||
foreach (var handler in handlers) | |||
{ | |||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); | |||
} | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,19 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp1.1</TargetFramework> | |||
<RuntimeFrameworkVersion>1.1.0</RuntimeFrameworkVersion> | |||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ</RootNamespace> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" /> | |||
<PackageReference Include="RabbitMQ.Client" Version="4.1.1" /> | |||
<PackageReference Include="System.ValueTuple" Version="4.3.0" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\EventBus\EventBus.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,13 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF | |||
{ | |||
public enum EventStateEnum | |||
{ | |||
NotPublished = 0, | |||
Published = 1, | |||
PublishedFailed = 2 | |||
} | |||
} |
@ -0,0 +1,48 @@ | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Metadata.Builders; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF | |||
{ | |||
public class IntegrationEventLogContext : DbContext | |||
{ | |||
public IntegrationEventLogContext(DbContextOptions<IntegrationEventLogContext> options) : base(options) | |||
{ | |||
} | |||
public DbSet<IntegrationEventLogEntry> IntegrationEventLogs { get; set; } | |||
protected override void OnModelCreating(ModelBuilder builder) | |||
{ | |||
builder.Entity<IntegrationEventLogEntry>(ConfigureIntegrationEventLogEntry); | |||
} | |||
void ConfigureIntegrationEventLogEntry(EntityTypeBuilder<IntegrationEventLogEntry> builder) | |||
{ | |||
builder.ToTable("IntegrationEventLog"); | |||
builder.HasKey(e => e.EventId); | |||
builder.Property(e => e.EventId) | |||
.IsRequired(); | |||
builder.Property(e => e.Content) | |||
.IsRequired(); | |||
builder.Property(e => e.CreationTime) | |||
.IsRequired(); | |||
builder.Property(e => e.State) | |||
.IsRequired(); | |||
builder.Property(e => e.TimesSent) | |||
.IsRequired(); | |||
builder.Property(e => e.EventTypeName) | |||
.IsRequired(); | |||
} | |||
} | |||
} |
@ -0,0 +1,27 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp1.1</TargetFramework> | |||
<RuntimeFrameworkVersion>1.1.0</RuntimeFrameworkVersion> | |||
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF</RootNamespace> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.0" /> | |||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\EventBus\EventBus.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,28 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using Newtonsoft.Json; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF | |||
{ | |||
public class IntegrationEventLogEntry | |||
{ | |||
private IntegrationEventLogEntry() { } | |||
public IntegrationEventLogEntry(IntegrationEvent @event) | |||
{ | |||
EventId = @event.Id; | |||
CreationTime = @event.CreationDate; | |||
EventTypeName = @event.GetType().FullName; | |||
Content = JsonConvert.SerializeObject(@event); | |||
State = EventStateEnum.NotPublished; | |||
TimesSent = 0; | |||
} | |||
public Guid EventId { get; private set; } | |||
public string EventTypeName { get; private set; } | |||
public EventStateEnum State { get; set; } | |||
public int TimesSent { get; set; } | |||
public DateTime CreationTime { get; private set; } | |||
public string Content { get; private set; } | |||
} | |||
} |
@ -0,0 +1,15 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Data.Common; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services | |||
{ | |||
public interface IIntegrationEventLogService | |||
{ | |||
Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction); | |||
Task MarkEventAsPublishedAsync(IntegrationEvent @event); | |||
} | |||
} |
@ -0,0 +1,53 @@ | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Storage; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System.Data.Common; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using System; | |||
namespace Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services | |||
{ | |||
public class IntegrationEventLogService : IIntegrationEventLogService | |||
{ | |||
private readonly IntegrationEventLogContext _integrationEventLogContext; | |||
private readonly DbConnection _dbConnection; | |||
public IntegrationEventLogService(DbConnection dbConnection) | |||
{ | |||
_dbConnection = dbConnection?? throw new ArgumentNullException("dbConnection"); | |||
_integrationEventLogContext = new IntegrationEventLogContext( | |||
new DbContextOptionsBuilder<IntegrationEventLogContext>() | |||
.UseSqlServer(_dbConnection) | |||
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)) | |||
.Options); | |||
} | |||
public Task SaveEventAsync(IntegrationEvent @event, DbTransaction transaction) | |||
{ | |||
if(transaction == null) | |||
{ | |||
throw new ArgumentNullException("transaction", $"A {typeof(DbTransaction).FullName} is required as a pre-requisite to save the event."); | |||
} | |||
var eventLogEntry = new IntegrationEventLogEntry(@event); | |||
_integrationEventLogContext.Database.UseTransaction(transaction); | |||
_integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); | |||
return _integrationEventLogContext.SaveChangesAsync(); | |||
} | |||
public Task MarkEventAsPublishedAsync(IntegrationEvent @event) | |||
{ | |||
var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == @event.Id); | |||
eventLogEntry.TimesSent++; | |||
eventLogEntry.State = EventStateEnum.Published; | |||
_integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry); | |||
return _integrationEventLogContext.SaveChangesAsync(); | |||
} | |||
} | |||
} |
@ -0,0 +1,68 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.AspNetCore.Http.Features; | |||
using Microsoft.Extensions.HealthChecks; | |||
using Newtonsoft.Json; | |||
namespace Microsoft.AspNetCore.HealthChecks | |||
{ | |||
public class HealthCheckMiddleware | |||
{ | |||
private RequestDelegate _next; | |||
private string _path; | |||
private int? _port; | |||
private IHealthCheckService _service; | |||
public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, int port) | |||
{ | |||
_port = port; | |||
_service = service; | |||
_next = next; | |||
} | |||
public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, string path) | |||
{ | |||
_path = path; | |||
_service = service; | |||
_next = next; | |||
} | |||
public async Task Invoke(HttpContext context) | |||
{ | |||
if (IsHealthCheckRequest(context)) | |||
{ | |||
var result = await _service.CheckHealthAsync(); | |||
var status = result.CheckStatus; | |||
if (status != CheckStatus.Healthy) | |||
context.Response.StatusCode = 503; | |||
context.Response.Headers.Add("content-type", "application/json"); | |||
await context.Response.WriteAsync(JsonConvert.SerializeObject(new { status = status.ToString() })); | |||
return; | |||
} | |||
else | |||
{ | |||
await _next.Invoke(context); | |||
} | |||
} | |||
private bool IsHealthCheckRequest(HttpContext context) | |||
{ | |||
if (_port.HasValue) | |||
{ | |||
var connInfo = context.Features.Get<IHttpConnectionFeature>(); | |||
if (connInfo.LocalPort == _port) | |||
return true; | |||
} | |||
if (context.Request.Path == _path) | |||
return true; | |||
return false; | |||
} | |||
} | |||
} |
@ -0,0 +1,38 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.Hosting; | |||
namespace Microsoft.AspNetCore.HealthChecks | |||
{ | |||
public class HealthCheckStartupFilter : IStartupFilter | |||
{ | |||
private string _path; | |||
private int? _port; | |||
public HealthCheckStartupFilter(int port) | |||
{ | |||
_port = port; | |||
} | |||
public HealthCheckStartupFilter(string path) | |||
{ | |||
_path = path; | |||
} | |||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) | |||
{ | |||
return app => | |||
{ | |||
if (_port.HasValue) | |||
app.UseMiddleware<HealthCheckMiddleware>(_port); | |||
else | |||
app.UseMiddleware<HealthCheckMiddleware>(_path); | |||
next(app); | |||
}; | |||
} | |||
} | |||
} |
@ -0,0 +1,36 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using Microsoft.AspNetCore.HealthChecks; | |||
using Microsoft.Extensions.DependencyInjection; | |||
namespace Microsoft.AspNetCore.Hosting | |||
{ | |||
public static class HealthCheckWebHostBuilderExtension | |||
{ | |||
public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, int port) | |||
{ | |||
Guard.ArgumentValid(port > 0 && port < 65536, nameof(port), "Port must be a value between 1 and 65535"); | |||
builder.ConfigureServices(services => | |||
{ | |||
var existingUrl = builder.GetSetting(WebHostDefaults.ServerUrlsKey); | |||
builder.UseSetting(WebHostDefaults.ServerUrlsKey, $"{existingUrl};http://localhost:{port}"); | |||
services.AddSingleton<IStartupFilter>(new HealthCheckStartupFilter(port)); | |||
}); | |||
return builder; | |||
} | |||
public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, string path) | |||
{ | |||
Guard.ArgumentNotNull(nameof(path), path); | |||
// REVIEW: Is there a better URL path validator somewhere? | |||
Guard.ArgumentValid(!path.Contains("?"), nameof(path), "Path cannot contain query string values"); | |||
Guard.ArgumentValid(path.StartsWith("/"), nameof(path), "Path should start with /"); | |||
builder.ConfigureServices(services => services.AddSingleton<IStartupFilter>(new HealthCheckStartupFilter(path))); | |||
return builder; | |||
} | |||
} | |||
} |
@ -0,0 +1,38 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using Microsoft.Extensions.HealthChecks; | |||
namespace Microsoft.AspNetCore.Hosting | |||
{ | |||
public static class HealthCheckWebHostExtensions | |||
{ | |||
private const int DEFAULT_TIMEOUT_SECONDS = 300; | |||
public static void RunWhenHealthy(this IWebHost webHost) | |||
{ | |||
webHost.RunWhenHealthy(TimeSpan.FromSeconds(DEFAULT_TIMEOUT_SECONDS)); | |||
} | |||
public static void RunWhenHealthy(this IWebHost webHost, TimeSpan timeout) | |||
{ | |||
var healthChecks = webHost.Services.GetService(typeof(IHealthCheckService)) as IHealthCheckService; | |||
var loops = 0; | |||
do | |||
{ | |||
var checkResult = healthChecks.CheckHealthAsync().Result; | |||
if (checkResult.CheckStatus == CheckStatus.Healthy) | |||
{ | |||
webHost.Run(); | |||
break; | |||
} | |||
System.Threading.Thread.Sleep(1000); | |||
loops++; | |||
} while (loops < timeout.TotalSeconds); | |||
} | |||
} | |||
} |
@ -0,0 +1,16 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp1.0</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Compile Include="..\common\Guard.cs" Link="Internal\Guard.cs" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="1.0.2" /> | |||
<ProjectReference Include="..\Microsoft.Extensions.HealthChecks\Microsoft.Extensions.HealthChecks.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,45 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Data; | |||
using System.Data.SqlClient; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public static class HealthCheckBuilderDataExtensions | |||
{ | |||
public static HealthCheckBuilder AddSqlCheck(this HealthCheckBuilder builder, string name, string connectionString) | |||
{ | |||
builder.AddCheck($"SqlCheck({name})", async () => | |||
{ | |||
try | |||
{ | |||
//TODO: There is probably a much better way to do this. | |||
using (var connection = new SqlConnection(connectionString)) | |||
{ | |||
connection.Open(); | |||
using (var command = connection.CreateCommand()) | |||
{ | |||
command.CommandType = CommandType.Text; | |||
command.CommandText = "SELECT 1"; | |||
var result = (int)await command.ExecuteScalarAsync().ConfigureAwait(false); | |||
if (result == 1) | |||
{ | |||
return HealthCheckResult.Healthy($"SqlCheck({name}): Healthy"); | |||
} | |||
return HealthCheckResult.Unhealthy($"SqlCheck({name}): Unhealthy"); | |||
} | |||
} | |||
} | |||
catch(Exception ex) | |||
{ | |||
return HealthCheckResult.Unhealthy($"SqlCheck({name}): Exception during check: {ex.GetType().FullName}"); | |||
} | |||
}); | |||
return builder; | |||
} | |||
} | |||
} |
@ -0,0 +1,19 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netstandard1.3</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Compile Include="..\common\Guard.cs" Link="Internal\Guard.cs" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="System.Data.SqlClient" Version="4.3.0" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\Microsoft.Extensions.HealthChecks\Microsoft.Extensions.HealthChecks.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,13 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public enum CheckStatus | |||
{ | |||
Unknown, | |||
Unhealthy, | |||
Healthy, | |||
Warning | |||
} | |||
} |
@ -0,0 +1,110 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public static partial class HealthCheckBuilderExtensions | |||
{ | |||
// Lambda versions of AddCheck for Func/Func<Task>/Func<ValueTask> | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<IHealthCheckResult> check) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromCheck(check, builder.DefaultCacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<CancellationToken, IHealthCheckResult> check) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromCheck(check, builder.DefaultCacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<IHealthCheckResult> check, TimeSpan cacheDuration) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromCheck(check, cacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<CancellationToken, IHealthCheckResult> check, TimeSpan cacheDuration) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromCheck(check, cacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<Task<IHealthCheckResult>> check) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromTaskCheck(check, builder.DefaultCacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<CancellationToken, Task<IHealthCheckResult>> check) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromTaskCheck(check, builder.DefaultCacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<Task<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromTaskCheck(check, cacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func<CancellationToken, Task<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromTaskCheck(check, cacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func<ValueTask<IHealthCheckResult>> check) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, builder.DefaultCacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func<CancellationToken, ValueTask<IHealthCheckResult>> check) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, builder.DefaultCacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func<ValueTask<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, cacheDuration)); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func<CancellationToken, ValueTask<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, cacheDuration)); | |||
return builder; | |||
} | |||
} | |||
} |
@ -0,0 +1,55 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Collections.Generic; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public static partial class HealthCheckBuilderExtensions | |||
{ | |||
// Numeric checks | |||
public static HealthCheckBuilder AddMinValueCheck<T>(this HealthCheckBuilder builder, string name, T minValue, Func<T> currentValueFunc) | |||
where T : IComparable<T> | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
Guard.ArgumentNotNullOrWhitespace(nameof(name), name); | |||
Guard.ArgumentNotNull(nameof(currentValueFunc), currentValueFunc); | |||
builder.AddCheck(name, () => | |||
{ | |||
var currentValue = currentValueFunc(); | |||
var status = currentValue.CompareTo(minValue) >= 0 ? CheckStatus.Healthy : CheckStatus.Unhealthy; | |||
return HealthCheckResult.FromStatus( | |||
status, | |||
$"{name}: min={minValue}, current={currentValue}", | |||
new Dictionary<string, object> { { "min", minValue }, { "current", currentValue } } | |||
); | |||
}); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddMaxValueCheck<T>(this HealthCheckBuilder builder, string name, T maxValue, Func<T> currentValueFunc) | |||
where T : IComparable<T> | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
Guard.ArgumentNotNullOrWhitespace(nameof(name), name); | |||
Guard.ArgumentNotNull(nameof(currentValueFunc), currentValueFunc); | |||
builder.AddCheck($"{name}", () => | |||
{ | |||
var currentValue = currentValueFunc(); | |||
var status = currentValue.CompareTo(maxValue) <= 0 ? CheckStatus.Healthy : CheckStatus.Unhealthy; | |||
return HealthCheckResult.FromStatus( | |||
status, | |||
$"{name}: max={maxValue}, current={currentValue}", | |||
new Dictionary<string, object> { { "max", maxValue }, { "current", currentValue } } | |||
); | |||
}); | |||
return builder; | |||
} | |||
} | |||
} |
@ -0,0 +1,21 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Diagnostics; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public static partial class HealthCheckBuilderExtensions | |||
{ | |||
// System checks | |||
public static HealthCheckBuilder AddPrivateMemorySizeCheck(this HealthCheckBuilder builder, long maxSize) | |||
=> AddMaxValueCheck(builder, $"PrivateMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().PrivateMemorySize64); | |||
public static HealthCheckBuilder AddVirtualMemorySizeCheck(this HealthCheckBuilder builder, long maxSize) | |||
=> AddMaxValueCheck(builder, $"VirtualMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().VirtualMemorySize64); | |||
public static HealthCheckBuilder AddWorkingSetCheck(this HealthCheckBuilder builder, long maxSize) | |||
=> AddMaxValueCheck(builder, $"WorkingSet({maxSize})", maxSize, () => Process.GetCurrentProcess().WorkingSet64); | |||
} | |||
} |
@ -0,0 +1,109 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Net.Http; | |||
using System.Threading.Tasks; | |||
using Microsoft.Extensions.HealthChecks.Internal; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public static partial class HealthCheckBuilderExtensions | |||
{ | |||
// URL checks | |||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url) | |||
=> AddUrlCheck(builder, url, response => UrlChecker.DefaultUrlCheck(response)); | |||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, | |||
Func<HttpResponseMessage, IHealthCheckResult> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
return AddUrlCheck(builder, url, response => new ValueTask<IHealthCheckResult>(checkFunc(response))); | |||
} | |||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, | |||
Func<HttpResponseMessage, Task<IHealthCheckResult>> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
return AddUrlCheck(builder, url, response => new ValueTask<IHealthCheckResult>(checkFunc(response))); | |||
} | |||
public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, | |||
Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
Guard.ArgumentNotNullOrWhitespace(nameof(url), url); | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
var urlCheck = new UrlChecker(checkFunc, url); | |||
builder.AddCheck($"UrlCheck({url})", () => urlCheck.CheckAsync()); | |||
return builder; | |||
} | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName) | |||
=> AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => UrlChecker.DefaultUrlCheck(response)); | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName, | |||
Func<HttpResponseMessage, IHealthCheckResult> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
return AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => new ValueTask<IHealthCheckResult>(checkFunc(response))); | |||
} | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName, | |||
Func<HttpResponseMessage, Task<IHealthCheckResult>> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
return AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => new ValueTask<IHealthCheckResult>(checkFunc(response))); | |||
} | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName, | |||
Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
return AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => checkFunc(response)); | |||
} | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName, | |||
CheckStatus partialSuccessStatus) | |||
=> AddUrlChecks(builder, urlItems, groupName, partialSuccessStatus, response => UrlChecker.DefaultUrlCheck(response)); | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName, | |||
CheckStatus partialSuccessStatus, Func<HttpResponseMessage, IHealthCheckResult> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
return AddUrlChecks(builder, urlItems, groupName, partialSuccessStatus, response => new ValueTask<IHealthCheckResult>(checkFunc(response))); | |||
} | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName, | |||
CheckStatus partialSuccessStatus, Func<HttpResponseMessage, Task<IHealthCheckResult>> checkFunc) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
return AddUrlChecks(builder, urlItems, groupName, partialSuccessStatus, response => new ValueTask<IHealthCheckResult>(checkFunc(response))); | |||
} | |||
public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable<string> urlItems, string groupName, | |||
CheckStatus partialSuccessStatus, Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> checkFunc) | |||
{ | |||
var urls = urlItems?.ToArray(); | |||
Guard.ArgumentNotNull(nameof(builder), builder); | |||
Guard.ArgumentNotNullOrEmpty(nameof(urlItems), urls); | |||
Guard.ArgumentNotNullOrWhitespace(nameof(groupName), groupName); | |||
var urlChecker = new UrlChecker(checkFunc, urls) { PartiallyHealthyStatus = partialSuccessStatus }; | |||
builder.AddCheck($"UrlChecks({groupName})", () => urlChecker.CheckAsync()); | |||
return builder; | |||
} | |||
} | |||
} |
@ -0,0 +1,83 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
// REVIEW: Does this need to be thread safe? | |||
/// <summary> | |||
/// Represents a composite health check result built from several results. | |||
/// </summary> | |||
public class CompositeHealthCheckResult : IHealthCheckResult | |||
{ | |||
private static readonly IReadOnlyDictionary<string, object> _emptyData = new Dictionary<string, object>(); | |||
private readonly CheckStatus _initialStatus; | |||
private readonly CheckStatus _partiallyHealthyStatus; | |||
private readonly Dictionary<string, IHealthCheckResult> _results = new Dictionary<string, IHealthCheckResult>(StringComparer.OrdinalIgnoreCase); | |||
public CompositeHealthCheckResult(CheckStatus partiallyHealthyStatus = CheckStatus.Warning, | |||
CheckStatus initialStatus = CheckStatus.Unknown) | |||
{ | |||
_partiallyHealthyStatus = partiallyHealthyStatus; | |||
_initialStatus = initialStatus; | |||
} | |||
public CheckStatus CheckStatus | |||
{ | |||
get | |||
{ | |||
var checkStatuses = new HashSet<CheckStatus>(_results.Select(x => x.Value.CheckStatus)); | |||
if (checkStatuses.Count == 0) | |||
return _initialStatus; | |||
if (checkStatuses.Count == 1) | |||
return checkStatuses.First(); | |||
if (checkStatuses.Contains(CheckStatus.Healthy)) | |||
return _partiallyHealthyStatus; | |||
return CheckStatus.Unhealthy; | |||
} | |||
} | |||
public string Description => string.Join(Environment.NewLine, _results.Select(r => r.Value.Description)); | |||
public IReadOnlyDictionary<string, object> Data | |||
{ | |||
get | |||
{ | |||
var result = new Dictionary<string, object>(); | |||
foreach (var kvp in _results) | |||
result.Add(kvp.Key, kvp.Value.Data); | |||
return result; | |||
} | |||
} | |||
public IReadOnlyDictionary<string, IHealthCheckResult> Results => _results; | |||
// REVIEW: Should description be required? Seems redundant for success checks. | |||
public void Add(string name, CheckStatus status, string description) | |||
=> Add(name, status, description, null); | |||
public void Add(string name, CheckStatus status, string description, Dictionary<string, object> data) | |||
{ | |||
Guard.ArgumentNotNullOrWhitespace(nameof(name), name); | |||
Guard.ArgumentValid(status != CheckStatus.Unknown, nameof(status), "Cannot add unknown status to composite health check result"); | |||
Guard.ArgumentNotNullOrWhitespace(nameof(description), description); | |||
_results.Add(name, HealthCheckResult.FromStatus(status, description, data)); | |||
} | |||
public void Add(string name, IHealthCheckResult checkResult) | |||
{ | |||
Guard.ArgumentNotNullOrWhitespace(nameof(name), name); | |||
Guard.ArgumentNotNull(nameof(checkResult), checkResult); | |||
_results.Add(name, checkResult); | |||
} | |||
} | |||
} |
@ -0,0 +1,76 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public class HealthCheck : IHealthCheck | |||
{ | |||
private DateTimeOffset _cacheExpiration; | |||
private IHealthCheckResult _cachedResult; | |||
private volatile int _writerCount; | |||
protected HealthCheck(Func<CancellationToken, ValueTask<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
{ | |||
Guard.ArgumentNotNull(nameof(check), check); | |||
Guard.ArgumentValid(cacheDuration >= TimeSpan.Zero, nameof(cacheDuration), "Cache duration must either be zero (disabled) or a positive value"); | |||
Check = check; | |||
CacheDuration = cacheDuration; | |||
} | |||
public TimeSpan CacheDuration { get; } | |||
protected Func<CancellationToken, ValueTask<IHealthCheckResult>> Check { get; } | |||
protected virtual DateTimeOffset UtcNow => DateTimeOffset.UtcNow; | |||
public async ValueTask<IHealthCheckResult> CheckAsync(CancellationToken cancellationToken) | |||
{ | |||
while (_cacheExpiration <= UtcNow) | |||
{ | |||
// Can't use a standard lock here because of async, so we'll use this flag to determine when we should write a value, | |||
// and the waiters who aren't allowed to write will just spin wait for the new value. | |||
if (Interlocked.Exchange(ref _writerCount, 1) != 0) | |||
{ | |||
await Task.Delay(5, cancellationToken).ConfigureAwait(false); | |||
continue; | |||
} | |||
try | |||
{ | |||
_cachedResult = await Check(cancellationToken).ConfigureAwait(false); | |||
_cacheExpiration = UtcNow + CacheDuration; | |||
break; | |||
} | |||
finally | |||
{ | |||
_writerCount = 0; | |||
} | |||
} | |||
return _cachedResult; | |||
} | |||
public static HealthCheck FromCheck(Func<IHealthCheckResult> check, TimeSpan cacheDuration) | |||
=> new HealthCheck(token => new ValueTask<IHealthCheckResult>(check()), cacheDuration); | |||
public static HealthCheck FromCheck(Func<CancellationToken, IHealthCheckResult> check, TimeSpan cacheDuration) | |||
=> new HealthCheck(token => new ValueTask<IHealthCheckResult>(check(token)), cacheDuration); | |||
public static HealthCheck FromTaskCheck(Func<Task<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
=> new HealthCheck(token => new ValueTask<IHealthCheckResult>(check()), cacheDuration); | |||
public static HealthCheck FromTaskCheck(Func<CancellationToken, Task<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
=> new HealthCheck(token => new ValueTask<IHealthCheckResult>(check(token)), cacheDuration); | |||
public static HealthCheck FromValueTaskCheck(Func<ValueTask<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
=> new HealthCheck(token => check(), cacheDuration); | |||
public static HealthCheck FromValueTaskCheck(Func<CancellationToken, ValueTask<IHealthCheckResult>> check, TimeSpan cacheDuration) | |||
=> new HealthCheck(check, cacheDuration); | |||
} | |||
} |
@ -0,0 +1,40 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Collections.Generic; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public class HealthCheckBuilder | |||
{ | |||
private readonly Dictionary<string, IHealthCheck> _checks; | |||
public HealthCheckBuilder() | |||
{ | |||
_checks = new Dictionary<string, IHealthCheck>(StringComparer.OrdinalIgnoreCase); | |||
DefaultCacheDuration = TimeSpan.FromMinutes(5); | |||
} | |||
public IReadOnlyDictionary<string, IHealthCheck> Checks => _checks; | |||
public TimeSpan DefaultCacheDuration { get; private set; } | |||
public HealthCheckBuilder AddCheck(string name, IHealthCheck check) | |||
{ | |||
Guard.ArgumentNotNullOrWhitespace(nameof(name), name); | |||
Guard.ArgumentNotNull(nameof(check), check); | |||
_checks.Add(name, check); | |||
return this; | |||
} | |||
public HealthCheckBuilder WithDefaultCacheDuration(TimeSpan duration) | |||
{ | |||
Guard.ArgumentValid(duration >= TimeSpan.Zero, nameof(duration), "Duration must be zero (disabled) or a positive duration"); | |||
DefaultCacheDuration = duration; | |||
return this; | |||
} | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public static class HealthCheckExtensions | |||
{ | |||
public static ValueTask<IHealthCheckResult> CheckAsync(this IHealthCheck healthCheck) | |||
{ | |||
Guard.ArgumentNotNull(nameof(healthCheck), healthCheck); | |||
return healthCheck.CheckAsync(CancellationToken.None); | |||
} | |||
} | |||
} |
@ -0,0 +1,54 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public class HealthCheckResult : IHealthCheckResult | |||
{ | |||
private static readonly IReadOnlyDictionary<string, object> _emptyData = new Dictionary<string, object>(); | |||
public CheckStatus CheckStatus { get; } | |||
public IReadOnlyDictionary<string, object> Data { get; } | |||
public string Description { get; } | |||
private HealthCheckResult(CheckStatus checkStatus, string description, IReadOnlyDictionary<string, object> data) | |||
{ | |||
CheckStatus = checkStatus; | |||
Description = description; | |||
Data = data ?? _emptyData; | |||
} | |||
public static HealthCheckResult Unhealthy(string description) | |||
=> new HealthCheckResult(CheckStatus.Unhealthy, description, null); | |||
public static HealthCheckResult Unhealthy(string description, IReadOnlyDictionary<string, object> data) | |||
=> new HealthCheckResult(CheckStatus.Unhealthy, description, data); | |||
public static HealthCheckResult Healthy(string description) | |||
=> new HealthCheckResult(CheckStatus.Healthy, description, null); | |||
public static HealthCheckResult Healthy(string description, IReadOnlyDictionary<string, object> data) | |||
=> new HealthCheckResult(CheckStatus.Healthy, description, data); | |||
public static HealthCheckResult Warning(string description) | |||
=> new HealthCheckResult(CheckStatus.Warning, description, null); | |||
public static HealthCheckResult Warning(string description, IReadOnlyDictionary<string, object> data) | |||
=> new HealthCheckResult(CheckStatus.Warning, description, data); | |||
public static HealthCheckResult Unknown(string description) | |||
=> new HealthCheckResult(CheckStatus.Unknown, description, null); | |||
public static HealthCheckResult Unknown(string description, IReadOnlyDictionary<string, object> data) | |||
=> new HealthCheckResult(CheckStatus.Unknown, description, data); | |||
public static HealthCheckResult FromStatus(CheckStatus status, string description) | |||
=> new HealthCheckResult(status, description, null); | |||
public static HealthCheckResult FromStatus(CheckStatus status, string description, IReadOnlyDictionary<string, object> data) | |||
=> new HealthCheckResult(status, description, data); | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Collections.Generic; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public class HealthCheckResults | |||
{ | |||
public IList<IHealthCheckResult> CheckResults { get; } = new List<IHealthCheckResult>(); | |||
} | |||
} |
@ -0,0 +1,54 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using Microsoft.Extensions.Logging; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public class HealthCheckService : IHealthCheckService | |||
{ | |||
public IReadOnlyDictionary<string, IHealthCheck> _checks; | |||
private ILogger<HealthCheckService> _logger; | |||
public HealthCheckService(HealthCheckBuilder builder, ILogger<HealthCheckService> logger) | |||
{ | |||
_checks = builder.Checks; | |||
_logger = logger; | |||
} | |||
public async Task<CompositeHealthCheckResult> CheckHealthAsync(CheckStatus partiallyHealthyStatus, CancellationToken cancellationToken) | |||
{ | |||
var logMessage = new StringBuilder(); | |||
var result = new CompositeHealthCheckResult(partiallyHealthyStatus); | |||
foreach (var check in _checks) | |||
{ | |||
try | |||
{ | |||
var healthCheckResult = await check.Value.CheckAsync().ConfigureAwait(false); | |||
logMessage.AppendLine($"HealthCheck: {check.Key} : {healthCheckResult.CheckStatus}"); | |||
result.Add(check.Key, healthCheckResult); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logMessage.AppendLine($"HealthCheck: {check.Key} : Exception {ex.GetType().FullName} thrown"); | |||
result.Add(check.Key, CheckStatus.Unhealthy, $"Exception during check: {ex.GetType().FullName}"); | |||
} | |||
} | |||
if (logMessage.Length == 0) | |||
logMessage.AppendLine("HealthCheck: No checks have been registered"); | |||
_logger.Log((result.CheckStatus == CheckStatus.Healthy ? LogLevel.Information : LogLevel.Error), 0, logMessage.ToString(), null, MessageFormatter); | |||
return result; | |||
} | |||
private static string MessageFormatter(string state, Exception error) => state; | |||
} | |||
} |
@ -0,0 +1,22 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using Microsoft.Extensions.HealthChecks; | |||
namespace Microsoft.Extensions.DependencyInjection | |||
{ | |||
public static class HealthCheckServiceCollectionExtensions | |||
{ | |||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, Action<HealthCheckBuilder> checkupAction) | |||
{ | |||
var checkupBuilder = new HealthCheckBuilder(); | |||
checkupAction.Invoke(checkupBuilder); | |||
services.AddSingleton(checkupBuilder); | |||
services.AddSingleton<IHealthCheckService, HealthCheckService>(); | |||
return services; | |||
} | |||
} | |||
} |
@ -0,0 +1,38 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public static class HealthCheckServiceExtensions | |||
{ | |||
public static Task<CompositeHealthCheckResult> CheckHealthAsync(this IHealthCheckService service) | |||
{ | |||
Guard.ArgumentNotNull(nameof(service), service); | |||
return service.CheckHealthAsync(CheckStatus.Unhealthy, CancellationToken.None); | |||
} | |||
public static Task<CompositeHealthCheckResult> CheckHealthAsync(this IHealthCheckService service, CheckStatus partiallyHealthyStatus) | |||
{ | |||
Guard.ArgumentNotNull(nameof(service), service); | |||
return service.CheckHealthAsync(partiallyHealthyStatus, CancellationToken.None); | |||
} | |||
public static Task<CompositeHealthCheckResult> CheckHealthAsync(this IHealthCheckService service, CancellationToken cancellationToken) | |||
{ | |||
Guard.ArgumentNotNull(nameof(service), service); | |||
return service.CheckHealthAsync(CheckStatus.Unhealthy, cancellationToken); | |||
} | |||
public static Task<CompositeHealthCheckResult> CheckHealthAsync(this IHealthCheckService service, CheckStatus partiallyHealthyStatus, CancellationToken cancellationToken) | |||
{ | |||
Guard.ArgumentNotNull(nameof(service), service); | |||
return service.CheckHealthAsync(partiallyHealthyStatus, cancellationToken); | |||
} | |||
} | |||
} |
@ -0,0 +1,16 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public interface IHealthCheck | |||
{ | |||
TimeSpan CacheDuration { get; } | |||
ValueTask<IHealthCheckResult> CheckAsync(CancellationToken cancellationToken); | |||
} | |||
} |
@ -0,0 +1,14 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Collections.Generic; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public interface IHealthCheckResult | |||
{ | |||
CheckStatus CheckStatus { get; } | |||
string Description { get; } | |||
IReadOnlyDictionary<string, object> Data { get; } | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.Extensions.HealthChecks | |||
{ | |||
public interface IHealthCheckService | |||
{ | |||
Task<CompositeHealthCheckResult> CheckHealthAsync(CheckStatus partiallyHealthyStatus, CancellationToken cancellationToken); | |||
} | |||
} |
@ -0,0 +1,92 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Net; | |||
using System.Net.Http; | |||
using System.Net.Http.Headers; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.Extensions.HealthChecks.Internal | |||
{ | |||
public class UrlChecker | |||
{ | |||
private readonly Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> _checkFunc; | |||
private readonly string[] _urls; | |||
// REVIEW: Cache timeout here? | |||
public UrlChecker(Func<HttpResponseMessage, ValueTask<IHealthCheckResult>> checkFunc, params string[] urls) | |||
{ | |||
Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); | |||
Guard.ArgumentNotNullOrEmpty(nameof(urls), urls); | |||
_checkFunc = checkFunc; | |||
_urls = urls; | |||
} | |||
public CheckStatus PartiallyHealthyStatus { get; set; } = CheckStatus.Warning; | |||
public Task<IHealthCheckResult> CheckAsync() | |||
=> _urls.Length == 1 ? CheckSingleAsync() : CheckMultiAsync(); | |||
public async Task<IHealthCheckResult> CheckSingleAsync() | |||
{ | |||
var httpClient = CreateHttpClient(); | |||
var result = default(IHealthCheckResult); | |||
await CheckUrlAsync(httpClient, _urls[0], (_, checkResult) => result = checkResult).ConfigureAwait(false); | |||
return result; | |||
} | |||
public async Task<IHealthCheckResult> CheckMultiAsync() | |||
{ | |||
var composite = new CompositeHealthCheckResult(PartiallyHealthyStatus); | |||
var httpClient = CreateHttpClient(); | |||
// REVIEW: Should these be done in parallel? | |||
foreach (var url in _urls) | |||
await CheckUrlAsync(httpClient, url, (name, checkResult) => composite.Add(name, checkResult)).ConfigureAwait(false); | |||
return composite; | |||
} | |||
private async Task CheckUrlAsync(HttpClient httpClient, string url, Action<string, IHealthCheckResult> adder) | |||
{ | |||
var name = $"UrlCheck({url})"; | |||
try | |||
{ | |||
var response = await httpClient.GetAsync(url).ConfigureAwait(false); | |||
var result = await _checkFunc(response); | |||
adder(name, result); | |||
} | |||
catch (Exception ex) | |||
{ | |||
adder(name, HealthCheckResult.Unhealthy($"Exception during check: {ex.GetType().FullName}")); | |||
} | |||
} | |||
private HttpClient CreateHttpClient() | |||
{ | |||
var httpClient = GetHttpClient(); | |||
httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { NoCache = true }; | |||
return httpClient; | |||
} | |||
public static async ValueTask<IHealthCheckResult> DefaultUrlCheck(HttpResponseMessage response) | |||
{ | |||
// REVIEW: Should this be an explicit 200 check, or just an "is success" check? | |||
var status = response.StatusCode == HttpStatusCode.OK ? CheckStatus.Healthy : CheckStatus.Unhealthy; | |||
var data = new Dictionary<string, object> | |||
{ | |||
{ "url", response.RequestMessage.RequestUri.ToString() }, | |||
{ "status", (int)response.StatusCode }, | |||
{ "reason", response.ReasonPhrase }, | |||
{ "body", await response.Content?.ReadAsStringAsync() } | |||
}; | |||
return HealthCheckResult.FromStatus(status, $"UrlCheck({response.RequestMessage.RequestUri}): status code {response.StatusCode} ({(int)response.StatusCode})", data); | |||
} | |||
protected virtual HttpClient GetHttpClient() | |||
=> new HttpClient(); | |||
} | |||
} |
@ -0,0 +1,20 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netstandard1.3</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Compile Include="..\common\Guard.cs" Link="Internal\Guard.cs" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.0.2" /> | |||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" /> | |||
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" /> | |||
<PackageReference Include="System.Threading.Tasks.Parallel" Version="4.3.0" /> | |||
<PackageReference Include="System.Threading.Thread" Version="4.3.0" /> | |||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.3.0" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,45 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
using System; | |||
using System.Collections.Generic; | |||
static class Guard | |||
{ | |||
public static void ArgumentNotNull(string argumentName, object value) | |||
{ | |||
if (value == null) | |||
throw new ArgumentNullException(argumentName); | |||
} | |||
public static void ArgumentNotNullOrEmpty<T>(string argumentName, string value) | |||
{ | |||
if (value == null) | |||
throw new ArgumentNullException(argumentName); | |||
if (string.IsNullOrEmpty(value)) | |||
throw new ArgumentException("Value cannot be an empty string", argumentName); | |||
} | |||
// Use IReadOnlyCollection<T> instead of IEnumerable<T> to discourage double enumeration | |||
public static void ArgumentNotNullOrEmpty<T>(string argumentName, IReadOnlyCollection<T> items) | |||
{ | |||
if (items == null) | |||
throw new ArgumentNullException(argumentName); | |||
if (items.Count == 0) | |||
throw new ArgumentException("Collection must contain at least one item", argumentName); | |||
} | |||
public static void ArgumentNotNullOrWhitespace(string argumentName, string value) | |||
{ | |||
if (value == null) | |||
throw new ArgumentNullException(argumentName); | |||
if (string.IsNullOrWhiteSpace(value)) | |||
throw new ArgumentException("Value must contain a non-whitespace value", argumentName); | |||
} | |||
public static void ArgumentValid(bool valid, string argumentName, string exceptionMessage) | |||
{ | |||
if (!valid) | |||
throw new ArgumentException(exceptionMessage, argumentName); | |||
} | |||
} |
@ -0,0 +1,8 @@ | |||
FROM microsoft/dotnet:1.1-runtime-nanoserver | |||
SHELL ["powershell"] | |||
ARG source | |||
WORKDIR /app | |||
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord | |||
EXPOSE 80 | |||
COPY ${source:-obj/Docker/publish} . | |||
ENTRYPOINT ["dotnet", "Basket.API.dll"] |
@ -0,0 +1,18 @@ | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.AspNetCore.Mvc; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Basket.API.Infrastructure.ActionResults | |||
{ | |||
public class InternalServerErrorObjectResult : ObjectResult | |||
{ | |||
public InternalServerErrorObjectResult(object error) | |||
: base(error) | |||
{ | |||
StatusCode = StatusCodes.Status500InternalServerError; | |||
} | |||
} | |||
} |
@ -0,0 +1,24 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Basket.API.Infrastructure.Exceptions | |||
{ | |||
/// <summary> | |||
/// Exception type for app exceptions | |||
/// </summary> | |||
public class BasketDomainException : Exception | |||
{ | |||
public BasketDomainException() | |||
{ } | |||
public BasketDomainException(string message) | |||
: base(message) | |||
{ } | |||
public BasketDomainException(string message, Exception innerException) | |||
: base(message, innerException) | |||
{ } | |||
} | |||
} |
@ -0,0 +1,67 @@ | |||
using Basket.API.Infrastructure.ActionResults; | |||
using Basket.API.Infrastructure.Exceptions; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.AspNetCore.Mvc.Filters; | |||
using Microsoft.Extensions.Logging; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Threading.Tasks; | |||
namespace Basket.API.Infrastructure.Filters | |||
{ | |||
public class HttpGlobalExceptionFilter : IExceptionFilter | |||
{ | |||
private readonly IHostingEnvironment env; | |||
private readonly ILogger<HttpGlobalExceptionFilter> logger; | |||
public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger<HttpGlobalExceptionFilter> logger) | |||
{ | |||
this.env = env; | |||
this.logger = logger; | |||
} | |||
public void OnException(ExceptionContext context) | |||
{ | |||
logger.LogError(new EventId(context.Exception.HResult), | |||
context.Exception, | |||
context.Exception.Message); | |||
if (context.Exception.GetType() == typeof(BasketDomainException)) | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { context.Exception.Message } | |||
}; | |||
context.Result = new BadRequestObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; | |||
} | |||
else | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { "An error ocurr.Try it again." } | |||
}; | |||
if (env.IsDevelopment()) | |||
{ | |||
json.DeveloperMeesage = context.Exception; | |||
} | |||
context.Result = new InternalServerErrorObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||
} | |||
context.ExceptionHandled = true; | |||
} | |||
private class JsonErrorResponse | |||
{ | |||
public string[] Messages { get; set; } | |||
public object DeveloperMeesage { get; set; } | |||
} | |||
} | |||
} |
@ -0,0 +1,46 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; | |||
using Microsoft.eShopOnContainers.Services.Basket.API.Model; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling | |||
{ | |||
public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent> | |||
{ | |||
private readonly IBasketRepository _repository; | |||
public ProductPriceChangedIntegrationEventHandler(IBasketRepository repository) | |||
{ | |||
_repository = repository; | |||
} | |||
public async Task Handle(ProductPriceChangedIntegrationEvent @event) | |||
{ | |||
var userIds = await _repository.GetUsers(); | |||
foreach (var id in userIds) | |||
{ | |||
var basket = await _repository.GetBasketAsync(id); | |||
await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, @event.OldPrice, 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(); | |||
if (itemsToUpdate != null) | |||
{ | |||
foreach (var item in itemsToUpdate) | |||
{ | |||
if(item.UnitPrice == oldPrice) | |||
{ | |||
var originalPrice = item.UnitPrice; | |||
item.UnitPrice = newPrice; | |||
item.OldUnitPrice = originalPrice; | |||
} | |||
} | |||
await _repository.UpdateBasketAsync(basket); | |||
} | |||
} | |||
} | |||
} | |||
@ -0,0 +1,26 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events | |||
{ | |||
// Integration Events notes: | |||
// An Event is “something that has happened in the past”, therefore its name has to be | |||
// An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems. | |||
public class ProductPriceChangedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int ProductId { get; private set; } | |||
public decimal NewPrice { get; private set; } | |||
public decimal OldPrice { get; private set; } | |||
public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) | |||
{ | |||
ProductId = productId; | |||
NewPrice = newPrice; | |||
OldPrice = oldPrice; | |||
} | |||
} | |||
} |
@ -0,0 +1,8 @@ | |||
FROM microsoft/dotnet:1.1-runtime-nanoserver | |||
SHELL ["powershell"] | |||
ARG source | |||
WORKDIR /app | |||
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord | |||
EXPOSE 80 | |||
COPY ${source:-obj/Docker/publish} . | |||
ENTRYPOINT ["dotnet", "Catalog.API.dll"] |
@ -0,0 +1,18 @@ | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.AspNetCore.Mvc; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Catalog.API.Infrastructure.ActionResults | |||
{ | |||
public class InternalServerErrorObjectResult : ObjectResult | |||
{ | |||
public InternalServerErrorObjectResult(object error) | |||
: base(error) | |||
{ | |||
StatusCode = StatusCodes.Status500InternalServerError; | |||
} | |||
} | |||
} |
@ -0,0 +1,122 @@ | |||
using System; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Metadata; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
[DbContext(typeof(CatalogContext))] | |||
[Migration("20170314083211_AddEventTable")] | |||
partial class AddEventTable | |||
{ | |||
protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
{ | |||
modelBuilder | |||
.HasAnnotation("ProductVersion", "1.1.0-rtm-22752") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_brand_hilo", "'catalog_brand_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_hilo", "'catalog_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_type_hilo", "'catalog_type_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Brand") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogBrand"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<int>("CatalogBrandId"); | |||
b.Property<int>("CatalogTypeId"); | |||
b.Property<string>("Description"); | |||
b.Property<string>("Name") | |||
.IsRequired() | |||
.HasMaxLength(50); | |||
b.Property<string>("PictureUri"); | |||
b.Property<decimal>("Price"); | |||
b.HasKey("Id"); | |||
b.HasIndex("CatalogBrandId"); | |||
b.HasIndex("CatalogTypeId"); | |||
b.ToTable("Catalog"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Type") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogType"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Common.Infrastructure.Data.IntegrationEvent", b => | |||
{ | |||
b.Property<Guid>("EventId") | |||
.ValueGeneratedOnAdd(); | |||
b.Property<string>("Content") | |||
.IsRequired(); | |||
b.Property<DateTime>("CreationTime"); | |||
b.Property<string>("EventTypeName") | |||
.IsRequired() | |||
.HasMaxLength(200); | |||
b.Property<int>("State"); | |||
b.Property<int>("TimesSent"); | |||
b.HasKey("EventId"); | |||
b.ToTable("IntegrationEvent"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", "CatalogBrand") | |||
.WithMany() | |||
.HasForeignKey("CatalogBrandId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", "CatalogType") | |||
.WithMany() | |||
.HasForeignKey("CatalogTypeId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,34 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
public partial class AddEventTable : Migration | |||
{ | |||
protected override void Up(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.CreateTable( | |||
name: "IntegrationEvent", | |||
columns: table => new | |||
{ | |||
EventId = table.Column<Guid>(nullable: false), | |||
Content = table.Column<string>(nullable: false), | |||
CreationTime = table.Column<DateTime>(nullable: false), | |||
EventTypeName = table.Column<string>(maxLength: 200, nullable: false), | |||
State = table.Column<int>(nullable: false), | |||
TimesSent = table.Column<int>(nullable: false) | |||
}, | |||
constraints: table => | |||
{ | |||
table.PrimaryKey("PK_IntegrationEvent", x => x.EventId); | |||
}); | |||
} | |||
protected override void Down(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.DropTable( | |||
name: "IntegrationEvent"); | |||
} | |||
} | |||
} |
@ -0,0 +1,121 @@ | |||
using System; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Metadata; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
[DbContext(typeof(CatalogContext))] | |||
[Migration("20170316012921_RefactoringToIntegrationEventLog")] | |||
partial class RefactoringToIntegrationEventLog | |||
{ | |||
protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
{ | |||
modelBuilder | |||
.HasAnnotation("ProductVersion", "1.1.1") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_brand_hilo", "'catalog_brand_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_hilo", "'catalog_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_type_hilo", "'catalog_type_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Brand") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogBrand"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<int>("CatalogBrandId"); | |||
b.Property<int>("CatalogTypeId"); | |||
b.Property<string>("Description"); | |||
b.Property<string>("Name") | |||
.IsRequired() | |||
.HasMaxLength(50); | |||
b.Property<string>("PictureUri"); | |||
b.Property<decimal>("Price"); | |||
b.HasKey("Id"); | |||
b.HasIndex("CatalogBrandId"); | |||
b.HasIndex("CatalogTypeId"); | |||
b.ToTable("Catalog"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Type") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogType"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Common.Infrastructure.Data.IntegrationEventLogEntry", b => | |||
{ | |||
b.Property<Guid>("EventId") | |||
.ValueGeneratedOnAdd(); | |||
b.Property<string>("Content") | |||
.IsRequired(); | |||
b.Property<DateTime>("CreationTime"); | |||
b.Property<string>("EventTypeName") | |||
.IsRequired(); | |||
b.Property<int>("State"); | |||
b.Property<int>("TimesSent"); | |||
b.HasKey("EventId"); | |||
b.ToTable("IntegrationEventLog"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", "CatalogBrand") | |||
.WithMany() | |||
.HasForeignKey("CatalogBrandId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", "CatalogType") | |||
.WithMany() | |||
.HasForeignKey("CatalogTypeId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,53 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
public partial class RefactoringToIntegrationEventLog : Migration | |||
{ | |||
protected override void Up(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.DropTable( | |||
name: "IntegrationEvent"); | |||
migrationBuilder.CreateTable( | |||
name: "IntegrationEventLog", | |||
columns: table => new | |||
{ | |||
EventId = table.Column<Guid>(nullable: false), | |||
Content = table.Column<string>(nullable: false), | |||
CreationTime = table.Column<DateTime>(nullable: false), | |||
EventTypeName = table.Column<string>(nullable: false), | |||
State = table.Column<int>(nullable: false), | |||
TimesSent = table.Column<int>(nullable: false) | |||
}, | |||
constraints: table => | |||
{ | |||
table.PrimaryKey("PK_IntegrationEventLog", x => x.EventId); | |||
}); | |||
} | |||
protected override void Down(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.DropTable( | |||
name: "IntegrationEventLog"); | |||
migrationBuilder.CreateTable( | |||
name: "IntegrationEvent", | |||
columns: table => new | |||
{ | |||
EventId = table.Column<Guid>(nullable: false), | |||
Content = table.Column<string>(nullable: false), | |||
CreationTime = table.Column<DateTime>(nullable: false), | |||
EventTypeName = table.Column<string>(maxLength: 200, nullable: false), | |||
State = table.Column<int>(nullable: false), | |||
TimesSent = table.Column<int>(nullable: false) | |||
}, | |||
constraints: table => | |||
{ | |||
table.PrimaryKey("PK_IntegrationEvent", x => x.EventId); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,122 @@ | |||
using System; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Metadata; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
[DbContext(typeof(CatalogContext))] | |||
[Migration("20170316120022_RefactoringEventBusNamespaces")] | |||
partial class RefactoringEventBusNamespaces | |||
{ | |||
protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
{ | |||
modelBuilder | |||
.HasAnnotation("ProductVersion", "1.1.1") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_brand_hilo", "'catalog_brand_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_hilo", "'catalog_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_type_hilo", "'catalog_type_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events.IntegrationEventLogEntry", b => | |||
{ | |||
b.Property<Guid>("EventId") | |||
.ValueGeneratedOnAdd(); | |||
b.Property<string>("Content") | |||
.IsRequired(); | |||
b.Property<DateTime>("CreationTime"); | |||
b.Property<string>("EventTypeName") | |||
.IsRequired(); | |||
b.Property<int>("State"); | |||
b.Property<int>("TimesSent"); | |||
b.HasKey("EventId"); | |||
b.ToTable("IntegrationEventLog"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Brand") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogBrand"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<int>("CatalogBrandId"); | |||
b.Property<int>("CatalogTypeId"); | |||
b.Property<string>("Description"); | |||
b.Property<string>("Name") | |||
.IsRequired() | |||
.HasMaxLength(50); | |||
b.Property<string>("PictureUri"); | |||
b.Property<decimal>("Price"); | |||
b.HasKey("Id"); | |||
b.HasIndex("CatalogBrandId"); | |||
b.HasIndex("CatalogTypeId"); | |||
b.ToTable("Catalog"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Type") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogType"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", "CatalogBrand") | |||
.WithMany() | |||
.HasForeignKey("CatalogBrandId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", "CatalogType") | |||
.WithMany() | |||
.HasForeignKey("CatalogTypeId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,19 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
public partial class RefactoringEventBusNamespaces : Migration | |||
{ | |||
protected override void Up(MigrationBuilder migrationBuilder) | |||
{ | |||
} | |||
protected override void Down(MigrationBuilder migrationBuilder) | |||
{ | |||
} | |||
} | |||
} |
@ -0,0 +1,99 @@ | |||
using System; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Metadata; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
[DbContext(typeof(CatalogContext))] | |||
[Migration("20170322124244_RemoveIntegrationEventLogs")] | |||
partial class RemoveIntegrationEventLogs | |||
{ | |||
protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
{ | |||
modelBuilder | |||
.HasAnnotation("ProductVersion", "1.1.1") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_brand_hilo", "'catalog_brand_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_hilo", "'catalog_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:Sequence:.catalog_type_hilo", "'catalog_type_hilo', '', '1', '10', '', '', 'Int64', 'False'") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_brand_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Brand") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogBrand"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<int>("CatalogBrandId"); | |||
b.Property<int>("CatalogTypeId"); | |||
b.Property<string>("Description"); | |||
b.Property<string>("Name") | |||
.IsRequired() | |||
.HasMaxLength(50); | |||
b.Property<string>("PictureUri"); | |||
b.Property<decimal>("Price"); | |||
b.HasKey("Id"); | |||
b.HasIndex("CatalogBrandId"); | |||
b.HasIndex("CatalogTypeId"); | |||
b.ToTable("Catalog"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", b => | |||
{ | |||
b.Property<int>("Id") | |||
.ValueGeneratedOnAdd() | |||
.HasAnnotation("SqlServer:HiLoSequenceName", "catalog_type_hilo") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); | |||
b.Property<string>("Type") | |||
.IsRequired() | |||
.HasMaxLength(100); | |||
b.HasKey("Id"); | |||
b.ToTable("CatalogType"); | |||
}); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogItem", b => | |||
{ | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogBrand", "CatalogBrand") | |||
.WithMany() | |||
.HasForeignKey("CatalogBrandId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
b.HasOne("Microsoft.eShopOnContainers.Services.Catalog.API.Model.CatalogType", "CatalogType") | |||
.WithMany() | |||
.HasForeignKey("CatalogTypeId") | |||
.OnDelete(DeleteBehavior.Cascade); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,34 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
namespace Catalog.API.Infrastructure.Migrations | |||
{ | |||
public partial class RemoveIntegrationEventLogs : Migration | |||
{ | |||
protected override void Up(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.DropTable( | |||
name: "IntegrationEventLog"); | |||
} | |||
protected override void Down(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.CreateTable( | |||
name: "IntegrationEventLog", | |||
columns: table => new | |||
{ | |||
EventId = table.Column<Guid>(nullable: false), | |||
Content = table.Column<string>(nullable: false), | |||
CreationTime = table.Column<DateTime>(nullable: false), | |||
EventTypeName = table.Column<string>(nullable: false), | |||
State = table.Column<int>(nullable: false), | |||
TimesSent = table.Column<int>(nullable: false) | |||
}, | |||
constraints: table => | |||
{ | |||
table.PrimaryKey("PK_IntegrationEventLog", x => x.EventId); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,24 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Catalog.API.Infrastructure.Exceptions | |||
{ | |||
/// <summary> | |||
/// Exception type for app exceptions | |||
/// </summary> | |||
public class CatalogDomainException : Exception | |||
{ | |||
public CatalogDomainException() | |||
{ } | |||
public CatalogDomainException(string message) | |||
: base(message) | |||
{ } | |||
public CatalogDomainException(string message, Exception innerException) | |||
: base(message, innerException) | |||
{ } | |||
} | |||
} |
@ -0,0 +1,63 @@ | |||
using Catalog.API.Infrastructure.ActionResults; | |||
using Catalog.API.Infrastructure.Exceptions; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.AspNetCore.Mvc.Filters; | |||
using Microsoft.Extensions.Logging; | |||
using System.Net; | |||
namespace Catalog.API.Infrastructure.Filters | |||
{ | |||
public class HttpGlobalExceptionFilter : IExceptionFilter | |||
{ | |||
private readonly IHostingEnvironment env; | |||
private readonly ILogger<HttpGlobalExceptionFilter> logger; | |||
public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger<HttpGlobalExceptionFilter> logger) | |||
{ | |||
this.env = env; | |||
this.logger = logger; | |||
} | |||
public void OnException(ExceptionContext context) | |||
{ | |||
logger.LogError(new EventId(context.Exception.HResult), | |||
context.Exception, | |||
context.Exception.Message); | |||
if (context.Exception.GetType() == typeof(CatalogDomainException)) | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { context.Exception.Message } | |||
}; | |||
context.Result = new BadRequestObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; | |||
} | |||
else | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { "An error ocurr.Try it again." } | |||
}; | |||
if (env.IsDevelopment()) | |||
{ | |||
json.DeveloperMeesage = context.Exception; | |||
} | |||
context.Result = new InternalServerErrorObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||
} | |||
context.ExceptionHandled = true; | |||
} | |||
private class JsonErrorResponse | |||
{ | |||
public string[] Messages { get; set; } | |||
public object DeveloperMeesage { get; set; } | |||
} | |||
} | |||
} |
@ -0,0 +1,43 @@ | |||
using System; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Metadata; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | |||
namespace Catalog.API.Migrations | |||
{ | |||
[DbContext(typeof(IntegrationEventLogContext))] | |||
[Migration("20170322145434_IntegrationEventInitial")] | |||
partial class IntegrationEventInitial | |||
{ | |||
protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
{ | |||
modelBuilder | |||
.HasAnnotation("ProductVersion", "1.1.1") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.IntegrationEventLogEntry", b => | |||
{ | |||
b.Property<Guid>("EventId") | |||
.ValueGeneratedOnAdd(); | |||
b.Property<string>("Content") | |||
.IsRequired(); | |||
b.Property<DateTime>("CreationTime"); | |||
b.Property<string>("EventTypeName") | |||
.IsRequired(); | |||
b.Property<int>("State"); | |||
b.Property<int>("TimesSent"); | |||
b.HasKey("EventId"); | |||
b.ToTable("IntegrationEventLog"); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,34 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
namespace Catalog.API.Migrations | |||
{ | |||
public partial class IntegrationEventInitial : Migration | |||
{ | |||
protected override void Up(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.CreateTable( | |||
name: "IntegrationEventLog", | |||
columns: table => new | |||
{ | |||
EventId = table.Column<Guid>(nullable: false), | |||
Content = table.Column<string>(nullable: false), | |||
CreationTime = table.Column<DateTime>(nullable: false), | |||
EventTypeName = table.Column<string>(nullable: false), | |||
State = table.Column<int>(nullable: false), | |||
TimesSent = table.Column<int>(nullable: false) | |||
}, | |||
constraints: table => | |||
{ | |||
table.PrimaryKey("PK_IntegrationEventLog", x => x.EventId); | |||
}); | |||
} | |||
protected override void Down(MigrationBuilder migrationBuilder) | |||
{ | |||
migrationBuilder.DropTable( | |||
name: "IntegrationEventLog"); | |||
} | |||
} | |||
} |
@ -0,0 +1,42 @@ | |||
using System; | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
using Microsoft.EntityFrameworkCore.Metadata; | |||
using Microsoft.EntityFrameworkCore.Migrations; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | |||
namespace Catalog.API.Migrations | |||
{ | |||
[DbContext(typeof(IntegrationEventLogContext))] | |||
partial class IntegrationEventLogContextModelSnapshot : ModelSnapshot | |||
{ | |||
protected override void BuildModel(ModelBuilder modelBuilder) | |||
{ | |||
modelBuilder | |||
.HasAnnotation("ProductVersion", "1.1.1") | |||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); | |||
modelBuilder.Entity("Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.IntegrationEventLogEntry", b => | |||
{ | |||
b.Property<Guid>("EventId") | |||
.ValueGeneratedOnAdd(); | |||
b.Property<string>("Content") | |||
.IsRequired(); | |||
b.Property<DateTime>("CreationTime"); | |||
b.Property<string>("EventTypeName") | |||
.IsRequired(); | |||
b.Property<int>("State"); | |||
b.Property<int>("TimesSent"); | |||
b.HasKey("EventId"); | |||
b.ToTable("IntegrationEventLog"); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,3 @@ | |||
| |||
// To implement ProductPriceChangedEvent.cs here |
@ -0,0 +1,26 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events | |||
{ | |||
// Integration Events notes: | |||
// An Event is “something that has happened in the past”, therefore its name has to be | |||
// An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems. | |||
public class ProductPriceChangedIntegrationEvent : IntegrationEvent | |||
{ | |||
public int ProductId { get; private set; } | |||
public decimal NewPrice { get; private set; } | |||
public decimal OldPrice { get; private set; } | |||
public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice) | |||
{ | |||
ProductId = productId; | |||
NewPrice = newPrice; | |||
OldPrice = oldPrice; | |||
} | |||
} | |||
} |
@ -0,0 +1,8 @@ | |||
FROM microsoft/dotnet:1.1-runtime-nanoserver | |||
SHELL ["powershell"] | |||
ARG source | |||
WORKDIR /app | |||
RUN set-itemproperty -path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' -Name ServerPriorityTimeLimit -Value 0 -Type DWord | |||
EXPOSE 80 | |||
COPY ${source:-obj/Docker/publish} . | |||
ENTRYPOINT ["dotnet", "Identity.API.dll"] |