Browse Source

Adding various language enhancements, removing unused/duplicated extension

pull/1884/head
szukuro 2 years ago
parent
commit
303c958e43
29 changed files with 440 additions and 664 deletions
  1. +1
    -1
      src/ApiGateways/Mobile.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs
  2. +1
    -1
      src/ApiGateways/Web.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs
  3. +46
    -53
      src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs
  4. +18
    -21
      src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs
  5. +3
    -5
      src/BuildingBlocks/EventBus/IntegrationEventLogEF/Utilities/ResilientTransaction.cs
  6. +34
    -36
      src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHostExtensions.cs
  7. +1
    -1
      src/Services/Basket/Basket.API/Infrastructure/Middlewares/AuthorizeCheckOperationFilter.cs
  8. +17
    -23
      src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs
  9. +22
    -26
      src/Services/Basket/Basket.FunctionalTests/RedisBasketRepositoryTests.cs
  10. +11
    -33
      src/Services/Catalog/Catalog.API/Controllers/PicController.cs
  11. +0
    -122
      src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs
  12. +33
    -35
      src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs
  13. +21
    -21
      src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs
  14. +50
    -72
      src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarios.cs
  15. +2
    -2
      src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs
  16. +10
    -12
      src/Services/Catalog/Catalog.UnitTests/Application/CatalogControllerTest.cs
  17. +7
    -11
      src/Services/Identity/Identity.API/Certificate/Certificate.cs
  18. +11
    -13
      src/Services/Identity/Identity.API/Data/ApplicationDbContextSeed.cs
  19. +35
    -37
      src/Services/Identity/Identity.API/IWebHostExtensions.cs
  20. +1
    -1
      src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs
  21. +15
    -21
      src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs
  22. +1
    -1
      src/Services/Ordering/Ordering.API/Infrastructure/Filters/AuthorizeCheckOperationFilter.cs
  23. +13
    -15
      src/Services/Ordering/Ordering.BackgroundTasks/Services/GracePeriodManagerService.cs
  24. +14
    -20
      src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs
  25. +1
    -1
      src/Services/Webhooks/Webhooks.API/Infrastructure/AuthorizeCheckOperationFilter.cs
  26. +30
    -32
      src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs
  27. +20
    -22
      src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenarios.cs
  28. +11
    -13
      src/Web/WebMVC/Infrastructure/WebContextSeed.cs
  29. +11
    -13
      src/Web/WebSPA/Server/Infrastructure/WebContextSeed.cs

+ 1
- 1
src/ApiGateways/Mobile.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs View File

@ -22,7 +22,7 @@
operation.Security = new List<OpenApiSecurityRequirement> operation.Security = new List<OpenApiSecurityRequirement>
{ {
new OpenApiSecurityRequirement
new()
{ {
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" } [ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" }
} }


+ 1
- 1
src/ApiGateways/Web.Bff.Shopping/aggregator/Filters/AuthorizeCheckOperationFilter.cs View File

@ -22,7 +22,7 @@
operation.Security = new List<OpenApiSecurityRequirement> operation.Security = new List<OpenApiSecurityRequirement>
{ {
new OpenApiSecurityRequirement
new()
{ {
[ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" } [ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" }
} }


+ 46
- 53
src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs View File

@ -34,17 +34,15 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
_persistentConnection.TryConnect(); _persistentConnection.TryConnect();
} }
using (var channel = _persistentConnection.CreateModel())
{
channel.QueueUnbind(queue: _queueName,
exchange: BROKER_NAME,
routingKey: eventName);
using var channel = _persistentConnection.CreateModel();
channel.QueueUnbind(queue: _queueName,
exchange: BROKER_NAME,
routingKey: eventName);
if (_subsManager.IsEmpty)
{
_queueName = string.Empty;
_consumerChannel.Close();
}
if (_subsManager.IsEmpty)
{
_queueName = string.Empty;
_consumerChannel.Close();
} }
} }
@ -66,32 +64,30 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
_logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", @event.Id, eventName); _logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", @event.Id, eventName);
using (var channel = _persistentConnection.CreateModel())
{
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", @event.Id);
using var channel = _persistentConnection.CreateModel();
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", @event.Id);
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions
{
WriteIndented = true
});
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
policy.Execute(() =>
{
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2; // persistent
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions
{
WriteIndented = true
});
policy.Execute(() =>
{
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2; // persistent
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id); _logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id);
channel.BasicPublish(
exchange: BROKER_NAME,
routingKey: eventName,
mandatory: true,
basicProperties: properties,
body: body);
});
}
channel.BasicPublish(
exchange: BROKER_NAME,
routingKey: eventName,
mandatory: true,
basicProperties: properties,
body: body);
});
} }
public void SubscribeDynamic<TH>(string eventName) public void SubscribeDynamic<TH>(string eventName)
@ -244,30 +240,27 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
if (_subsManager.HasSubscriptionsForEvent(eventName)) if (_subsManager.HasSubscriptionsForEvent(eventName))
{ {
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
using var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME);
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
{ {
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
if (subscription.IsDynamic)
{
if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue;
using dynamic eventData = JsonDocument.Parse(message);
await Task.Yield();
await handler.Handle(eventData);
}
else
{ {
if (subscription.IsDynamic)
{
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
if (handler == null) continue;
using dynamic eventData = JsonDocument.Parse(message);
await Task.Yield();
await handler.Handle(eventData);
}
else
{
var handler = scope.ResolveOptional(subscription.HandlerType);
if (handler == null) continue;
var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive= true});
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await Task.Yield();
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
}
var handler = scope.ResolveOptional(subscription.HandlerType);
if (handler == null) continue;
var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await Task.Yield();
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
} }
} }
} }


+ 18
- 21
src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.cs View File

@ -155,32 +155,29 @@ public class EventBusServiceBus : IEventBus, IDisposable
var processed = false; var processed = false;
if (_subsManager.HasSubscriptionsForEvent(eventName)) if (_subsManager.HasSubscriptionsForEvent(eventName))
{ {
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME);
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
{ {
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
if (subscription.IsDynamic)
{ {
if (subscription.IsDynamic)
{
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
if (handler == null) continue;
using dynamic eventData = JsonDocument.Parse(message);
await handler.Handle(eventData);
}
else
{
var handler = scope.ResolveOptional(subscription.HandlerType);
if (handler == null) continue;
var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonSerializer.Deserialize(message, eventType);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
}
if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue;
using dynamic eventData = JsonDocument.Parse(message);
await handler.Handle(eventData);
}
else
{
var handler = scope.ResolveOptional(subscription.HandlerType);
if (handler == null) continue;
var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonSerializer.Deserialize(message, eventType);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
} }
} }
processed = true;
} }
processed = true;
return processed; return processed;
} }


+ 3
- 5
src/BuildingBlocks/EventBus/IntegrationEventLogEF/Utilities/ResilientTransaction.cs View File

@ -15,11 +15,9 @@ public class ResilientTransaction
var strategy = _context.Database.CreateExecutionStrategy(); var strategy = _context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () => await strategy.ExecuteAsync(async () =>
{ {
using (var transaction = _context.Database.BeginTransaction())
{
await action();
transaction.Commit();
}
using var transaction = _context.Database.BeginTransaction();
await action();
transaction.Commit();
}); });
} }
} }

+ 34
- 36
src/BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHostExtensions.cs View File

@ -21,48 +21,46 @@ namespace Microsoft.AspNetCore.Hosting
{ {
var underK8s = webHost.IsInKubernetes(); var underK8s = webHost.IsInKubernetes();
using (var scope = webHost.Services.CreateScope())
using var scope = webHost.Services.CreateScope();
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try
{ {
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
try
if (underK8s)
{ {
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retries = 10;
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(
retryCount: retries,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retry, ctx) =>
{
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
});
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
}
InvokeSeeder(seeder, context, services);
}
else
{
var retries = 10;
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(
retryCount: retries,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retry, ctx) =>
{
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
});
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
} }
catch (Exception ex)
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{ {
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
}
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
} }
} }


+ 1
- 1
src/Services/Basket/Basket.API/Infrastructure/Middlewares/AuthorizeCheckOperationFilter.cs View File

@ -20,7 +20,7 @@ public class AuthorizeCheckOperationFilter : IOperationFilter
operation.Security = new List<OpenApiSecurityRequirement> operation.Security = new List<OpenApiSecurityRequirement>
{ {
new OpenApiSecurityRequirement
new()
{ {
[ oAuthScheme ] = new [] { "basketapi" } [ oAuthScheme ] = new [] { "basketapi" }
} }


+ 17
- 23
src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs View File

@ -6,45 +6,39 @@ public class BasketScenarios
[Fact] [Fact]
public async Task Post_basket_and_response_ok_status_code() public async Task Post_basket_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateClient()
.PostAsync(Post.Basket, content);
using var server = CreateServer();
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateClient()
.PostAsync(Post.Basket, content);
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_basket_and_response_ok_status_code() public async Task Get_basket_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.GetBasket(1));
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.GetBasket(1));
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Send_Checkout_basket_and_response_ok_status_code() public async Task Send_Checkout_basket_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
using var server = CreateServer();
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
await server.CreateClient()
.PostAsync(Post.Basket, contentBasket);
await server.CreateClient()
.PostAsync(Post.Basket, contentBasket);
var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json");
var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateIdempotentClient()
.PostAsync(Post.CheckoutOrder, contentCheckout);
var response = await server.CreateIdempotentClient()
.PostAsync(Post.CheckoutOrder, contentCheckout);
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
string BuildBasket() string BuildBasket()


+ 22
- 26
src/Services/Basket/Basket.FunctionalTests/RedisBasketRepositoryTests.cs View File

@ -9,21 +9,19 @@ namespace Basket.FunctionalTests
[Fact] [Fact]
public async Task UpdateBasket_return_and_add_basket() public async Task UpdateBasket_return_and_add_basket()
{ {
using (var server = CreateServer())
{
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
using var server = CreateServer();
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
var redisBasketRepository = BuildBasketRepository(redis);
var redisBasketRepository = BuildBasketRepository(redis);
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
{
BuyerId = "buyerId",
Items = BuildBasketItems()
});
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
{
BuyerId = "buyerId",
Items = BuildBasketItems()
});
Assert.NotNull(basket);
Assert.Single(basket.Items);
}
Assert.NotNull(basket);
Assert.Single(basket.Items);
} }
@ -32,25 +30,23 @@ namespace Basket.FunctionalTests
public async Task Delete_Basket_return_null() public async Task Delete_Basket_return_null()
{ {
using (var server = CreateServer())
{
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
using var server = CreateServer();
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
var redisBasketRepository = BuildBasketRepository(redis);
var redisBasketRepository = BuildBasketRepository(redis);
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
{
BuyerId = "buyerId",
Items = BuildBasketItems()
});
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
{
BuyerId = "buyerId",
Items = BuildBasketItems()
});
var deleteResult = await redisBasketRepository.DeleteBasketAsync("buyerId");
var deleteResult = await redisBasketRepository.DeleteBasketAsync("buyerId");
var result = await redisBasketRepository.GetBasketAsync(basket.BuyerId);
var result = await redisBasketRepository.GetBasketAsync(basket.BuyerId);
Assert.True(deleteResult);
Assert.Null(result);
}
Assert.True(deleteResult);
Assert.Null(result);
} }
RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux) RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux)


+ 11
- 33
src/Services/Catalog/Catalog.API/Controllers/PicController.cs View File

@ -47,40 +47,18 @@ public class PicController : ControllerBase
private string GetImageMimeTypeFromImageFileExtension(string extension) private string GetImageMimeTypeFromImageFileExtension(string extension)
{ {
string mimetype;
switch (extension)
string mimetype = extension switch
{ {
case ".png":
mimetype = "image/png";
break;
case ".gif":
mimetype = "image/gif";
break;
case ".jpg":
case ".jpeg":
mimetype = "image/jpeg";
break;
case ".bmp":
mimetype = "image/bmp";
break;
case ".tiff":
mimetype = "image/tiff";
break;
case ".wmf":
mimetype = "image/wmf";
break;
case ".jp2":
mimetype = "image/jp2";
break;
case ".svg":
mimetype = "image/svg+xml";
break;
default:
mimetype = "application/octet-stream";
break;
}
".png" => "image/png",
".gif" => "image/gif",
".jpg" or ".jpeg" => "image/jpeg",
".bmp" => "image/bmp",
".tiff" => "image/tiff",
".wmf" => "image/wmf",
".jp2" => "image/jp2",
".svg" => "image/svg+xml",
_ => "application/octet-stream",
};
return mimetype; return mimetype;
} }
} }

+ 0
- 122
src/Services/Catalog/Catalog.API/Extensions/HostExtensions.cs View File

@ -1,122 +0,0 @@
namespace Catalog.API.Extensions;
public static class HostExtensions
{
public static bool IsInKubernetes(this IHost host)
{
var cfg = host.Services.GetService<IConfiguration>();
var orchestratorType = cfg.GetValue<string>("OrchestratorType");
return orchestratorType?.ToUpper() == "K8S";
}
public static IHost MigrateDbContext<TContext>(this IHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext
{
var underK8s = host.IsInKubernetes();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try
{
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(new TimeSpan[]
{
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(8),
});
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
}
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
}
}
}
return host;
}
public static IWebHost MigrateDbContext<TContext>(this IWebHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext
{
var underK8s = host.IsInKubernetes();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try
{
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(new TimeSpan[]
{
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(8),
});
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
}
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
}
}
}
return host;
}
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)
where TContext : DbContext
{
context.Database.Migrate();
seeder(context, services);
}
}

+ 33
- 35
src/Services/Catalog/Catalog.API/Extensions/WebHostExtensions.cs View File

@ -13,48 +13,46 @@ public static class WebHostExtensions
{ {
var underK8s = host.IsInKubernetes(); var underK8s = host.IsInKubernetes();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
using var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
var context = services.GetService<TContext>();
var logger = services.GetRequiredService<ILogger<TContext>>();
try
{
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
var context = services.GetService<TContext>();
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(new TimeSpan[]
{
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(8),
});
try
{
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
}
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(new TimeSpan[]
{
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(8),
});
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
} }
catch (Exception ex)
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{ {
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
}
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
} }
} }


+ 21
- 21
src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs View File

@ -92,11 +92,11 @@ public class CatalogContextSeed
{ {
return new List<CatalogBrand>() return new List<CatalogBrand>()
{ {
new CatalogBrand() { Brand = "Azure"},
new CatalogBrand() { Brand = ".NET" },
new CatalogBrand() { Brand = "Visual Studio" },
new CatalogBrand() { Brand = "SQL Server" },
new CatalogBrand() { Brand = "Other" }
new() { Brand = "Azure"},
new() { Brand = ".NET" },
new() { Brand = "Visual Studio" },
new() { Brand = "SQL Server" },
new() { Brand = "Other" }
}; };
} }
@ -147,10 +147,10 @@ public class CatalogContextSeed
{ {
return new List<CatalogType>() return new List<CatalogType>()
{ {
new CatalogType() { Type = "Mug"},
new CatalogType() { Type = "T-Shirt" },
new CatalogType() { Type = "Sheet" },
new CatalogType() { Type = "USB Memory Stick" }
new() { Type = "Mug"},
new() { Type = "T-Shirt" },
new() { Type = "Sheet" },
new() { Type = "USB Memory Stick" }
}; };
} }
@ -297,18 +297,18 @@ public class CatalogContextSeed
{ {
return new List<CatalogItem>() return new List<CatalogItem>()
{ {
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureFileName = "1.png" },
new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureFileName = "2.png" },
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureFileName = "3.png" },
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureFileName = "4.png" },
new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureFileName = "5.png" },
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureFileName = "6.png" },
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureFileName = "7.png" },
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureFileName = "8.png" },
new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 5, AvailableStock = 100, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureFileName = "9.png" },
new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureFileName = "10.png" },
new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureFileName = "11.png" },
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureFileName = "12.png" },
new() { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureFileName = "1.png" },
new() { CatalogTypeId = 1, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureFileName = "2.png" },
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureFileName = "3.png" },
new() { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureFileName = "4.png" },
new() { CatalogTypeId = 3, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureFileName = "5.png" },
new() { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureFileName = "6.png" },
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureFileName = "7.png" },
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureFileName = "8.png" },
new() { CatalogTypeId = 1, CatalogBrandId = 5, AvailableStock = 100, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureFileName = "9.png" },
new() { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureFileName = "10.png" },
new() { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureFileName = "11.png" },
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureFileName = "12.png" },
}; };
} }


+ 50
- 72
src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarios.cs View File

@ -6,135 +6,113 @@ public class CatalogScenarios
[Fact] [Fact]
public async Task Get_get_all_catalogitems_and_response_ok_status_code() public async Task Get_get_all_catalogitems_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.Items());
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.Items());
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_get_catalogitem_by_id_and_response_ok_status_code() public async Task Get_get_catalogitem_by_id_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.ItemById(1));
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.ItemById(1));
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_get_catalogitem_by_id_and_response_bad_request_status_code() public async Task Get_get_catalogitem_by_id_and_response_bad_request_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.ItemById(int.MinValue));
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.ItemById(int.MinValue));
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
} }
[Fact] [Fact]
public async Task Get_get_catalogitem_by_id_and_response_not_found_status_code() public async Task Get_get_catalogitem_by_id_and_response_not_found_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.ItemById(int.MaxValue));
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.ItemById(int.MaxValue));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
} }
[Fact] [Fact]
public async Task Get_get_catalogitem_by_name_and_response_ok_status_code() public async Task Get_get_catalogitem_by_name_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.ItemByName(".NET"));
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.ItemByName(".NET"));
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_get_paginated_catalogitem_by_name_and_response_ok_status_code() public async Task Get_get_paginated_catalogitem_by_name_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
const bool paginated = true;
var response = await server.CreateClient()
.GetAsync(Get.ItemByName(".NET", paginated));
response.EnsureSuccessStatusCode();
}
using var server = CreateServer();
const bool paginated = true;
var response = await server.CreateClient()
.GetAsync(Get.ItemByName(".NET", paginated));
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_get_paginated_catalog_items_and_response_ok_status_code() public async Task Get_get_paginated_catalog_items_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
const bool paginated = true;
var response = await server.CreateClient()
.GetAsync(Get.Items(paginated));
response.EnsureSuccessStatusCode();
}
using var server = CreateServer();
const bool paginated = true;
var response = await server.CreateClient()
.GetAsync(Get.Items(paginated));
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_get_filtered_catalog_items_and_response_ok_status_code() public async Task Get_get_filtered_catalog_items_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.Filtered(1, 1));
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.Filtered(1, 1));
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_get_paginated_filtered_catalog_items_and_response_ok_status_code() public async Task Get_get_paginated_filtered_catalog_items_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
const bool paginated = true;
var response = await server.CreateClient()
.GetAsync(Get.Filtered(1, 1, paginated));
response.EnsureSuccessStatusCode();
}
using var server = CreateServer();
const bool paginated = true;
var response = await server.CreateClient()
.GetAsync(Get.Filtered(1, 1, paginated));
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_catalog_types_response_ok_status_code() public async Task Get_catalog_types_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.Types);
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.Types);
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Get_catalog_brands_response_ok_status_code() public async Task Get_catalog_brands_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.Brands);
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.Brands);
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
} }

+ 2
- 2
src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs View File

@ -1,7 +1,7 @@
global using Catalog.API.Extensions;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.TestHost; global using Microsoft.AspNetCore.TestHost;
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions;
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure;
global using Microsoft.eShopOnContainers.Services.Catalog.API; global using Microsoft.eShopOnContainers.Services.Catalog.API;
global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.Configuration;


+ 10
- 12
src/Services/Catalog/Catalog.UnitTests/Application/CatalogControllerTest.cs View File

@ -25,11 +25,9 @@ public class CatalogControllerTest
.UseInMemoryDatabase(databaseName: "in-memory") .UseInMemoryDatabase(databaseName: "in-memory")
.Options; .Options;
using (var dbContext = new CatalogContext(_dbOptions))
{
dbContext.AddRange(GetFakeCatalog());
dbContext.SaveChanges();
}
using var dbContext = new CatalogContext(_dbOptions);
dbContext.AddRange(GetFakeCatalog());
dbContext.SaveChanges();
} }
[Fact] [Fact]
@ -66,7 +64,7 @@ public class CatalogControllerTest
{ {
return new List<CatalogItem>() return new List<CatalogItem>()
{ {
new CatalogItem()
new()
{ {
Id = 1, Id = 1,
Name = "fakeItemA", Name = "fakeItemA",
@ -74,7 +72,7 @@ public class CatalogControllerTest
CatalogBrandId = 1, CatalogBrandId = 1,
PictureFileName = "fakeItemA.png" PictureFileName = "fakeItemA.png"
}, },
new CatalogItem()
new()
{ {
Id = 2, Id = 2,
Name = "fakeItemB", Name = "fakeItemB",
@ -82,7 +80,7 @@ public class CatalogControllerTest
CatalogBrandId = 1, CatalogBrandId = 1,
PictureFileName = "fakeItemB.png" PictureFileName = "fakeItemB.png"
}, },
new CatalogItem()
new()
{ {
Id = 3, Id = 3,
Name = "fakeItemC", Name = "fakeItemC",
@ -90,7 +88,7 @@ public class CatalogControllerTest
CatalogBrandId = 1, CatalogBrandId = 1,
PictureFileName = "fakeItemC.png" PictureFileName = "fakeItemC.png"
}, },
new CatalogItem()
new()
{ {
Id = 4, Id = 4,
Name = "fakeItemD", Name = "fakeItemD",
@ -98,7 +96,7 @@ public class CatalogControllerTest
CatalogBrandId = 1, CatalogBrandId = 1,
PictureFileName = "fakeItemD.png" PictureFileName = "fakeItemD.png"
}, },
new CatalogItem()
new()
{ {
Id = 5, Id = 5,
Name = "fakeItemE", Name = "fakeItemE",
@ -106,7 +104,7 @@ public class CatalogControllerTest
CatalogBrandId = 1, CatalogBrandId = 1,
PictureFileName = "fakeItemE.png" PictureFileName = "fakeItemE.png"
}, },
new CatalogItem()
new()
{ {
Id = 6, Id = 6,
Name = "fakeItemF", Name = "fakeItemF",
@ -120,7 +118,7 @@ public class CatalogControllerTest
public class TestCatalogSettings : IOptionsSnapshot<CatalogSettings> public class TestCatalogSettings : IOptionsSnapshot<CatalogSettings>
{ {
public CatalogSettings Value => new CatalogSettings
public CatalogSettings Value => new()
{ {
PicBaseUrl = "http://image-server.com/", PicBaseUrl = "http://image-server.com/",
AzureStorageEnabled = true AzureStorageEnabled = true


+ 7
- 11
src/Services/Identity/Identity.API/Certificate/Certificate.cs View File

@ -12,24 +12,20 @@
* real environment the certificate should be created and stored in a secure way, which is out * real environment the certificate should be created and stored in a secure way, which is out
* of the scope of this project. * of the scope of this project.
**********************************************************************************************/ **********************************************************************************************/
using (var stream = assembly.GetManifestResourceStream("Identity.API.Certificate.idsrv3test.pfx"))
{
return new X509Certificate2(ReadStream(stream), "idsrv3test");
}
using var stream = assembly.GetManifestResourceStream("Identity.API.Certificate.idsrv3test.pfx");
return new X509Certificate2(ReadStream(stream), "idsrv3test");
} }
private static byte[] ReadStream(Stream input) private static byte[] ReadStream(Stream input)
{ {
byte[] buffer = new byte[16 * 1024]; byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
using MemoryStream ms = new MemoryStream();
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{ {
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
ms.Write(buffer, 0, read);
} }
return ms.ToArray();
} }
} }
} }

+ 11
- 13
src/Services/Identity/Identity.API/Data/ApplicationDbContextSeed.cs View File

@ -191,23 +191,21 @@
string imagePath = Path.Combine(webroot, "images"); string imagePath = Path.Combine(webroot, "images");
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray(); string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
using (ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read))
using ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read);
foreach (ZipArchiveEntry entry in zip.Entries)
{ {
foreach (ZipArchiveEntry entry in zip.Entries)
if (imageFiles.Contains(entry.Name))
{ {
if (imageFiles.Contains(entry.Name))
string destinationFilename = Path.Combine(imagePath, entry.Name);
if (File.Exists(destinationFilename))
{ {
string destinationFilename = Path.Combine(imagePath, entry.Name);
if (File.Exists(destinationFilename))
{
File.Delete(destinationFilename);
}
entry.ExtractToFile(destinationFilename);
}
else
{
logger.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
File.Delete(destinationFilename);
} }
entry.ExtractToFile(destinationFilename);
}
else
{
logger.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
} }
} }


+ 35
- 37
src/Services/Identity/Identity.API/IWebHostExtensions.cs View File

@ -13,52 +13,50 @@ namespace Microsoft.AspNetCore.Hosting
{ {
var underK8s = webHost.IsInKubernetes(); var underK8s = webHost.IsInKubernetes();
using (var scope = webHost.Services.CreateScope())
using var scope = webHost.Services.CreateScope();
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try
{ {
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
try
if (underK8s)
{ {
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retries = 10;
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(
retryCount: retries,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retry, ctx) =>
{
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
});
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
}
InvokeSeeder(seeder, context, services);
}
else
{
var retries = 10;
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(
retryCount: retries,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retry, ctx) =>
{
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
});
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
} }
catch (Exception ex)
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{ {
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
}
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
} }
} }
return webHost;
return webHost;
} }
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)


+ 1
- 1
src/Services/Ordering/Ordering.API/Application/Behaviors/TransactionBehaviour.cs View File

@ -35,7 +35,7 @@ public class TransactionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequ
{ {
Guid transactionId; Guid transactionId;
using (>n class="kt">var transaction = await _dbContext.BeginTransactionAsync())
using var transaction = await _dbContext.BeginTransactionAsync();
using (LogContext.PushProperty("TransactionContext", transaction.TransactionId)) using (LogContext.PushProperty("TransactionContext", transaction.TransactionId))
{ {
_logger.LogInformation("----- Begin transaction {TransactionId} for {CommandName} ({@Command})", transaction.TransactionId, typeName, request); _logger.LogInformation("----- Begin transaction {TransactionId} for {CommandName} ({@Command})", transaction.TransactionId, typeName, request);


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

@ -13,12 +13,11 @@ public class OrderQueries
public async Task<Order> GetOrderAsync(int id) public async Task<Order> GetOrderAsync(int id)
{ {
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using var connection = new SqlConnection(_connectionString);
connection.Open();
var result = await connection.QueryAsync<dynamic>(
@"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description,
var result = await connection.QueryAsync<dynamic>(
@"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description,
o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode, o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode,
os.Name as status, os.Name as status,
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl
@ -26,23 +25,21 @@ public class OrderQueries
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid
LEFT JOIN ordering.orderstatus os on o.OrderStatusId = os.Id LEFT JOIN ordering.orderstatus os on o.OrderStatusId = os.Id
WHERE o.Id=@id" WHERE o.Id=@id"
, new { id }
);
, new { id }
);
if (result.AsList().Count == 0)
throw new KeyNotFoundException();
if (result.AsList().Count == 0)
throw new KeyNotFoundException();
return MapOrderItems(result);
}
return MapOrderItems(result);
} }
public async Task<IEnumerable<OrderSummary>> GetOrdersFromUserAsync(Guid userId) public async Task<IEnumerable<OrderSummary>> GetOrdersFromUserAsync(Guid userId)
{ {
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using var connection = new SqlConnection(_connectionString);
connection.Open();
return await connection.QueryAsync<OrderSummary>(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status], SUM(oi.units*oi.unitprice) as total
return await connection.QueryAsync<OrderSummary>(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status], SUM(oi.units*oi.unitprice) as total
FROM [ordering].[Orders] o FROM [ordering].[Orders] o
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
@ -50,17 +47,14 @@ public class OrderQueries
WHERE ob.IdentityGuid = @userId WHERE ob.IdentityGuid = @userId
GROUP BY o.[Id], o.[OrderDate], os.[Name] GROUP BY o.[Id], o.[OrderDate], os.[Name]
ORDER BY o.[Id]", new { userId }); ORDER BY o.[Id]", new { userId });
}
} }
public async Task<IEnumerable<CardType>> GetCardTypesAsync() public async Task<IEnumerable<CardType>> GetCardTypesAsync()
{ {
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using var connection = new SqlConnection(_connectionString);
connection.Open();
return await connection.QueryAsync<CardType>("SELECT * FROM ordering.cardtypes");
}
return await connection.QueryAsync<CardType>("SELECT * FROM ordering.cardtypes");
} }
private Order MapOrderItems(dynamic result) private Order MapOrderItems(dynamic result)


+ 1
- 1
src/Services/Ordering/Ordering.API/Infrastructure/Filters/AuthorizeCheckOperationFilter.cs View File

@ -20,7 +20,7 @@ public class AuthorizeCheckOperationFilter : IOperationFilter
operation.Security = new List<OpenApiSecurityRequirement> operation.Security = new List<OpenApiSecurityRequirement>
{ {
new OpenApiSecurityRequirement
new()
{ {
[ oAuthScheme ] = new [] { "orderingapi" } [ oAuthScheme ] = new [] { "orderingapi" }
} }


+ 13
- 15
src/Services/Ordering/Ordering.BackgroundTasks/Services/GracePeriodManagerService.cs View File

@ -63,24 +63,22 @@ namespace Ordering.BackgroundTasks.Services
{ {
IEnumerable<int> orderIds = new List<int>(); IEnumerable<int> orderIds = new List<int>();
using (var conn = new SqlConnection(_settings.ConnectionString))
using var conn = new SqlConnection(_settings.ConnectionString);
try
{ {
try
{
conn.Open();
orderIds = conn.Query<int>(
@"SELECT Id FROM [ordering].[orders]
WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime
AND [OrderStatusId] = 1",
new { _settings.GracePeriodTime });
}
catch (SqlException exception)
{
_logger.LogCritical(exception, "FATAL ERROR: Database connections could not be opened: {Message}", exception.Message);
}
conn.Open();
orderIds = conn.Query<int>(
@"SELECT Id FROM [ordering].[orders]
WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime
AND [OrderStatusId] = 1",
new { _settings.GracePeriodTime });
}
catch (SqlException exception)
{
_logger.LogCritical(exception, "FATAL ERROR: Database connections could not be opened: {Message}", exception.Message);
} }
return orderIds; return orderIds;
} }
} }


+ 14
- 20
src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs View File

@ -14,39 +14,33 @@ namespace Ordering.FunctionalTests
[Fact] [Fact]
public async Task Get_get_all_stored_orders_and_response_ok_status_code() public async Task Get_get_all_stored_orders_and_response_ok_status_code()
{ {
using (var server = CreateServer())
{
var response = await server.CreateClient()
.GetAsync(Get.Orders);
using var server = CreateServer();
var response = await server.CreateClient()
.GetAsync(Get.Orders);
response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task Cancel_order_no_order_created_bad_request_response() public async Task Cancel_order_no_order_created_bad_request_response()
{ {
using (var server = CreateServer())
{
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateIdempotentClient()
.PutAsync(Put.CancelOrder, content);
using var server = CreateServer();
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateIdempotentClient()
.PutAsync(Put.CancelOrder, content);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
} }
[Fact] [Fact]
public async Task Ship_order_no_order_created_bad_request_response() public async Task Ship_order_no_order_created_bad_request_response()
{ {
using (var server = CreateServer())
{
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateIdempotentClient()
.PutAsync(Put.ShipOrder, content);
using var server = CreateServer();
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
var response = await server.CreateIdempotentClient()
.PutAsync(Put.ShipOrder, content);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
} }
string BuildOrder() string BuildOrder()


+ 1
- 1
src/Services/Webhooks/Webhooks.API/Infrastructure/AuthorizeCheckOperationFilter.cs View File

@ -20,7 +20,7 @@ public class AuthorizeCheckOperationFilter : IOperationFilter
operation.Security = new List<OpenApiSecurityRequirement> operation.Security = new List<OpenApiSecurityRequirement>
{ {
new OpenApiSecurityRequirement
new()
{ {
[ oAuthScheme ] = new [] { "webhooksapi" } [ oAuthScheme ] = new [] { "webhooksapi" }
} }


+ 30
- 32
src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs View File

@ -11,45 +11,43 @@ public class IntegrationEventsScenarios
decimal priceModification = 0.15M; decimal priceModification = 0.15M;
string userId = "JohnId"; string userId = "JohnId";
using (var catalogServer = new CatalogScenariosBase().CreateServer())
using (var basketServer = new BasketScenariosBase().CreateServer())
{
var catalogClient = catalogServer.CreateClient();
var basketClient = basketServer.CreateClient();
using var catalogServer = new CatalogScenariosBase().CreateServer();
using var basketServer = new BasketScenariosBase().CreateServer();
var catalogClient = catalogServer.CreateClient();
var basketClient = basketServer.CreateClient();
// GIVEN a product catalog list
var originalCatalogProducts = await GetCatalogAsync(catalogClient);
// GIVEN a product catalog list
var originalCatalogProducts = await GetCatalogAsync(catalogClient);
// AND a user basket filled with products
var basket = ComposeBasket(userId, originalCatalogProducts.Data.Take(3));
var res = await basketClient.PostAsync(
BasketScenariosBase.Post.CreateBasket,
new StringContent(JsonSerializer.Serialize(basket), UTF8Encoding.UTF8, "application/json")
);
// AND a user basket filled with products
var basket = ComposeBasket(userId, originalCatalogProducts.Data.Take(3));
var res = await basketClient.PostAsync(
BasketScenariosBase.Post.CreateBasket,
new StringContent(JsonSerializer.Serialize(basket), UTF8Encoding.UTF8, "application/json")
);
// WHEN the price of one product is modified in the catalog
var itemToModify = basket.Items[2];
var oldPrice = itemToModify.UnitPrice;
var newPrice = oldPrice + priceModification;
var pRes = await catalogClient.PutAsync(CatalogScenariosBase.Put.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json"));
// WHEN the price of one product is modified in the catalog
var itemToModify = basket.Items[2];
var oldPrice = itemToModify.UnitPrice;
var newPrice = oldPrice + priceModification;
var pRes = await catalogClient.PutAsync(CatalogScenariosBase.Put.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json"));
var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
var itemUpdated = await GetUpdatedBasketItem(newPrice, itemToModify.ProductId, userId, basketClient);
var itemUpdated = await GetUpdatedBasketItem(newPrice, itemToModify.ProductId, userId, basketClient);
if (itemUpdated == null)
{
Assert.False(true, $"The basket service has not been updated.");
}
else
{
//THEN the product price changes in the catalog
Assert.Equal(newPrice, modifiedCatalogProducts.Data.Single(it => it.Id == itemToModify.ProductId).Price);
if (itemUpdated == null)
{
Assert.False(true, $"The basket service has not been updated.");
}
else
{
//THEN the product price changes in the catalog
Assert.Equal(newPrice, modifiedCatalogProducts.Data.Single(it => it.Id == itemToModify.ProductId).Price);
// AND the products in the basket reflects the changed priced and the original price
Assert.Equal(newPrice, itemUpdated.UnitPrice);
Assert.Equal(oldPrice, itemUpdated.OldUnitPrice);
}
// AND the products in the basket reflects the changed priced and the original price
Assert.Equal(newPrice, itemUpdated.UnitPrice);
Assert.Equal(oldPrice, itemUpdated.OldUnitPrice);
} }
} }


+ 20
- 22
src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenarios.cs View File

@ -5,35 +5,33 @@ public class OrderingScenarios : OrderingScenariosBase
[Fact] [Fact]
public async Task Cancel_basket_and_check_order_status_cancelled() public async Task Cancel_basket_and_check_order_status_cancelled()
{ {
using (var orderServer = new OrderingScenariosBase().CreateServer())
using (var basketServer = new BasketScenariosBase().CreateServer())
{
// Expected data
var cityExpected = $"city-{Guid.NewGuid()}";
var orderStatusExpected = "cancelled";
using var orderServer = new OrderingScenariosBase().CreateServer();
using var basketServer = new BasketScenariosBase().CreateServer();
// Expected data
var cityExpected = $"city-{Guid.NewGuid()}";
var orderStatusExpected = "cancelled";
var basketClient = basketServer.CreateIdempotentClient();
var orderClient = orderServer.CreateIdempotentClient();
var basketClient = basketServer.CreateIdempotentClient();
var orderClient = orderServer.CreateIdempotentClient();
// GIVEN a basket is created
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
await basketClient.PostAsync(BasketScenariosBase.Post.CreateBasket, contentBasket);
// GIVEN a basket is created
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
await basketClient.PostAsync(BasketScenariosBase.Post.CreateBasket, contentBasket);
// AND basket checkout is sent
await basketClient.PostAsync(BasketScenariosBase.Post.CheckoutOrder, new StringContent(BuildCheckout(cityExpected), UTF8Encoding.UTF8, "application/json"));
// AND basket checkout is sent
await basketClient.PostAsync(BasketScenariosBase.Post.CheckoutOrder, new StringContent(BuildCheckout(cityExpected), UTF8Encoding.UTF8, "application/json"));
// WHEN Order is created in Ordering.api
var newOrder = await TryGetNewOrderCreated(cityExpected, orderClient);
// WHEN Order is created in Ordering.api
var newOrder = await TryGetNewOrderCreated(cityExpected, orderClient);
// AND Order is cancelled in Ordering.api
await orderClient.PutAsync(OrderingScenariosBase.Put.CancelOrder, new StringContent(BuildCancelOrder(newOrder.OrderNumber), UTF8Encoding.UTF8, "application/json"));
// AND Order is cancelled in Ordering.api
await orderClient.PutAsync(OrderingScenariosBase.Put.CancelOrder, new StringContent(BuildCancelOrder(newOrder.OrderNumber), UTF8Encoding.UTF8, "application/json"));
// AND the requested order is retrieved
var order = await TryGetOrder(newOrder.OrderNumber, orderClient);
// AND the requested order is retrieved
var order = await TryGetOrder(newOrder.OrderNumber, orderClient);
// THEN check status
Assert.Equal(orderStatusExpected, order.Status);
}
// THEN check status
Assert.Equal(orderStatusExpected, order.Status);
} }
async Task<Order> TryGetOrder(string orderNumber, HttpClient orderClient) async Task<Order> TryGetOrder(string orderNumber, HttpClient orderClient)


+ 11
- 13
src/Web/WebMVC/Infrastructure/WebContextSeed.cs View File

@ -56,23 +56,21 @@ public class WebContextSeed
string imagePath = Path.Combine(webroot, "images"); string imagePath = Path.Combine(webroot, "images");
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray(); string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
using (ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read))
using ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read);
foreach (ZipArchiveEntry entry in zip.Entries)
{ {
foreach (ZipArchiveEntry entry in zip.Entries)
if (imageFiles.Contains(entry.Name))
{ {
if (imageFiles.Contains(entry.Name))
string destinationFilename = Path.Combine(imagePath, entry.Name);
if (File.Exists(destinationFilename))
{ {
string destinationFilename = Path.Combine(imagePath, entry.Name);
if (File.Exists(destinationFilename))
{
File.Delete(destinationFilename);
}
entry.ExtractToFile(destinationFilename);
}
else
{
log.Warning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
File.Delete(destinationFilename);
} }
entry.ExtractToFile(destinationFilename);
}
else
{
log.Warning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
} }
} }


+ 11
- 13
src/Web/WebSPA/Server/Infrastructure/WebContextSeed.cs View File

@ -39,23 +39,21 @@ public class WebContextSeed
} }
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray(); string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
using (ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read))
using ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read);
foreach (ZipArchiveEntry entry in zip.Entries)
{ {
foreach (ZipArchiveEntry entry in zip.Entries)
if (!imageFiles.Contains(entry.Name))
{ {
if (!imageFiles.Contains(entry.Name))
string destinationFilename = Path.Combine(imagePath, entry.Name);
if (File.Exists(destinationFilename))
{ {
string destinationFilename = Path.Combine(imagePath, entry.Name);
if (File.Exists(destinationFilename))
{
File.Delete(destinationFilename);
}
entry.ExtractToFile(destinationFilename);
}
else
{
log.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
File.Delete(destinationFilename);
} }
entry.ExtractToFile(destinationFilename);
}
else
{
log.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
} }
} }


Loading…
Cancel
Save