diff --git a/src/HelloShop.AppHost/DaprComponents/redis-lock.yaml b/src/HelloShop.AppHost/DaprComponents/redis-lock.yaml new file mode 100644 index 0000000..fe28ae5 --- /dev/null +++ b/src/HelloShop.AppHost/DaprComponents/redis-lock.yaml @@ -0,0 +1,15 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: lockstore +spec: + type: lock.redis + version: v1 + metadata: + - name: redisHost + secretKeyRef: + name: ConnectionStrings__cache + - name: redisPassword + value: "" +auth: + secretStore: env-secretstore \ No newline at end of file diff --git a/src/HelloShop.AppHost/Program.cs b/src/HelloShop.AppHost/Program.cs index 2f8b185..7bbb1cc 100644 --- a/src/HelloShop.AppHost/Program.cs +++ b/src/HelloShop.AppHost/Program.cs @@ -6,7 +6,7 @@ using Aspire.Hosting.Dapr; var builder = DistributedApplication.CreateBuilder(args); -var cache = builder.AddRedis("cache", port: 6379); +var cache = builder.AddRedis("cache", port: 6380).WithPersistence(); var rabbitmq = builder.AddRabbitMQ("rabbitmq").WithManagementPlugin(); @@ -19,14 +19,14 @@ var orderingService = builder.AddProject("or .WithReference(identityService) .WithDaprSidecar(options => { - options.WithOptions(daprSidecarOptions).WithReference(rabbitmq); + options.WithOptions(daprSidecarOptions).WithReference(rabbitmq).WithReference(cache); }); var productService = builder.AddProject("productservice") .WithReference(identityService) .WithDaprSidecar(options => { - options.WithOptions(daprSidecarOptions).WithReference(rabbitmq); + options.WithOptions(daprSidecarOptions).WithReference(rabbitmq).WithReference(cache); }); ; var basketService = builder.AddProject("basketservice") @@ -34,7 +34,7 @@ var basketService = builder.AddProject("basket .WithReference(cache) .WithDaprSidecar(options => { - options.WithOptions(daprSidecarOptions).WithReference(rabbitmq); + options.WithOptions(daprSidecarOptions).WithReference(rabbitmq).WithReference(cache); }); var apiservice = builder.AddProject("apiservice") diff --git a/src/HelloShop.OrderingService/HelloShop.OrderingService.http b/src/HelloShop.OrderingService/HelloShop.OrderingService.http index b767d74..628abfe 100644 --- a/src/HelloShop.OrderingService/HelloShop.OrderingService.http +++ b/src/HelloShop.OrderingService/HelloShop.OrderingService.http @@ -2,7 +2,7 @@ POST {{HelloShop.OrderingService_HostAddress}}/api/orders Content-Type: application/json -x-request-id: 5ddc1737-6a31-4b4f-bccd-63d0966348a1 +x-request-id: 8ddc1737-6a31-4b4f-bccd-63d0966348a1 Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwidW5pcXVlX25hbWUiOiJhZG1pbiIsInJvbGVpZCI6IjEiLCJuYmYiOjE3MjA1NzY3NDYsImV4cCI6MTc0MjYwODc0NiwiaWF0IjoxNzIwNTc2NzQ2fQ.ju_D3zeGLKqJYVckbb8Y3yNkp40nOqRAJrdOsISs4d4 { diff --git a/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderAwaitingValidationDistributedEventHandler.cs b/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderAwaitingValidationDistributedEventHandler.cs index 67fa2ca..5aa86ed 100644 --- a/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderAwaitingValidationDistributedEventHandler.cs +++ b/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderAwaitingValidationDistributedEventHandler.cs @@ -5,13 +5,16 @@ using HelloShop.ProductService.DistributedEvents.Events; using HelloShop.ProductService.Entities.Products; using HelloShop.ProductService.Infrastructure; using HelloShop.ServiceDefaults.DistributedEvents.Abstractions; +using HelloShop.ServiceDefaults.DistributedLocks; namespace HelloShop.ProductService.DistributedEvents.EventHandling { - public class OrderAwaitingValidationDistributedEventHandler(ProductServiceDbContext dbContext, IDistributedEventBus distributedEventBus, ILogger logger) : IDistributedEventHandler + public class OrderAwaitingValidationDistributedEventHandler(ProductServiceDbContext dbContext, IDistributedEventBus distributedEventBus, IDistributedLock distributedLock, ILogger logger) : IDistributedEventHandler { public async Task HandleAsync(OrderAwaitingValidationDistributedEvent @event) { + await using var lockResult = await distributedLock.LockAsync("stock"); + logger.LogInformation("Handling distributed event {EventId} {Event}", @event.Id, @event); var confirmedOrderStockItems = new Dictionary(); diff --git a/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderPaidDistributedEventHandler.cs b/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderPaidDistributedEventHandler.cs index a21c94d..9e61f28 100644 --- a/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderPaidDistributedEventHandler.cs +++ b/src/HelloShop.ProductService/DistributedEvents/EventHandling/OrderPaidDistributedEventHandler.cs @@ -5,13 +5,16 @@ using HelloShop.ProductService.DistributedEvents.Events; using HelloShop.ProductService.Entities.Products; using HelloShop.ProductService.Infrastructure; using HelloShop.ServiceDefaults.DistributedEvents.Abstractions; +using HelloShop.ServiceDefaults.DistributedLocks; namespace HelloShop.ProductService.DistributedEvents.EventHandling { - public class OrderPaidDistributedEventHandler(ProductServiceDbContext dbContext) : IDistributedEventHandler + public class OrderPaidDistributedEventHandler(ProductServiceDbContext dbContext, IDistributedLock distributedLock) : IDistributedEventHandler { public async Task HandleAsync(OrderPaidDistributedEvent @event) { + await using var lockResult = await distributedLock.LockAsync("stock"); + foreach (var orderStockItem in @event.OrderStockItems) { var product = await dbContext.Set().FindAsync(orderStockItem.ProductId) ?? throw new Exception($"Product with id {orderStockItem.ProductId} not found"); diff --git a/src/HelloShop.ProductService/Program.cs b/src/HelloShop.ProductService/Program.cs index 6365db6..3aeeaf7 100644 --- a/src/HelloShop.ProductService/Program.cs +++ b/src/HelloShop.ProductService/Program.cs @@ -5,6 +5,7 @@ using HelloShop.ProductService.Constants; using HelloShop.ProductService.Infrastructure; using HelloShop.ServiceDefaults.DistributedEvents.Abstractions; using HelloShop.ServiceDefaults.DistributedEvents.DaprBuildingBlocks; +using HelloShop.ServiceDefaults.DistributedLocks; using HelloShop.ServiceDefaults.Extensions; using Microsoft.EntityFrameworkCore; @@ -29,6 +30,7 @@ builder.Services.AddModelMapper().AddModelValidator(); builder.Services.AddLocalization().AddPermissionDefinitions(); builder.Services.AddAuthorization().AddRemotePermissionChecker().AddCustomAuthorization(); builder.AddDaprDistributedEventBus().AddSubscriptionFromAssembly(); +builder.Services.AddSingleton(); // End addd extensions services to the container. var app = builder.Build(); diff --git a/src/HelloShop.ProductService/WeatherForecast.cs b/src/HelloShop.ProductService/WeatherForecast.cs deleted file mode 100644 index b2a2d04..0000000 --- a/src/HelloShop.ProductService/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) HelloShop Corporation. All rights reserved. -// See the license file in the project root for more information. - -namespace HelloShop.ProductService; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} diff --git a/src/HelloShop.ServiceDefaults/DistributedLocks/DaprDistributedLock.cs b/src/HelloShop.ServiceDefaults/DistributedLocks/DaprDistributedLock.cs new file mode 100644 index 0000000..c47f94a --- /dev/null +++ b/src/HelloShop.ServiceDefaults/DistributedLocks/DaprDistributedLock.cs @@ -0,0 +1,29 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using Dapr.Client; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HelloShop.ServiceDefaults.DistributedLocks +{ + public class DaprDistributedLock(DaprClient daprClient) : IDistributedLock + { + public async Task LockAsync(string resourceId, int expiryInSeconds = default, CancellationToken cancellationToken = default) + { + expiryInSeconds = expiryInSeconds == default ? 60 : expiryInSeconds; + + string? lockOwner = new StackTrace().GetFrame(1)?.GetMethod()?.DeclaringType?.Name; + +#pragma warning disable CS0618 + TryLockResponse response = await daprClient.Lock("lockstore", resourceId, lockOwner, expiryInSeconds, cancellationToken); +#pragma warning restore CS0618 + + return new DaprDistributedLockResult(response); + } + } +} diff --git a/src/HelloShop.ServiceDefaults/DistributedLocks/DaprDistributedLockResult.cs b/src/HelloShop.ServiceDefaults/DistributedLocks/DaprDistributedLockResult.cs new file mode 100644 index 0000000..2921a71 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/DistributedLocks/DaprDistributedLockResult.cs @@ -0,0 +1,23 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using Dapr.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HelloShop.ServiceDefaults.DistributedLocks +{ +#pragma warning disable CS0618 + public class DaprDistributedLockResult(TryLockResponse tryLockResponse) : IDistributedLockResult +#pragma warning restore CS0618 + { + public async ValueTask DisposeAsync() + { + await tryLockResponse.DisposeAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/HelloShop.ServiceDefaults/DistributedLocks/IDistributedLock.cs b/src/HelloShop.ServiceDefaults/DistributedLocks/IDistributedLock.cs new file mode 100644 index 0000000..f441ce0 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/DistributedLocks/IDistributedLock.cs @@ -0,0 +1,16 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HelloShop.ServiceDefaults.DistributedLocks +{ + public interface IDistributedLock + { + public abstract Task LockAsync(string resourceId, int expiryInSeconds = default, CancellationToken cancellationToken = default); + } +} diff --git a/src/HelloShop.ServiceDefaults/DistributedLocks/IDistributedLockResult.cs b/src/HelloShop.ServiceDefaults/DistributedLocks/IDistributedLockResult.cs new file mode 100644 index 0000000..1d6e9b3 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/DistributedLocks/IDistributedLockResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HelloShop.ServiceDefaults.DistributedLocks +{ + public interface IDistributedLockResult : IAsyncDisposable { } +}