diff --git a/src/BuildingBlocks/EFCore/AppDbContextBase.cs b/src/BuildingBlocks/EFCore/AppDbContextBase.cs index 7ae1f1d..437a589 100644 --- a/src/BuildingBlocks/EFCore/AppDbContextBase.cs +++ b/src/BuildingBlocks/EFCore/AppDbContextBase.cs @@ -1,18 +1,13 @@ +namespace BuildingBlocks.EFCore; + using System.Collections.Immutable; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; - -namespace BuildingBlocks.EFCore; - using System.Data; -using System.Net; using System.Security.Claims; -using global::Polly; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Exception = System.Exception; public abstract class AppDbContextBase : DbContext, IDbContext @@ -76,51 +71,7 @@ public abstract class AppDbContextBase : DbContext, IDbContext public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { OnBeforeSaving(); - try - { - return await base.SaveChangesAsync(cancellationToken); - } - //ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api#resolving-concurrency-conflicts - catch (DbUpdateConcurrencyException ex) - { - throw new DbUpdateConcurrencyException("try for get unhandled exception with DbUpdateConcurrencyException", ex); - var logger = _httpContextAccessor?.HttpContext?.RequestServices - .GetRequiredService>(); - - var entry = ex.Entries.SingleOrDefault(); - - if (entry == null) - { - return 0; - } - - var currentValue = entry.CurrentValues; - var databaseValue = await entry.GetDatabaseValuesAsync(cancellationToken); - - logger?.LogInformation("The entity being updated is already use by another Thread!" + - " database value is: {DatabaseValue} and current value is: {CurrentValue}", - databaseValue, currentValue); - - 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); - } - }); - return await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken)); - } - catch (Exception ex) - { - throw new Exception("try for get unhandled exception bt default", ex); - } + return await base.SaveChangesAsync(cancellationToken); } public IReadOnlyList GetDomainEvents() diff --git a/src/BuildingBlocks/EFCore/Extensions.cs b/src/BuildingBlocks/EFCore/Extensions.cs index 7237edb..45d76f3 100644 --- a/src/BuildingBlocks/EFCore/Extensions.cs +++ b/src/BuildingBlocks/EFCore/Extensions.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Hosting; namespace BuildingBlocks.EFCore; +using Ardalis.GuardClauses; using Humanizer; using Microsoft.EntityFrameworkCore.Metadata; @@ -28,6 +29,8 @@ public static class Extensions { var postgresOptions = sp.GetRequiredService(); + Guard.Against.Null(options, nameof(postgresOptions)); + options.UseNpgsql(postgresOptions?.ConnectionString, dbOptions => { diff --git a/src/BuildingBlocks/EFCore/DatabaseOptions.cs b/src/BuildingBlocks/EFCore/PostgresOptions.cs similarity index 100% rename from src/BuildingBlocks/EFCore/DatabaseOptions.cs rename to src/BuildingBlocks/EFCore/PostgresOptions.cs diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs index 5d75328..3142840 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Configurations/PersistMessageConfiguration.cs @@ -14,6 +14,9 @@ public class PersistMessageConfiguration : IEntityTypeConfiguration r.Id) .IsRequired().ValueGeneratedNever(); + // // ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api + builder.Property(r => r.Version).IsConcurrencyToken(); + builder.Property(x => x.DeliveryType) .HasDefaultValue(MessageDeliveryType.Outbox) .HasConversion( diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.Designer.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.Designer.cs similarity index 91% rename from src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.Designer.cs rename to src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.Designer.cs index 3265c90..55c6a0a 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.Designer.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace BuildingBlocks.PersistMessageProcessor.Data.Migrations { [DbContext(typeof(PersistMessageDbContext))] - [Migration("20230120222214_initial")] + [Migration("20230122153121_initial")] partial class initial { /// @@ -61,6 +61,11 @@ namespace BuildingBlocks.PersistMessageProcessor.Data.Migrations .HasColumnType("integer") .HasColumnName("retry_count"); + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + b.HasKey("Id") .HasName("pk_persist_message"); diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.cs similarity index 94% rename from src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.cs rename to src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.cs index d69e646..47847cf 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230120222214_initial.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/20230122153121_initial.cs @@ -21,7 +21,8 @@ namespace BuildingBlocks.PersistMessageProcessor.Data.Migrations created = table.Column(type: "timestamp with time zone", nullable: false), retrycount = table.Column(name: "retry_count", type: "integer", nullable: false), messagestatus = table.Column(name: "message_status", type: "text", nullable: false, defaultValue: "InProgress"), - deliverytype = table.Column(name: "delivery_type", type: "text", nullable: false, defaultValue: "Outbox") + deliverytype = table.Column(name: "delivery_type", type: "text", nullable: false, defaultValue: "Outbox"), + version = table.Column(type: "bigint", nullable: false) }, constraints: table => { diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs index 5b82f17..2f55c12 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/Migrations/PersistMessageDbContextModelSnapshot.cs @@ -58,6 +58,11 @@ namespace BuildingBlocks.PersistMessageProcessor.Data.Migrations .HasColumnType("integer") .HasColumnName("retry_count"); + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint") + .HasColumnName("version"); + b.HasKey("Id") .HasName("pk_persist_message"); diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs index 295868b..b0ecdaa 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs @@ -1,22 +1,85 @@ using BuildingBlocks.EFCore; -using BuildingBlocks.PersistMessageProcessor.Data.Configurations; using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.PersistMessageProcessor.Data; -using Microsoft.AspNetCore.Http; +using System.Net; +using Configurations; +using global::Polly; +using Microsoft.Extensions.Logging; -public class PersistMessageDbContext : AppDbContextBase, IPersistMessageDbContext +public class PersistMessageDbContext : DbContext, IPersistMessageDbContext { - public PersistMessageDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) - : base(options, httpContextAccessor) + public PersistMessageDbContext(DbContextOptions options) + : base(options) { } + public DbSet PersistMessages { get; set; } + protected override void OnModelCreating(ModelBuilder builder) { builder.ApplyConfiguration(new PersistMessageConfiguration()); base.OnModelCreating(builder); builder.ToSnakeCaseTables(); } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + OnBeforeSaving(); + + var policy = Policy.Handle() + .WaitAndRetryAsync(retryCount: 3, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(1), + onRetry: (exception, timeSpan, retryCount, context) => + { + if (exception != null) + { + var factory = LoggerFactory.Create(b => b.AddConsole()); + var logger = factory.CreateLogger(); + + logger.LogError(exception, + "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.", + HttpStatusCode.Conflict, + timeSpan, + retryCount); + } + }); + try + { + await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken)); + } + catch (DbUpdateConcurrencyException ex) + { + foreach (var entry in ex.Entries) + { + var currentEntity = entry.Entity; + var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken); + + if (databaseValues != null) + entry.OriginalValues.SetValues(databaseValues); + } + + return await base.SaveChangesAsync(cancellationToken); + } + + return 0; + } + + private void OnBeforeSaving() + { + foreach (var entry in ChangeTracker.Entries()) + { + switch (entry.State) + { + case EntityState.Modified: + entry.Entity.Version++; + break; + + case EntityState.Deleted: + entry.Entity.Version++; + break; + } + } + } } diff --git a/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs b/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs index 2552126..0eee375 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/IPersistMessageDbContext.cs @@ -1,9 +1,11 @@ -using BuildingBlocks.EFCore; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.PersistMessageProcessor; -public interface IPersistMessageDbContext : IDbContext +using EFCore; + +public interface IPersistMessageDbContext { - DbSet PersistMessages => Set(); + DbSet PersistMessages { get; set; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs b/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs index 4335eb2..7752658 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/PersistMessage.cs @@ -1,6 +1,4 @@ -using System.Reflection; - -namespace BuildingBlocks.PersistMessageProcessor; +namespace BuildingBlocks.PersistMessageProcessor; public class PersistMessage { @@ -22,6 +20,7 @@ public class PersistMessage public int RetryCount { get; private set; } public MessageStatus MessageStatus { get; private set; } public MessageDeliveryType DeliveryType { get; private set; } + public long Version { get; set; } public void ChangeState(MessageStatus messageStatus) { diff --git a/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs b/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs index 00cdc5f..0fc0be6 100644 --- a/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs +++ b/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs @@ -100,7 +100,7 @@ public static class InfrastructureExtensions }); app.UseCorrelationId(); app.UseHttpMetrics(); - app.UseMigration(env); + // app.UseMigration(env); app.UseCustomHealthCheck(); app.MapMetrics(); app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));