diff --git a/src/HelloShop.ApiService/HelloShop.ApiService.csproj b/src/HelloShop.ApiService/HelloShop.ApiService.csproj
index 66e34c5..b3ff7dc 100644
--- a/src/HelloShop.ApiService/HelloShop.ApiService.csproj
+++ b/src/HelloShop.ApiService/HelloShop.ApiService.csproj
@@ -9,6 +9,6 @@
-
+
\ No newline at end of file
diff --git a/src/HelloShop.AppHost/HelloShop.AppHost.csproj b/src/HelloShop.AppHost/HelloShop.AppHost.csproj
index 0d68099..8ce83a0 100644
--- a/src/HelloShop.AppHost/HelloShop.AppHost.csproj
+++ b/src/HelloShop.AppHost/HelloShop.AppHost.csproj
@@ -15,7 +15,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/HelloShop.BasketService/HelloShop.BasketService.csproj b/src/HelloShop.BasketService/HelloShop.BasketService.csproj
index 6aea2ac..125ea08 100644
--- a/src/HelloShop.BasketService/HelloShop.BasketService.csproj
+++ b/src/HelloShop.BasketService/HelloShop.BasketService.csproj
@@ -13,12 +13,12 @@
-
+
-
-
-
-
+
+
+
+
diff --git a/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj b/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj
index 63578c4..4368cfd 100644
--- a/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj
+++ b/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj
@@ -58,10 +58,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj b/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj
index b610f84..997f47f 100644
--- a/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj
+++ b/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj
@@ -8,12 +8,12 @@
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
\ No newline at end of file
diff --git a/src/HelloShop.OrderingService/Behaviors/TransactionBehavior.cs b/src/HelloShop.OrderingService/Behaviors/TransactionBehavior.cs
index fda8b7e..568e4ac 100644
--- a/src/HelloShop.OrderingService/Behaviors/TransactionBehavior.cs
+++ b/src/HelloShop.OrderingService/Behaviors/TransactionBehavior.cs
@@ -3,13 +3,14 @@
using HelloShop.OrderingService.Extensions;
using HelloShop.OrderingService.Infrastructure;
+using HelloShop.OrderingService.Services;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Data;
namespace HelloShop.OrderingService.Behaviors
{
- public class TransactionBehavior(OrderingServiceDbContext dbContext, ILoggerFactory loggerFactory) : IPipelineBehavior where TRequest : IRequest
+ public class TransactionBehavior(OrderingServiceDbContext dbContext, ILoggerFactory loggerFactory, IDistributedEventService eventService) : IPipelineBehavior where TRequest : IRequest
{
private readonly ILogger> _logger = loggerFactory.CreateLogger>();
@@ -42,6 +43,8 @@ namespace HelloShop.OrderingService.Behaviors
}
await transaction.CommitAsync();
+
+ await eventService.PublishEventsThroughEventBusAsync(transaction.TransactionId, cancellationToken);
});
return response ?? throw new ApplicationException($"Command {typeName} returned null response");
diff --git a/src/HelloShop.OrderingService/Commands/Orders/CreateOrderCommandHandler.cs b/src/HelloShop.OrderingService/Commands/Orders/CreateOrderCommandHandler.cs
index c10c65c..38e5b89 100644
--- a/src/HelloShop.OrderingService/Commands/Orders/CreateOrderCommandHandler.cs
+++ b/src/HelloShop.OrderingService/Commands/Orders/CreateOrderCommandHandler.cs
@@ -14,10 +14,14 @@ using Microsoft.EntityFrameworkCore;
namespace HelloShop.OrderingService.Commands.Orders
{
- public class CreateOrderCommandHandler(IMediator mediator, OrderingServiceDbContext dbContext, IMapper mapper, IDistributedEventBus distributedEventBus) : IRequestHandler
+ public class CreateOrderCommandHandler(IMediator mediator, IDistributedEventService distributedEventService, OrderingServiceDbContext dbContext, IMapper mapper) : IRequestHandler
{
public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
+ var orderStartedIntegrationEvent = new OrderStartedDistributedEvent(request.UserId);
+
+ await distributedEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent, cancellationToken);
+
Address address = mapper.Map(request);
IEnumerable orderItems = mapper.Map>(request.OrderItems);
@@ -47,8 +51,6 @@ namespace HelloShop.OrderingService.Commands.Orders
await mediator.Publish(new OrderStartedLocalEvent(order), cancellationToken);
- await distributedEventBus.PublishAsync(new OrderStartedDistributedEvent(request.UserId), cancellationToken);
-
return await Task.FromResult(true);
}
}
diff --git a/src/HelloShop.OrderingService/DistributedEvents/EventHandling/OrderPaymentSucceededDistributedEventHandler.cs b/src/HelloShop.OrderingService/DistributedEvents/EventHandling/OrderPaymentSucceededDistributedEventHandler.cs
index 34c6cc0..6e27c61 100644
--- a/src/HelloShop.OrderingService/DistributedEvents/EventHandling/OrderPaymentSucceededDistributedEventHandler.cs
+++ b/src/HelloShop.OrderingService/DistributedEvents/EventHandling/OrderPaymentSucceededDistributedEventHandler.cs
@@ -4,12 +4,13 @@
using HelloShop.OrderingService.DistributedEvents.Events;
using HelloShop.OrderingService.Entities.Orders;
using HelloShop.OrderingService.Infrastructure;
+using HelloShop.OrderingService.Services;
using HelloShop.ServiceDefaults.DistributedEvents.Abstractions;
using Microsoft.EntityFrameworkCore;
namespace HelloShop.OrderingService.DistributedEvents.EventHandling
{
- public class OrderPaymentSucceededDistributedEventHandler(OrderingServiceDbContext dbContext, IDistributedEventBus distributedEventBus) : IDistributedEventHandler
+ public class OrderPaymentSucceededDistributedEventHandler(OrderingServiceDbContext dbContext, IDistributedEventService distributedEventService) : IDistributedEventHandler
{
public async Task HandleAsync(OrderPaymentSucceededDistributedEvent @event)
{
@@ -29,13 +30,15 @@ namespace HelloShop.OrderingService.DistributedEvents.EventHandling
await dbContext.SaveChangesAsync();
- await transaction.CommitAsync();
-
var orderStockList = order.OrderItems.Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.Units));
var integrationEvent = new OrderPaidDistributedEvent(order.Id, orderStockList);
- await distributedEventBus.PublishAsync(integrationEvent);
+ await distributedEventService.AddAndSaveEventAsync(integrationEvent);
+
+ await distributedEventService.PublishEventsThroughEventBusAsync(transaction.TransactionId);
+
+ await transaction.CommitAsync();
});
}
}
diff --git a/src/HelloShop.OrderingService/Entities/EventLogs/DistributedEventLog.cs b/src/HelloShop.OrderingService/Entities/EventLogs/DistributedEventLog.cs
new file mode 100644
index 0000000..0c66301
--- /dev/null
+++ b/src/HelloShop.OrderingService/Entities/EventLogs/DistributedEventLog.cs
@@ -0,0 +1,42 @@
+// Copyright (c) HelloShop Corporation. All rights reserved.
+// See the license file in the project root for more information.
+
+using HelloShop.ServiceDefaults.DistributedEvents.Abstractions;
+using System.Diagnostics.CodeAnalysis;
+
+namespace HelloShop.OrderingService.Entities.EventLogs
+{
+ public enum DistributedEventStatus { NotPublished, InProgress, Published, PublishedFailed }
+
+ public class DistributedEventLog
+ {
+ public Guid EventId { get; set; }
+
+ public required string EventTypeName { get; set; }
+
+ public required DistributedEvent DistributedEvent { get; set; }
+
+ public DistributedEventStatus Status { get; set; } = DistributedEventStatus.NotPublished;
+
+ public int TimesSent { get; set; }
+
+ public DateTimeOffset CreationTime { get; set; } = DateTimeOffset.UtcNow;
+
+ public required Guid TransactionId { get; set; }
+
+ ///
+ /// EF Core cannot set navigation properties using a constructor.
+ /// The constructor can be public, private, or have any other accessibility.
+ ///
+ private DistributedEventLog() { }
+
+ [SetsRequiredMembers]
+ public DistributedEventLog(DistributedEvent @event, Guid transactionId)
+ {
+ EventId = @event.Id;
+ EventTypeName = @event.GetType().Name;
+ DistributedEvent = @event;
+ TransactionId = transactionId;
+ }
+ }
+}
diff --git a/src/HelloShop.OrderingService/Extensions/Extensions.cs b/src/HelloShop.OrderingService/Extensions/Extensions.cs
index 2acae5e..53253d0 100644
--- a/src/HelloShop.OrderingService/Extensions/Extensions.cs
+++ b/src/HelloShop.OrderingService/Extensions/Extensions.cs
@@ -48,6 +48,8 @@ namespace HelloShop.OrderingService.Extensions
builder.Services.AddHostedService();
builder.Services.AddHostedService();
+
+ builder.Services.AddTransient>();
}
public static WebApplication MapApplicationEndpoints(this WebApplication app)
diff --git a/src/HelloShop.OrderingService/HelloShop.OrderingService.csproj b/src/HelloShop.OrderingService/HelloShop.OrderingService.csproj
index b92c965..210ccca 100644
--- a/src/HelloShop.OrderingService/HelloShop.OrderingService.csproj
+++ b/src/HelloShop.OrderingService/HelloShop.OrderingService.csproj
@@ -8,12 +8,12 @@
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
\ No newline at end of file
diff --git a/src/HelloShop.OrderingService/Infrastructure/EntityConfigurations/EventLogs/DistributedEventLogEntityTypeConfiguration.cs b/src/HelloShop.OrderingService/Infrastructure/EntityConfigurations/EventLogs/DistributedEventLogEntityTypeConfiguration.cs
new file mode 100644
index 0000000..4e17d4d
--- /dev/null
+++ b/src/HelloShop.OrderingService/Infrastructure/EntityConfigurations/EventLogs/DistributedEventLogEntityTypeConfiguration.cs
@@ -0,0 +1,27 @@
+// Copyright (c) HelloShop Corporation. All rights reserved.
+// See the license file in the project root for more information.
+
+using HelloShop.OrderingService.Entities.EventLogs;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore;
+using System.Text.Json;
+using HelloShop.ServiceDefaults.DistributedEvents.Abstractions;
+
+namespace HelloShop.OrderingService.Infrastructure.EntityConfigurations.EventLogs
+{
+ public class DistributedEventLogEntityTypeConfiguration : IEntityTypeConfiguration
+ {
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions = new(JsonSerializerDefaults.Web);
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("DistributedEventLogs");
+
+ builder.HasKey(x => x.EventId);
+ builder.Property(x => x.EventTypeName).HasMaxLength(32);
+ builder.Property(x => x.Status).HasConversion();
+
+ builder.Property(x => x.DistributedEvent).HasConversion(v => JsonSerializer.Serialize(v, v.GetType(), s_jsonSerializerOptions), v => JsonSerializer.Deserialize(v, s_jsonSerializerOptions)!);
+ }
+ }
+}
diff --git a/src/HelloShop.OrderingService/Services/DistributedEventService.cs b/src/HelloShop.OrderingService/Services/DistributedEventService.cs
new file mode 100644
index 0000000..8d02035
--- /dev/null
+++ b/src/HelloShop.OrderingService/Services/DistributedEventService.cs
@@ -0,0 +1,89 @@
+// Copyright (c) HelloShop Corporation. All rights reserved.
+// See the license file in the project root for more information.
+
+using HelloShop.OrderingService.Entities.EventLogs;
+using HelloShop.ServiceDefaults.DistributedEvents.Abstractions;
+using Microsoft.EntityFrameworkCore;
+
+namespace HelloShop.OrderingService.Services
+{
+ public class DistributedEventService(TContext dbContext, IDistributedEventBus distributedEventBus, ILogger> logger) : IDistributedEventService, IDisposable where TContext : DbContext
+ {
+ private volatile bool _disposedValue;
+
+ public async Task UpdateEventStatusAsync(Guid eventId, DistributedEventStatus status, CancellationToken cancellationToken = default)
+ {
+ var eventLogEntry = dbContext.Set().Single(ie => ie.EventId == eventId);
+
+ eventLogEntry.Status = status;
+
+ if (status == DistributedEventStatus.InProgress)
+ {
+ eventLogEntry.TimesSent++;
+ }
+
+ await dbContext.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task> RetrieveEventLogsPendingToPublishAsync(Guid transactionId, CancellationToken cancellationToken = default)
+ {
+ var result = await dbContext.Set().Where(e => e.TransactionId == transactionId && e.Status == DistributedEventStatus.NotPublished).ToListAsync(cancellationToken: cancellationToken);
+
+ return result.Count != 0 ? result.OrderBy(o => o.CreationTime) : [];
+ }
+
+ public async Task AddAndSaveEventAsync(DistributedEvent @event, CancellationToken cancellationToken = default)
+ {
+ var transaction = dbContext.Database.CurrentTransaction ?? throw new InvalidOperationException("This method must be called within a transaction scope.");
+
+ var eventLog = new DistributedEventLog(@event, transaction.TransactionId);
+
+ await dbContext.AddAsync(eventLog, cancellationToken);
+
+ await dbContext.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task PublishEventsThroughEventBusAsync(Guid transactionId, CancellationToken cancellationToken = default)
+ {
+ var pendingEventLogs = await RetrieveEventLogsPendingToPublishAsync(transactionId, cancellationToken);
+
+ foreach (var eventLog in pendingEventLogs)
+ {
+ logger.LogInformation("Publishing integration event {EventId} {DistributedEvent}", eventLog.EventId, eventLog.DistributedEvent);
+
+ try
+ {
+ await UpdateEventStatusAsync(eventLog.EventId, DistributedEventStatus.InProgress, cancellationToken);
+ await distributedEventBus.PublishAsync(eventLog.DistributedEvent, cancellationToken);
+ await UpdateEventStatusAsync(eventLog.EventId, DistributedEventStatus.Published, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error publishing distributed event {EventId}", eventLog.EventId);
+
+ await UpdateEventStatusAsync(eventLog.EventId, DistributedEventStatus.PublishedFailed, cancellationToken);
+ }
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ dbContext.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+}
diff --git a/src/HelloShop.OrderingService/Services/IDistributedEventService.cs b/src/HelloShop.OrderingService/Services/IDistributedEventService.cs
new file mode 100644
index 0000000..2e24319
--- /dev/null
+++ b/src/HelloShop.OrderingService/Services/IDistributedEventService.cs
@@ -0,0 +1,19 @@
+// Copyright (c) HelloShop Corporation. All rights reserved.
+// See the license file in the project root for more information.
+
+using HelloShop.OrderingService.Entities.EventLogs;
+using HelloShop.ServiceDefaults.DistributedEvents.Abstractions;
+
+namespace HelloShop.OrderingService.Services
+{
+ public interface IDistributedEventService
+ {
+ Task> RetrieveEventLogsPendingToPublishAsync(Guid transactionId, CancellationToken cancellationToken = default);
+
+ Task AddAndSaveEventAsync(DistributedEvent @event, CancellationToken cancellationToken = default);
+
+ Task UpdateEventStatusAsync(Guid eventId, DistributedEventStatus status, CancellationToken cancellationToken = default);
+
+ Task PublishEventsThroughEventBusAsync(Guid transactionId, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/HelloShop.ProductService/HelloShop.ProductService.csproj b/src/HelloShop.ProductService/HelloShop.ProductService.csproj
index af10394..125b29b 100644
--- a/src/HelloShop.ProductService/HelloShop.ProductService.csproj
+++ b/src/HelloShop.ProductService/HelloShop.ProductService.csproj
@@ -8,11 +8,11 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
\ No newline at end of file
diff --git a/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj b/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj
index 955f878..032b2a7 100644
--- a/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj
+++ b/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj
@@ -8,14 +8,14 @@
-
-
+
+
-
+
diff --git a/tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj b/tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj
index df16895..4f293d5 100644
--- a/tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj
+++ b/tests/HelloShop.BasketService.FunctionalTests/HelloShop.BasketService.FunctionalTests.csproj
@@ -15,15 +15,15 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj b/tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj
index 0c01a46..131e442 100644
--- a/tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj
+++ b/tests/HelloShop.BasketService.UnitTests/HelloShop.BasketService.UnitTests.csproj
@@ -14,9 +14,9 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj b/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj
index 5396ee6..a949376 100644
--- a/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj
+++ b/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj
@@ -9,13 +9,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj b/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj
index b1e92fe..9068ac2 100644
--- a/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj
+++ b/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj
@@ -14,10 +14,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj b/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj
index 1e39637..0454e9f 100644
--- a/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj
+++ b/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj
@@ -14,10 +14,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive