namespace BuildingBlocks.EFCore; 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 readonly ILogger? _logger; private IDbContextTransaction _currentTransaction; protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider? currentUserProvider = null, ILogger? logger = null) : base(options) { _currentUserProvider = currentUserProvider; _logger = logger; } protected override void OnModelCreating(ModelBuilder builder) { } public IExecutionStrategy CreateExecutionStrategy() => Database.CreateExecutionStrategy(); public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { if (_currentTransaction != null) return; _currentTransaction = await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); } public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) { try { await SaveChangesAsync(cancellationToken); await _currentTransaction?.CommitAsync(cancellationToken)!; } catch { await RollbackTransactionAsync(cancellationToken); throw; } finally { _currentTransaction?.Dispose(); _currentTransaction = null; } } public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) { try { await _currentTransaction?.RollbackAsync(cancellationToken)!; } finally { _currentTransaction?.Dispose(); _currentTransaction = null; } } //ref: https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency#execution-strategies-and-transactions public Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default) { var strategy = 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 IReadOnlyList GetDomainEvents() { var domainEntities = ChangeTracker .Entries() .Where(x => x.Entity.DomainEvents.Any()) .Select(x => x.Entity) .ToList(); var domainEvents = domainEntities .SelectMany(x => x.DomainEvents) .ToImmutableList(); domainEntities.ForEach(entity => entity.ClearDomainEvents()); return domainEvents.ToImmutableList(); } // ref: https://www.meziantou.net/entity-framework-core-generate-tracking-columns.htm // ref: https://www.meziantou.net/entity-framework-core-soft-delete-using-query-filters.htm private void OnBeforeSaving() { try { foreach (var entry in ChangeTracker.Entries()) { var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate)); var userId = _currentUserProvider?.GetCurrentUserId() ?? 0; if (isAuditable) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedBy = userId; entry.Entity.CreatedAt = DateTime.Now; break; case EntityState.Modified: entry.Entity.LastModifiedBy = userId; entry.Entity.LastModified = DateTime.Now; entry.Entity.Version++; break; case EntityState.Deleted: entry.State = EntityState.Modified; entry.Entity.LastModifiedBy = userId; entry.Entity.LastModified = DateTime.Now; entry.Entity.IsDeleted = true; entry.Entity.Version++; break; } } } } catch (Exception ex) { throw new Exception("try for find IAggregate", ex); } } }