Merge pull request #5869 from elsa-workflows/enh/db_error_handling

Add customizable DB exception handlers
This commit is contained in:
raymonddenhaan 2024-08-09 09:18:59 +02:00 committed by GitHub
commit bf642cb6d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 149 additions and 54 deletions

View file

@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore.Common.Contracts;
/// Defines the contract for an exception handler in a database context.
/// <remarks>
/// <para>Implementing this interface allows for customized handling of exceptions that occur during database operations. </para>
/// <para>The <see cref="TDbContext"/> parameter is used to be able to inject different ExceptionHandlers for each DbContext. </para>
/// </remarks>
public interface IDbExceptionHandler<TDbContext> where TDbContext : DbContext
{
/// Handles the given exception that occurs during database operations.
public void Handle(DbUpdateException exception);
}

View file

@ -1,4 +1,5 @@
using Elsa.Common.Entities;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore.Common;
@ -8,13 +9,9 @@ namespace Elsa.EntityFrameworkCore.Common;
/// </summary>
/// <typeparam name="TDbContext">The type of the database context.</typeparam>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public class EntityStore<TDbContext, TEntity> : Store<TDbContext, TEntity> where TDbContext : DbContext where TEntity : Entity, new()
public class EntityStore<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextFactory, IDbExceptionHandler<TDbContext> exceptionHandler)
: Store<TDbContext, TEntity>(dbContextFactory, exceptionHandler) where TDbContext : DbContext where TEntity : Entity, new()
{
/// <inheritdoc />
public EntityStore(IDbContextFactory<TDbContext> dbContextFactory) : base(dbContextFactory)
{
}
/// <summary>
/// Saves the entity.
/// </summary>

View file

@ -1,6 +1,7 @@
using System.Linq.Expressions;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Extensions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
@ -14,28 +15,18 @@ namespace Elsa.EntityFrameworkCore.Common;
/// <typeparam name="TDbContext">The type of the database context.</typeparam>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
[PublicAPI]
public class Store<TDbContext, TEntity> where TDbContext : DbContext where TEntity : class, new()
public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextFactory, IDbExceptionHandler<TDbContext> exceptionHandler) where TDbContext : DbContext where TEntity : class, new()
{
// ReSharper disable once StaticMemberInGenericType
// Justification: This is a static member that is used to ensure that only one thread can access the database for TEntity at a time.
private static readonly SemaphoreSlim Semaphore = new(1, 1);
private readonly IDbContextFactory<TDbContext> _dbContextFactory;
/// <summary>
/// Initializes a new instance of the <see cref="Store{TDbContext, TEntity}"/> class.
/// </summary>
public Store(IDbContextFactory<TDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
/// <summary>
/// Creates a new instance of the database context.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The database context.</returns>
public async Task<TDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default) => await _dbContextFactory.CreateDbContextAsync(cancellationToken);
public async Task<TDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default) => await dbContextFactory.CreateDbContextAsync(cancellationToken);
/// <summary>
/// Adds the specified entity.
@ -132,6 +123,10 @@ public class Store<TDbContext, TEntity> where TDbContext : DbContext where TEnti
set.Entry(entity).State = exists ? EntityState.Modified : EntityState.Added;
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException ex)
{
exceptionHandler.Handle(ex);
}
finally
{
Semaphore.Release();
@ -168,7 +163,14 @@ public class Store<TDbContext, TEntity> where TDbContext : DbContext where TEnti
await Task.WhenAll(savingTasks);
}
await dbContext.BulkUpsertAsync(entityList, keySelector, cancellationToken);
try
{
await dbContext.BulkUpsertAsync(entityList, keySelector, cancellationToken);
}
catch (DbUpdateException ex)
{
exceptionHandler.Handle(ex);
}
}
/// <summary>

View file

@ -5,6 +5,7 @@ using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.EntityFrameworkCore.PostgreSql.Handlers;
// ReSharper disable once CheckNamespace
namespace Elsa.EntityFrameworkCore.Extensions;
@ -22,6 +23,7 @@ public static class PostgreSqlProvidersExtensions
public static EFCoreIdentityPersistenceFeature UsePostgreSql(this EFCoreIdentityPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(Assembly, connectionString, options);
feature.DbExceptionHandler = _ => new DbExceptionHandler();
return feature;
}
@ -31,6 +33,7 @@ public static class PostgreSqlProvidersExtensions
public static EFCoreAlterationsPersistenceFeature UsePostgreSql(this EFCoreAlterationsPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(Assembly, connectionString, options);
feature.DbExceptionHandler = _ => new DbExceptionHandler();
return feature;
}
@ -40,6 +43,7 @@ public static class PostgreSqlProvidersExtensions
public static EFCoreLabelPersistenceFeature UsePostgreSql(this EFCoreLabelPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(Assembly, connectionString, options);
feature.DbExceptionHandler = _ => new DbExceptionHandler();
return feature;
}
@ -49,6 +53,7 @@ public static class PostgreSqlProvidersExtensions
public static EFCoreWorkflowDefinitionPersistenceFeature UsePostgreSql(this EFCoreWorkflowDefinitionPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(Assembly, connectionString, options);
feature.DbExceptionHandler = _ => new DbExceptionHandler();
return feature;
}
@ -58,6 +63,7 @@ public static class PostgreSqlProvidersExtensions
public static EFCoreWorkflowInstancePersistenceFeature UsePostgreSql(this EFCoreWorkflowInstancePersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(Assembly, connectionString, options);
feature.DbExceptionHandler = _ => new DbExceptionHandler();
return feature;
}
@ -67,6 +73,7 @@ public static class PostgreSqlProvidersExtensions
public static WorkflowManagementPersistenceFeature UsePostgreSql(this WorkflowManagementPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(Assembly, connectionString, options);
feature.DbExceptionHandler = _ => new DbExceptionHandler();
return feature;
}
@ -76,6 +83,7 @@ public static class PostgreSqlProvidersExtensions
public static EFCoreWorkflowRuntimePersistenceFeature UsePostgreSql(this EFCoreWorkflowRuntimePersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(Assembly, connectionString, options);
feature.DbExceptionHandler = _ => new DbExceptionHandler();
return feature;
}
}

View file

@ -0,0 +1,29 @@
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Modules.Alterations;
using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Workflows.Exceptions;
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Elsa.EntityFrameworkCore.PostgreSql.Handlers;
/// <summary>
/// Handles database exceptions encountered when using a postgreSQL database.
/// </summary>
public class DbExceptionHandler : IDbExceptionHandler<AlterationsElsaDbContext>,
IDbExceptionHandler<IdentityElsaDbContext>,
IDbExceptionHandler<LabelsElsaDbContext>,
IDbExceptionHandler<ManagementElsaDbContext>,
IDbExceptionHandler<RuntimeElsaDbContext>
{
/// Handles database exceptions encountered when using a postgreSQL database.
public void Handle(DbUpdateException exception)
{
var ex = exception.InnerException as PostgresException;
throw new DataProcessingException(ex?.SqlState == "23505", "Unable to save data", exception);
}
}

View file

@ -0,0 +1,23 @@
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Modules.Alterations;
using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore.Handlers;
/// A No-Op database exception handler.
public class NoopDbExceptionHandler : IDbExceptionHandler<AlterationsElsaDbContext>,
IDbExceptionHandler<IdentityElsaDbContext>,
IDbExceptionHandler<LabelsElsaDbContext>,
IDbExceptionHandler<ManagementElsaDbContext>,
IDbExceptionHandler<RuntimeElsaDbContext>
{
/// Handles the given exception that occurs during database operations.
public void Handle(DbUpdateException exception)
{
throw exception;
}
}

View file

@ -1,6 +1,8 @@
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Features;
using Elsa.EntityFrameworkCore.Common;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Handlers;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
@ -11,12 +13,10 @@ namespace Elsa.EntityFrameworkCore.Modules.Alterations;
/// Configures the default workflow runtime to use EF Core persistence providers.
/// </summary>
[DependsOn(typeof(AlterationsFeature))]
public class EFCoreAlterationsPersistenceFeature : PersistenceFeatureBase<AlterationsElsaDbContext>
public class EFCoreAlterationsPersistenceFeature(IModule module) : PersistenceFeatureBase<AlterationsElsaDbContext>(module)
{
/// <inheritdoc />
public EFCoreAlterationsPersistenceFeature(IModule module) : base(module)
{
}
/// Delegate for determining the exception handler.
public Func<IServiceProvider, IDbExceptionHandler<AlterationsElsaDbContext>> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
/// <inheritdoc />
public override void Configure()
@ -32,6 +32,7 @@ public class EFCoreAlterationsPersistenceFeature : PersistenceFeatureBase<Altera
public override void Apply()
{
base.Apply();
Services.AddScoped(DbExceptionHandler);
AddEntityStore<AlterationPlan, EFCoreAlterationPlanStore>();
AddEntityStore<AlterationJob, EFCoreAlterationJobStore>();

View file

@ -1,4 +1,6 @@
using Elsa.EntityFrameworkCore.Common;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Handlers;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Identity.Entities;
@ -13,12 +15,10 @@ namespace Elsa.EntityFrameworkCore.Modules.Identity;
/// </summary>
[DependsOn(typeof(IdentityFeature))]
[PublicAPI]
public class EFCoreIdentityPersistenceFeature : PersistenceFeatureBase<IdentityElsaDbContext>
public class EFCoreIdentityPersistenceFeature(IModule module) : PersistenceFeatureBase<IdentityElsaDbContext>(module)
{
/// <inheritdoc />
public EFCoreIdentityPersistenceFeature(IModule module) : base(module)
{
}
/// Delegate for determining the exception handler.
public Func<IServiceProvider, IDbExceptionHandler<IdentityElsaDbContext>> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
/// <inheritdoc />
public override void Configure()
@ -36,6 +36,8 @@ public class EFCoreIdentityPersistenceFeature : PersistenceFeatureBase<IdentityE
{
base.Apply();
Services.AddScoped(DbExceptionHandler);
AddEntityStore<User, EFCoreUserStore>();
AddEntityStore<Application, EFCoreApplicationStore>();
AddEntityStore<Role, EFCoreRoleStore>();

View file

@ -1,4 +1,6 @@
using Elsa.EntityFrameworkCore.Common;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Handlers;
using Elsa.Extensions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
@ -9,11 +11,10 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.EntityFrameworkCore.Modules.Labels;
[DependsOn(typeof(LabelsFeature))]
public class EFCoreLabelPersistenceFeature : PersistenceFeatureBase<LabelsElsaDbContext>
public class EFCoreLabelPersistenceFeature(IModule module) : PersistenceFeatureBase<LabelsElsaDbContext>(module)
{
public EFCoreLabelPersistenceFeature(IModule module) : base(module)
{
}
/// Delegate for determining the exception handler.
public Func<IServiceProvider, IDbExceptionHandler<LabelsElsaDbContext>> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
public override void Configure()
{
@ -28,6 +29,8 @@ public class EFCoreLabelPersistenceFeature : PersistenceFeatureBase<LabelsElsaDb
{
base.Apply();
Services.AddScoped(DbExceptionHandler);
AddEntityStore<Label, EFCoreLabelStore>();
AddEntityStore<WorkflowDefinitionLabel, EFCoreWorkflowDefinitionLabelStore>();
}

View file

@ -1,4 +1,6 @@
using Elsa.EntityFrameworkCore.Common;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Handlers;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Entities;
@ -11,12 +13,10 @@ namespace Elsa.EntityFrameworkCore.Modules.Management;
/// Configures the <see cref="WorkflowDefinitionsFeature"/> feature with an Entity Framework Core persistence provider.
/// </summary>
[DependsOn(typeof(WorkflowManagementFeature))]
public class EFCoreWorkflowDefinitionPersistenceFeature : PersistenceFeatureBase<ManagementElsaDbContext>
public class EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : PersistenceFeatureBase<ManagementElsaDbContext>(module)
{
/// <inheritdoc />
public EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : base(module)
{
}
/// Delegate for determining the exception handler.
public Func<IServiceProvider, IDbExceptionHandler<ManagementElsaDbContext>> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
/// <inheritdoc />
public override void Configure()
@ -32,6 +32,8 @@ public class EFCoreWorkflowDefinitionPersistenceFeature : PersistenceFeatureBase
{
base.Apply();
Services.AddScoped(DbExceptionHandler);
AddEntityStore<WorkflowDefinition, EFCoreWorkflowDefinitionStore>();
}
}

View file

@ -1,4 +1,6 @@
using Elsa.EntityFrameworkCore.Common;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Handlers;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Entities;
@ -11,12 +13,10 @@ namespace Elsa.EntityFrameworkCore.Modules.Management;
/// Configures the <see cref="WorkflowInstancesFeature"/> feature with an Entity Framework Core persistence provider.
/// </summary>
[DependsOn(typeof(WorkflowManagementFeature))]
public class EFCoreWorkflowInstancePersistenceFeature : PersistenceFeatureBase<ManagementElsaDbContext>
public class EFCoreWorkflowInstancePersistenceFeature(IModule module) : PersistenceFeatureBase<ManagementElsaDbContext>(module)
{
/// <inheritdoc />
public EFCoreWorkflowInstancePersistenceFeature(IModule module) : base(module)
{
}
/// Delegate for determining the exception handler.
public Func<IServiceProvider, IDbExceptionHandler<ManagementElsaDbContext>> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
/// <inheritdoc />
public override void Configure()
@ -29,6 +29,8 @@ public class EFCoreWorkflowInstancePersistenceFeature : PersistenceFeatureBase<M
{
base.Apply();
Services.AddScoped(DbExceptionHandler);
AddEntityStore<WorkflowInstance, EFCoreWorkflowInstanceStore>();
}
}

View file

@ -1,4 +1,6 @@
using Elsa.EntityFrameworkCore.Common;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Handlers;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Entities;
@ -15,12 +17,10 @@ namespace Elsa.EntityFrameworkCore.Modules.Management;
[DependsOn(typeof(WorkflowInstancesFeature))]
[DependsOn(typeof(WorkflowDefinitionsFeature))]
[PublicAPI]
public class WorkflowManagementPersistenceFeature : PersistenceFeatureBase<ManagementElsaDbContext>
public class WorkflowManagementPersistenceFeature(IModule module) : PersistenceFeatureBase<ManagementElsaDbContext>(module)
{
/// <inheritdoc />
public WorkflowManagementPersistenceFeature(IModule module) : base(module)
{
}
/// Delegate for determining the exception handler.
public Func<IServiceProvider, IDbExceptionHandler<ManagementElsaDbContext>> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
/// <inheritdoc />
public override void Configure()
@ -34,6 +34,8 @@ public class WorkflowManagementPersistenceFeature : PersistenceFeatureBase<Manag
{
base.Apply();
Services.AddScoped(DbExceptionHandler);
AddEntityStore<WorkflowInstance, EFCoreWorkflowInstanceStore>();
AddEntityStore<WorkflowDefinition, EFCoreWorkflowDefinitionStore>();
}

View file

@ -1,4 +1,6 @@
using Elsa.EntityFrameworkCore.Common;
using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Handlers;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.KeyValues.Entities;
@ -13,12 +15,10 @@ namespace Elsa.EntityFrameworkCore.Modules.Runtime;
/// Configures the default workflow runtime to use EF Core persistence providers.
/// </summary>
[DependsOn(typeof(WorkflowRuntimeFeature))]
public class EFCoreWorkflowRuntimePersistenceFeature : PersistenceFeatureBase<RuntimeElsaDbContext>
public class EFCoreWorkflowRuntimePersistenceFeature(IModule module) : PersistenceFeatureBase<RuntimeElsaDbContext>(module)
{
/// <inheritdoc />
public EFCoreWorkflowRuntimePersistenceFeature(IModule module) : base(module)
{
}
/// Delegate for determining the exception handler.
public Func<IServiceProvider, IDbExceptionHandler<RuntimeElsaDbContext>> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
/// <inheritdoc />
public override void Configure()
@ -42,6 +42,8 @@ public class EFCoreWorkflowRuntimePersistenceFeature : PersistenceFeatureBase<Ru
{
base.Apply();
Services.AddScoped(DbExceptionHandler);
AddEntityStore<StoredTrigger, EFCoreTriggerStore>();
AddStore<StoredBookmark, EFCoreBookmarkStore>();
AddEntityStore<WorkflowInboxMessage, EFCoreWorkflowInboxMessageStore>();

View file

@ -165,7 +165,7 @@ public partial class WorkflowExecutionContext : IExecutionContext
Action<WorkflowExecutionContext>? statusUpdatedCallback = null,
CancellationTokens cancellationTokens = default)
{
// Setup a workflow execution context.
// Set up a workflow execution context.
var workflowExecutionContext = new WorkflowExecutionContext(
serviceProvider,
workflowGraph,

View file

@ -0,0 +1,8 @@
namespace Elsa.Workflows.Exceptions;
/// An exception that occurs during data processing.
public class DataProcessingException(bool isUkViolation, string message, Exception exception) : Exception(message, exception)
{
/// Gets a value indicating whether the exception is a Unique Key violation.
public bool IsUkViolation { get; } = isUkViolation;
}