using BuildingBlocks.EFCore; using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.PersistMessageProcessor.Data; using Configurations; using Core.Model; using Microsoft.Extensions.Logging; using Exception = System.Exception; using IsolationLevel = System.Data.IsolationLevel; public class PersistMessageDbContext : DbContext, IPersistMessageDbContext { private readonly ILogger? _logger; public PersistMessageDbContext(DbContextOptions options, ILogger? logger = null) : base(options) { _logger = logger; } public DbSet PersistMessages => Set(); protected override void OnModelCreating(ModelBuilder builder) { builder.ApplyConfiguration(new PersistMessageConfiguration()); base.OnModelCreating(builder); builder.ToSnakeCaseTables(); } //ref: https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency#execution-strategies-and-transactions public Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default) { var strategy = Database.CreateExecutionStrategy(); return strategy.ExecuteAsync(async () => { await using var transaction = await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); try { await SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); } catch { await transaction.RollbackAsync(cancellationToken); throw; } }); } 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=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 void CreatePersistMessageTable() { if (Database.GetPendingMigrations().Any()) { throw new InvalidOperationException("Cannot create table if there are pending migrations."); } string createTableSql = @" create table if not exists persist_message ( id uuid not null, data_type text, data text, created timestamp with time zone not null, retry_count integer not null, message_status text not null default 'InProgress'::text, delivery_type text not null default 'Outbox'::text, version bigint not null, constraint pk_persist_message primary key (id) )"; Database.ExecuteSqlRaw(createTableSql); } private void OnBeforeSaving() { try { foreach (var entry in ChangeTracker.Entries()) { switch (entry.State) { case EntityState.Modified: entry.Entity.Version++; break; case EntityState.Deleted: entry.Entity.Version++; break; } } } catch (Exception ex) { throw new Exception("try for find IVersion", ex); } } }