Merge pull request #1884 from szukuro/dev

Adding various language enhancements, removing unused/duplicated extension
This commit is contained in:
Tarun Jain 2022-04-29 19:26:06 +05:30 committed by GitHub
commit 59805331cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 447 additions and 671 deletions

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" }
} }

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" }
} }

View File

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

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); if (subscription.IsDynamic)
foreach (var subscription in subscriptions)
{ {
if (subscription.IsDynamic) if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue;
{
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler; using dynamic eventData = JsonDocument.Parse(message);
if (handler == null) continue; await handler.Handle(eventData);
}
using dynamic eventData = JsonDocument.Parse(message); else
await handler.Handle(eventData); {
} var handler = scope.ResolveOptional(subscription.HandlerType);
else if (handler == null) continue;
{ var eventType = _subsManager.GetEventTypeByName(eventName);
var handler = scope.ResolveOptional(subscription.HandlerType); var integrationEvent = JsonSerializer.Deserialize(message, eventType);
if (handler == null) continue; var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
var eventType = _subsManager.GetEventTypeByName(eventName); await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
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;
} }

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()) using var transaction = _context.Database.BeginTransaction();
{ await action();
await action(); transaction.Commit();
transaction.Commit();
}
}); });
} }
} }

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; logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try if (underK8s)
{ {
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); InvokeSeeder(seeder, context, services);
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));
}
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
} }
catch (Exception ex) else
{ {
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); var retries = 10;
if (underK8s) var retry = Policy.Handle<SqlException>()
{ .WaitAndRetry(
throw; // Rethrow under k8s because we rely on k8s to re-run the pod 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));
}
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
} }
} }

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" }
} }

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()) using var server = CreateServer();
{ var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json"); var response = await server.CreateClient()
var response = await server.CreateClient() .PostAsync(Post.Basket, content);
.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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.GetBasket(1));
.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()) using var server = CreateServer();
{ var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
await server.CreateClient() await server.CreateClient()
.PostAsync(Post.Basket, contentBasket); .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() var response = await server.CreateIdempotentClient()
.PostAsync(Post.CheckoutOrder, contentCheckout); .PostAsync(Post.CheckoutOrder, contentCheckout);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
}
} }
string BuildBasket() string BuildBasket()

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()) using var server = CreateServer();
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
var redisBasketRepository = BuildBasketRepository(redis);
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
{ {
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>(); BuyerId = "buyerId",
Items = BuildBasketItems()
});
var redisBasketRepository = BuildBasketRepository(redis); Assert.NotNull(basket);
Assert.Single(basket.Items);
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
{
BuyerId = "buyerId",
Items = BuildBasketItems()
});
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()) using var server = CreateServer();
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
var redisBasketRepository = BuildBasketRepository(redis);
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
{ {
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>(); BuyerId = "buyerId",
Items = BuildBasketItems()
});
var redisBasketRepository = BuildBasketRepository(redis); var deleteResult = await redisBasketRepository.DeleteBasketAsync("buyerId");
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId") var result = await redisBasketRepository.GetBasketAsync(basket.BuyerId);
{
BuyerId = "buyerId",
Items = BuildBasketItems()
});
var deleteResult = await redisBasketRepository.DeleteBasketAsync("buyerId"); Assert.True(deleteResult);
Assert.Null(result);
var result = await redisBasketRepository.GetBasketAsync(basket.BuyerId);
Assert.True(deleteResult);
Assert.Null(result);
}
} }
RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux) RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux)

View File

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

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);
}
}

View File

@ -13,48 +13,46 @@ public static class WebHostExtensions
{ {
var underK8s = host.IsInKubernetes(); var underK8s = host.IsInKubernetes();
using (var scope = host.Services.CreateScope()) using var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try
{ {
var services = scope.ServiceProvider; logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
var logger = services.GetRequiredService<ILogger<TContext>>(); if (underK8s)
var context = services.GetService<TContext>();
try
{ {
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); 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),
});
//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) else
{ {
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); var retry = Policy.Handle<SqlException>()
if (underK8s) .WaitAndRetry(new TimeSpan[]
{ {
throw; // Rethrow under k8s because we rely on k8s to re-run the pod 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
} }
} }

View File

@ -92,11 +92,11 @@ public class CatalogContextSeed
{ {
return new List<CatalogBrand>() return new List<CatalogBrand>()
{ {
new CatalogBrand() { Brand = "Azure"}, new() { Brand = "Azure"},
new CatalogBrand() { Brand = ".NET" }, new() { Brand = ".NET" },
new CatalogBrand() { Brand = "Visual Studio" }, new() { Brand = "Visual Studio" },
new CatalogBrand() { Brand = "SQL Server" }, new() { Brand = "SQL Server" },
new CatalogBrand() { Brand = "Other" } new() { Brand = "Other" }
}; };
} }
@ -147,10 +147,10 @@ public class CatalogContextSeed
{ {
return new List<CatalogType>() return new List<CatalogType>()
{ {
new CatalogType() { Type = "Mug"}, new() { Type = "Mug"},
new CatalogType() { Type = "T-Shirt" }, new() { Type = "T-Shirt" },
new CatalogType() { Type = "Sheet" }, new() { Type = "Sheet" },
new CatalogType() { Type = "USB Memory Stick" } 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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 = 5, AvailableStock = 100, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureFileName = "12.png" },
}; };
} }

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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.Items());
.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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.ItemById(1));
.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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.ItemById(int.MinValue));
.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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.ItemById(int.MaxValue));
.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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.ItemByName(".NET"));
.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()) using var server = CreateServer();
{ const bool paginated = true;
const bool paginated = true; var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.ItemByName(".NET", paginated));
.GetAsync(Get.ItemByName(".NET", paginated));
response.EnsureSuccessStatusCode(); 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()) using var server = CreateServer();
{ const bool paginated = true;
const bool paginated = true; var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.Items(paginated));
.GetAsync(Get.Items(paginated));
response.EnsureSuccessStatusCode(); 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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.Filtered(1, 1));
.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()) using var server = CreateServer();
{ const bool paginated = true;
const bool paginated = true; var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.Filtered(1, 1, paginated));
.GetAsync(Get.Filtered(1, 1, paginated));
response.EnsureSuccessStatusCode(); 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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.Types);
.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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.Brands);
.GetAsync(Get.Brands);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
}
} }
} }

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;

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)) using var dbContext = new CatalogContext(_dbOptions);
{ dbContext.AddRange(GetFakeCatalog());
dbContext.AddRange(GetFakeCatalog()); dbContext.SaveChanges();
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

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")) using var stream = assembly.GetManifestResourceStream("Identity.API.Certificate.idsrv3test.pfx");
{ return new X509Certificate2(ReadStream(stream), "idsrv3test");
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; ms.Write(buffer, 0, read);
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
} }
return ms.ToArray();
} }
} }
} }

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); File.Delete(destinationFilename);
if (File.Exists(destinationFilename))
{
File.Delete(destinationFilename);
}
entry.ExtractToFile(destinationFilename);
}
else
{
logger.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
entry.ExtractToFile(destinationFilename);
}
else
{
logger.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
} }
} }

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; logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try if (underK8s)
{ {
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); InvokeSeeder(seeder, context, services);
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));
}
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
} }
catch (Exception ex) else
{ {
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); var retries = 10;
if (underK8s) var retry = Policy.Handle<SqlException>()
{ .WaitAndRetry(
throw; // Rethrow under k8s because we rely on k8s to re-run the pod 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));
}
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 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)

View File

@ -35,7 +35,7 @@ public class TransactionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequ
{ {
Guid transactionId; Guid transactionId;
using (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);

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)) using var connection = new SqlConnection(_connectionString);
{ connection.Open();
connection.Open();
var result = await connection.QueryAsync<dynamic>( var result = await connection.QueryAsync<dynamic>(
@"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description, @"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) if (result.AsList().Count == 0)
throw new KeyNotFoundException(); 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)) using var connection = new SqlConnection(_connectionString);
{ connection.Open();
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)) using var connection = new SqlConnection(_connectionString);
{ connection.Open();
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)

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" }
} }

View File

@ -63,23 +63,21 @@ 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>(
conn.Open(); @"SELECT Id FROM [ordering].[orders]
orderIds = conn.Query<int>( WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime
@"SELECT Id FROM [ordering].[orders] AND [OrderStatusId] = 1",
WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime new { _settings.GracePeriodTime });
AND [OrderStatusId] = 1",
new { _settings.GracePeriodTime });
}
catch (SqlException exception)
{
_logger.LogCritical(exception, "FATAL ERROR: Database connections could not be opened: {Message}", exception.Message);
}
} }
catch (SqlException exception)
{
_logger.LogCritical(exception, "FATAL ERROR: Database connections could not be opened: {Message}", exception.Message);
}
return orderIds; return orderIds;
} }

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()) using var server = CreateServer();
{ var response = await server.CreateClient()
var response = await server.CreateClient() .GetAsync(Get.Orders);
.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()) using var server = CreateServer();
{ var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); var response = await server.CreateIdempotentClient()
var response = await server.CreateIdempotentClient() .PutAsync(Put.CancelOrder, content);
.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()) using var server = CreateServer();
{ var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json"); var response = await server.CreateIdempotentClient()
var response = await server.CreateIdempotentClient() .PutAsync(Put.ShipOrder, content);
.PutAsync(Put.ShipOrder, content);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
} }
string BuildOrder() string BuildOrder()

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" }
} }

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 catalogServer = new CatalogScenariosBase().CreateServer();
using (var basketServer = new BasketScenariosBase().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);
// 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"));
var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
var itemUpdated = await GetUpdatedBasketItem(newPrice, itemToModify.ProductId, userId, basketClient);
if (itemUpdated == null)
{ {
var catalogClient = catalogServer.CreateClient(); Assert.False(true, $"The basket service has not been updated.");
var basketClient = basketServer.CreateClient(); }
else
{
//THEN the product price changes in the catalog
Assert.Equal(newPrice, modifiedCatalogProducts.Data.Single(it => it.Id == itemToModify.ProductId).Price);
// GIVEN a product catalog list // AND the products in the basket reflects the changed priced and the original price
var originalCatalogProducts = await GetCatalogAsync(catalogClient); Assert.Equal(newPrice, itemUpdated.UnitPrice);
Assert.Equal(oldPrice, itemUpdated.OldUnitPrice);
// 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"));
var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
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);
// AND the products in the basket reflects the changed priced and the original price
Assert.Equal(newPrice, itemUpdated.UnitPrice);
Assert.Equal(oldPrice, itemUpdated.OldUnitPrice);
}
} }
} }

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

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); File.Delete(destinationFilename);
if (File.Exists(destinationFilename))
{
File.Delete(destinationFilename);
}
entry.ExtractToFile(destinationFilename);
}
else
{
log.Warning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
entry.ExtractToFile(destinationFilename);
}
else
{
log.Warning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
} }
} }

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); File.Delete(destinationFilename);
if (File.Exists(destinationFilename))
{
File.Delete(destinationFilename);
}
entry.ExtractToFile(destinationFilename);
}
else
{
log.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
entry.ExtractToFile(destinationFilename);
}
else
{
log.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
} }
} }
} }