feat: Add retry failure for Npgsql

This commit is contained in:
meysamhadeli 2023-01-23 15:36:08 +03:30
parent c4002bf295
commit 17082a2365
10 changed files with 67 additions and 135 deletions

View File

@ -6,66 +6,41 @@ using BuildingBlocks.Core.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using System.Data;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Web;
using Exception = System.Exception;
public abstract class AppDbContextBase : DbContext, IDbContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
private IDbContextTransaction _currentTransaction;
private readonly ICurrentUserProvider _currentUserProvider;
protected AppDbContextBase(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) :
protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider currentUserProvider) :
base(options)
{
_httpContextAccessor = httpContextAccessor;
_currentUserProvider = currentUserProvider;
}
protected override void OnModelCreating(ModelBuilder builder)
{
// ref: https://github.com/pdevito3/MessageBusTestingInMemHarness/blob/main/RecipeManagement/src/RecipeManagement/Databases/RecipesDbContext.cs
}
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
//ref: https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency#execution-strategies-and-transactions
public Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default)
{
if (_currentTransaction != null)
var strategy = Database.CreateExecutionStrategy();
return strategy.ExecuteAsync(async () =>
{
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;
}
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<int> SaveChangesAsync(CancellationToken cancellationToken = default)
@ -100,7 +75,7 @@ public abstract class AppDbContextBase : DbContext, IDbContext
foreach (var entry in ChangeTracker.Entries<IAggregate>())
{
var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate));
var userId = GetCurrentUserId();
var userId = _currentUserProvider.GetCurrentUserId();
if (isAuditable)
{
@ -133,13 +108,4 @@ public abstract class AppDbContextBase : DbContext, IDbContext
throw new Exception("try for find IAggregate", ex);
}
}
private long? GetCurrentUserId()
{
var nameIdentifier = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
long.TryParse(nameIdentifier, out var userId);
return userId;
}
}

View File

@ -42,29 +42,20 @@ public class EfTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TRe
nameof(EfTxBehavior<TRequest, TResponse>),
typeof(TRequest).FullName);
await _dbContextBase.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
var response = await next();
_logger.LogInformation(
"{Prefix} Executed the {MediatrRequest} request",
nameof(EfTxBehavior<TRequest, TResponse>),
typeof(TRequest).FullName);
_logger.LogInformation(
"{Prefix} Executed the {MediatrRequest} request",
nameof(EfTxBehavior<TRequest, TResponse>),
typeof(TRequest).FullName);
var domainEvents = _dbContextBase.GetDomainEvents();
var domainEvents = _dbContextBase.GetDomainEvents();
await _eventDispatcher.SendAsync(domainEvents.ToArray(), typeof(TRequest), cancellationToken);
await _eventDispatcher.SendAsync(domainEvents.ToArray(), typeof(TRequest), cancellationToken);
await _dbContextBase.CommitTransactionAsync(cancellationToken);
await _dbContextBase.ExecuteTransactionalAsync(cancellationToken);
return response;
}
catch (System.Exception ex)
{
await _dbContextBase.RollbackTransactionAsync(cancellationToken);
throw;
}
return response;
}
}

View File

@ -35,6 +35,8 @@ public static class Extensions
dbOptions =>
{
dbOptions.MigrationsAssembly(typeof(TContext).Assembly.GetName().Name);
//ref: https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
dbOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(1), null);
})
// https://github.com/efcore/EFCore.NamingConventions
.UseSnakeCaseNamingConvention();

View File

@ -7,8 +7,6 @@ public interface IDbContext
{
DbSet<TEntity> Set<TEntity>() where TEntity : class;
IReadOnlyList<IDomainEvent> GetDomainEvents();
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
Task ExecuteTransactionalAsync(CancellationToken cancellationToken = default);
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@ -16,7 +16,7 @@ public class PersistMessageDbContext : DbContext, IPersistMessageDbContext
{
}
public DbSet<PersistMessage> PersistMessages { get; set; }
public DbSet<PersistMessage> PersistMessages => Set<PersistMessage>();
protected override void OnModelCreating(ModelBuilder builder)
{
@ -48,13 +48,13 @@ public class PersistMessageDbContext : DbContext, IPersistMessageDbContext
});
try
{
await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken));
return await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken));
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
var currentEntity = entry.Entity;
var currentEntity = entry.Entity; // we can use it for specific merging
var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken);
if (databaseValues != null)
@ -65,8 +65,6 @@ public class PersistMessageDbContext : DbContext, IPersistMessageDbContext
return await base.SaveChangesAsync(cancellationToken);
}
return 0;
}
private void OnBeforeSaving()

View File

@ -2,10 +2,8 @@
namespace BuildingBlocks.PersistMessageProcessor;
using EFCore;
public interface IPersistMessageDbContext
{
DbSet<PersistMessage> PersistMessages { get; set; }
DbSet<PersistMessage> PersistMessages { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@ -6,11 +6,12 @@ using BuildingBlocks.IdsGenerator;
using BuildingBlocks.Utils;
using MassTransit;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace BuildingBlocks.PersistMessageProcessor;
using Microsoft.EntityFrameworkCore;
public class PersistMessageProcessor : IPersistMessageProcessor
{
private readonly ILogger<PersistMessageProcessor> _logger;

View File

@ -6,12 +6,12 @@ using Microsoft.EntityFrameworkCore;
namespace Flight.Data;
using Microsoft.AspNetCore.Http;
using BuildingBlocks.Web;
public sealed class FlightDbContext : AppDbContextBase
{
public FlightDbContext(DbContextOptions<FlightDbContext> options, IHttpContextAccessor httpContextAccessor = default) : base(
options, httpContextAccessor)
public FlightDbContext(DbContextOptions<FlightDbContext> options, ICurrentUserProvider currentUserProvider = default) : base(
options, currentUserProvider)
{
}
@ -23,8 +23,8 @@ public sealed class FlightDbContext : AppDbContextBase
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.FilterSoftDeletedProperties();
builder.ApplyConfigurationsFromAssembly(typeof(FlightRoot).Assembly);
builder.FilterSoftDeletedProperties();
builder.ToSnakeCaseTables();
}
}

View File

@ -34,54 +34,32 @@ public sealed class IdentityContext : IdentityDbContext<User, Role, long,
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<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
OnBeforeSaving();
return await base.SaveChangesAsync(cancellationToken);
}
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;
}
}
public IReadOnlyList<IDomainEvent> GetDomainEvents()
{
var domainEntities = ChangeTracker

View File

@ -1,16 +1,15 @@
using System.Reflection;
using BuildingBlocks.EFCore;
using Microsoft.EntityFrameworkCore;
using BuildingBlocks.Web;
namespace Passenger.Data;
using Microsoft.AspNetCore.Http;
public sealed class PassengerDbContext : AppDbContextBase
{
public PassengerDbContext(DbContextOptions<PassengerDbContext> options,
IHttpContextAccessor httpContextAccessor = default) :
base(options, httpContextAccessor)
ICurrentUserProvider currentUserProvider = default) :
base(options, currentUserProvider)
{
}
@ -20,6 +19,7 @@ public sealed class PassengerDbContext : AppDbContextBase
{
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(builder);
builder.FilterSoftDeletedProperties();
builder.ToSnakeCaseTables();
}
}