From 31a6d9f6e9d9453b2338c047ac2dbb3c47638dd3 Mon Sep 17 00:00:00 2001 From: Pc Date: Thu, 1 Jun 2023 16:55:31 +0330 Subject: [PATCH] refactor: Refactor persist message processor --- src/BuildingBlocks/BuildingBlocks.csproj | 4 +-- src/BuildingBlocks/Core/Model/IVersion.cs | 1 + src/BuildingBlocks/EFCore/AppDbContextBase.cs | 34 ++++++++++++++++--- .../Data/DesignTimeDbContextFactory.cs | 2 +- .../Data/PersistMessageDbContext.cs | 22 ++---------- .../PersistMessageProcessor.cs | 14 +------- .../Booking/src/Booking/Booking.csproj | 5 +-- .../IntegrationTest/Integration.Test.csproj | 1 + .../Flight/Data/DesignTimeDbContextFactory.cs | 2 +- .../Flight/src/Flight/Data/FlightDbContext.cs | 6 ++-- src/Services/Flight/src/Flight/Flight.csproj | 1 + .../tests/EndToEndTest/EndToEnd.Test.csproj | 1 - .../IntegrationTest/Integration.Test.csproj | 1 + .../tests/UnitTest/Common/DbContextFactory.cs | 2 +- .../Flight/tests/UnitTest/Unit.Test.csproj | 1 + .../src/Identity/Data/IdentityContext.cs | 30 ++++++++++++++-- .../Identity/src/Identity/Identity.csproj | 1 + .../IntegrationTest/Integration.Test.csproj | 1 + .../src/Passenger.Api/Passenger.Api.csproj | 1 + .../Data/DesignTimeDbContextFactory.cs | 2 +- .../src/Passenger/Data/PassengerDbContext.cs | 7 ++-- .../Passenger/src/Passenger/Passenger.csproj | 1 + .../IntegrationTest/Integration.Test.csproj | 1 + 23 files changed, 88 insertions(+), 53 deletions(-) diff --git a/src/BuildingBlocks/BuildingBlocks.csproj b/src/BuildingBlocks/BuildingBlocks.csproj index 81ab599..8595b07 100644 --- a/src/BuildingBlocks/BuildingBlocks.csproj +++ b/src/BuildingBlocks/BuildingBlocks.csproj @@ -3,9 +3,9 @@ net7.0 enable + enable - @@ -144,7 +144,7 @@ - + diff --git a/src/BuildingBlocks/Core/Model/IVersion.cs b/src/BuildingBlocks/Core/Model/IVersion.cs index 3dc2a27..e2b3138 100644 --- a/src/BuildingBlocks/Core/Model/IVersion.cs +++ b/src/BuildingBlocks/Core/Model/IVersion.cs @@ -1,5 +1,6 @@ namespace BuildingBlocks.Core.Model; +// For handling optimistic concurrency public interface IVersion { long Version { get; set; } diff --git a/src/BuildingBlocks/EFCore/AppDbContextBase.cs b/src/BuildingBlocks/EFCore/AppDbContextBase.cs index 4a2684c..9d33010 100644 --- a/src/BuildingBlocks/EFCore/AppDbContextBase.cs +++ b/src/BuildingBlocks/EFCore/AppDbContextBase.cs @@ -4,22 +4,24 @@ using System.Collections.Immutable; using Core.Event; using Core.Model; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; using Web; using Exception = System.Exception; using IsolationLevel = System.Data.IsolationLevel; public abstract class AppDbContextBase : DbContext, IDbContext { - private readonly ICurrentUserProvider _currentUserProvider; - private IDbContextTransaction? _currentTransaction; + private readonly ICurrentUserProvider? _currentUserProvider; + private readonly ILogger? _logger; - protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider currentUserProvider) : + protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider? currentUserProvider = null, ILogger? logger = null) : base(options) { _currentUserProvider = currentUserProvider; + _logger = logger; } + protected override void OnModelCreating(ModelBuilder builder) { } @@ -48,7 +50,29 @@ public abstract class AppDbContextBase : DbContext, IDbContext public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { OnBeforeSaving(); - return await base.SaveChangesAsync(cancellationToken); + try + { + return await base.SaveChangesAsync(cancellationToken); + } + //ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#resolving-concurrency-conflicts + catch (DbUpdateConcurrencyException ex) + { + foreach (var entry in ex.Entries) + { + var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken); + + if (databaseValues == null) + { + _logger.LogError("The record no longer exists in the database, The record has been deleted by another user."); + throw; + } + + // Refresh the original values to bypass next concurrency check + entry.OriginalValues.SetValues(databaseValues); + } + + return await base.SaveChangesAsync(cancellationToken); + } } public IReadOnlyList GetDomainEvents() diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/DesignTimeDbContextFactory.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/DesignTimeDbContextFactory.cs index ce710a6..5609afe 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/DesignTimeDbContextFactory.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/DesignTimeDbContextFactory.cs @@ -11,6 +11,6 @@ public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory _logger; + private readonly ILogger? _logger; public PersistMessageDbContext(DbContextOptions options, - ILogger logger) + ILogger? logger = null) : base(options) { _logger = logger; @@ -34,23 +32,9 @@ public class PersistMessageDbContext : DbContext, IPersistMessageDbContext { OnBeforeSaving(); - var policy = Policy.Handle() - .WaitAndRetryAsync(retryCount: 3, - sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(1), - onRetry: (exception, timeSpan, retryCount, context) => - { - if (exception != null) - { - _logger.LogError(exception, - "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.", - HttpStatusCode.Conflict, - timeSpan, - retryCount); - } - }); try { - return await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken)); + return await base.SaveChangesAsync(cancellationToken); } //ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#resolving-concurrency-conflicts catch (DbUpdateConcurrencyException ex) diff --git a/src/BuildingBlocks/PersistMessageProcessor/PersistMessageProcessor.cs b/src/BuildingBlocks/PersistMessageProcessor/PersistMessageProcessor.cs index ff18f6e..d34d587 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/PersistMessageProcessor.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/PersistMessageProcessor.cs @@ -17,8 +17,6 @@ public class PersistMessageProcessor : IPersistMessageProcessor private readonly IMediator _mediator; private readonly IPersistMessageDbContext _persistMessageDbContext; private readonly IPublishEndpoint _publishEndpoint; - private SemaphoreSlim Semaphore => new SemaphoreSlim(1); - public PersistMessageProcessor( ILogger logger, IMediator mediator, @@ -108,7 +106,6 @@ public class PersistMessageProcessor : IPersistMessageProcessor } } - public async Task ProcessAllAsync(CancellationToken cancellationToken = default) { var messages = await _persistMessageDbContext.PersistMessages @@ -117,16 +114,7 @@ public class PersistMessageProcessor : IPersistMessageProcessor foreach (var message in messages) { - await Semaphore.WaitAsync(cancellationToken); - - try - { - await ProcessAsync(message.Id, message.DeliveryType, cancellationToken); - } - finally - { - Semaphore.Release(); - } + await ProcessAsync(message.Id, message.DeliveryType, cancellationToken); } } diff --git a/src/Services/Booking/src/Booking/Booking.csproj b/src/Services/Booking/src/Booking/Booking.csproj index 6f0239f..22fd0ed 100644 --- a/src/Services/Booking/src/Booking/Booking.csproj +++ b/src/Services/Booking/src/Booking/Booking.csproj @@ -3,6 +3,7 @@ net7.0 enable + enable @@ -29,11 +30,11 @@ - + <_Parameter1>Integration.Test - + diff --git a/src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj b/src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj index c21a265..6b0d8b3 100644 --- a/src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj +++ b/src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj @@ -3,6 +3,7 @@ net7.0 false + enable diff --git a/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs b/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs index 6047782..b9a7a6c 100644 --- a/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs +++ b/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs @@ -11,7 +11,7 @@ namespace Flight.Data builder.UseNpgsql("Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true") .UseSnakeCaseNamingConvention(); - return new FlightDbContext(builder.Options, null); + return new FlightDbContext(builder.Options); } } } diff --git a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs index b1945e3..8f9f77c 100644 --- a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs +++ b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs @@ -7,11 +7,13 @@ using Microsoft.EntityFrameworkCore; namespace Flight.Data; using BuildingBlocks.Web; +using Microsoft.Extensions.Logging; public sealed class FlightDbContext : AppDbContextBase { - public FlightDbContext(DbContextOptions options, ICurrentUserProvider currentUserProvider) : base( - options, currentUserProvider) + public FlightDbContext(DbContextOptions options, ICurrentUserProvider? currentUserProvider = null, + ILogger? logger = null) : base( + options, currentUserProvider, logger) { } diff --git a/src/Services/Flight/src/Flight/Flight.csproj b/src/Services/Flight/src/Flight/Flight.csproj index b8d25fe..5ec1a9a 100644 --- a/src/Services/Flight/src/Flight/Flight.csproj +++ b/src/Services/Flight/src/Flight/Flight.csproj @@ -2,6 +2,7 @@ net7.0 + enable diff --git a/src/Services/Flight/tests/EndToEndTest/EndToEnd.Test.csproj b/src/Services/Flight/tests/EndToEndTest/EndToEnd.Test.csproj index 5d4348e..6acf245 100644 --- a/src/Services/Flight/tests/EndToEndTest/EndToEnd.Test.csproj +++ b/src/Services/Flight/tests/EndToEndTest/EndToEnd.Test.csproj @@ -4,7 +4,6 @@ net7.0 enable enable - false diff --git a/src/Services/Flight/tests/IntegrationTest/Integration.Test.csproj b/src/Services/Flight/tests/IntegrationTest/Integration.Test.csproj index 1807978..ed55732 100644 --- a/src/Services/Flight/tests/IntegrationTest/Integration.Test.csproj +++ b/src/Services/Flight/tests/IntegrationTest/Integration.Test.csproj @@ -3,6 +3,7 @@ net7.0 false + enable diff --git a/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs b/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs index 97fccd7..de15c88 100644 --- a/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs +++ b/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs @@ -23,7 +23,7 @@ public static class DbContextFactory var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options; - var context = new FlightDbContext(options, currentUserProvider: null); + var context = new FlightDbContext(options, currentUserProvider: null, null); // Seed our data FlightDataSeeder(context); diff --git a/src/Services/Flight/tests/UnitTest/Unit.Test.csproj b/src/Services/Flight/tests/UnitTest/Unit.Test.csproj index 7ebb982..d69eeb2 100644 --- a/src/Services/Flight/tests/UnitTest/Unit.Test.csproj +++ b/src/Services/Flight/tests/UnitTest/Unit.Test.csproj @@ -3,6 +3,7 @@ net7.0 false + enable diff --git a/src/Services/Identity/src/Identity/Data/IdentityContext.cs b/src/Services/Identity/src/Identity/Data/IdentityContext.cs index ff67c9a..2195862 100644 --- a/src/Services/Identity/src/Identity/Data/IdentityContext.cs +++ b/src/Services/Identity/src/Identity/Data/IdentityContext.cs @@ -16,14 +16,16 @@ namespace Identity.Data; using System; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; public sealed class IdentityContext : IdentityDbContext, IDbContext { - private IDbContextTransaction? _currentTransaction; + private readonly ILogger? _logger; - public IdentityContext(DbContextOptions options) : base(options) + public IdentityContext(DbContextOptions options, ILogger? logger = null) : base(options) { + _logger = logger; } protected override void OnModelCreating(ModelBuilder builder) @@ -58,7 +60,29 @@ public sealed class IdentityContext : IdentityDbContext SaveChangesAsync(CancellationToken cancellationToken = default) { OnBeforeSaving(); - return await base.SaveChangesAsync(cancellationToken); + try + { + return await base.SaveChangesAsync(cancellationToken); + } + //ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#resolving-concurrency-conflicts + catch (DbUpdateConcurrencyException ex) + { + foreach (var entry in ex.Entries) + { + var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken); + + if (databaseValues == null) + { + _logger.LogError("The record no longer exists in the database, The record has been deleted by another user."); + throw; + } + + // Refresh the original values to bypass next concurrency check + entry.OriginalValues.SetValues(databaseValues); + } + + return await base.SaveChangesAsync(cancellationToken); + } } public IReadOnlyList GetDomainEvents() diff --git a/src/Services/Identity/src/Identity/Identity.csproj b/src/Services/Identity/src/Identity/Identity.csproj index 26628df..cf433e1 100644 --- a/src/Services/Identity/src/Identity/Identity.csproj +++ b/src/Services/Identity/src/Identity/Identity.csproj @@ -2,6 +2,7 @@ net7.0 + enable diff --git a/src/Services/Identity/tests/IntegrationTest/Integration.Test.csproj b/src/Services/Identity/tests/IntegrationTest/Integration.Test.csproj index 5cefc2e..7153e37 100644 --- a/src/Services/Identity/tests/IntegrationTest/Integration.Test.csproj +++ b/src/Services/Identity/tests/IntegrationTest/Integration.Test.csproj @@ -3,6 +3,7 @@ net7.0 false + enable diff --git a/src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj b/src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj index 91eafd6..f18f014 100644 --- a/src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj +++ b/src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj @@ -3,6 +3,7 @@ net7.0 enable + enable diff --git a/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs b/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs index 87860fb..d56404d 100644 --- a/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs +++ b/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs @@ -11,6 +11,6 @@ public class DesignTimeDbContextFactory: IDesignTimeDbContextFactory options, ICurrentUserProvider currentUserProvider) : - base(options, currentUserProvider) + public PassengerDbContext(DbContextOptions options, + ICurrentUserProvider? currentUserProvider = null, ILogger? logger = null) : + base(options, currentUserProvider, logger) { } diff --git a/src/Services/Passenger/src/Passenger/Passenger.csproj b/src/Services/Passenger/src/Passenger/Passenger.csproj index afe0565..40cf04a 100644 --- a/src/Services/Passenger/src/Passenger/Passenger.csproj +++ b/src/Services/Passenger/src/Passenger/Passenger.csproj @@ -3,6 +3,7 @@ net7.0 enable + enable diff --git a/src/Services/Passenger/tests/IntegrationTest/Integration.Test.csproj b/src/Services/Passenger/tests/IntegrationTest/Integration.Test.csproj index f96ccb9..686c844 100644 --- a/src/Services/Passenger/tests/IntegrationTest/Integration.Test.csproj +++ b/src/Services/Passenger/tests/IntegrationTest/Integration.Test.csproj @@ -3,6 +3,7 @@ net7.0 false + enable