diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Contracts/IDbExceptionHandler.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Contracts/IDbExceptionHandler.cs new file mode 100644 index 000000000..78c8707fd --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Contracts/IDbExceptionHandler.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace Elsa.EntityFrameworkCore.Common.Contracts; + +/// Defines the contract for an exception handler in a database context. +/// +/// Implementing this interface allows for customized handling of exceptions that occur during database operations. +/// The parameter is used to be able to inject different ExceptionHandlers for each DbContext. +/// +public interface IDbExceptionHandler where TDbContext : DbContext +{ + /// Handles the given exception that occurs during database operations. + public void Handle(DbUpdateException exception); +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/EntityStore.cs b/src/modules/Elsa.EntityFrameworkCore.Common/EntityStore.cs index d97a47a06..1097da421 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/EntityStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/EntityStore.cs @@ -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; /// /// The type of the database context. /// The type of the entity. -public class EntityStore : Store where TDbContext : DbContext where TEntity : Entity, new() +public class EntityStore(IDbContextFactory dbContextFactory, IDbExceptionHandler exceptionHandler) + : Store(dbContextFactory, exceptionHandler) where TDbContext : DbContext where TEntity : Entity, new() { - /// - public EntityStore(IDbContextFactory dbContextFactory) : base(dbContextFactory) - { - } - /// /// Saves the entity. /// diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs index 622bb773d..c1fbdf658 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs @@ -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; /// The type of the database context. /// The type of the entity. [PublicAPI] -public class Store where TDbContext : DbContext where TEntity : class, new() +public class Store(IDbContextFactory dbContextFactory, IDbExceptionHandler 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 _dbContextFactory; - - /// - /// Initializes a new instance of the class. - /// - public Store(IDbContextFactory dbContextFactory) - { - _dbContextFactory = dbContextFactory; - } - /// /// Creates a new instance of the database context. /// /// The cancellation token. /// The database context. - public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) => await _dbContextFactory.CreateDbContextAsync(cancellationToken); + public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) => await dbContextFactory.CreateDbContextAsync(cancellationToken); /// /// Adds the specified entity. @@ -132,6 +123,10 @@ public class Store 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 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); + } } /// diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Features.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Features.cs index 3bfee3284..064912eb0 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Features.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Features.cs @@ -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; } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Handlers/DbExceptionHandler.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Handlers/DbExceptionHandler.cs new file mode 100644 index 000000000..f23150beb --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Handlers/DbExceptionHandler.cs @@ -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; + +/// +/// Handles database exceptions encountered when using a postgreSQL database. +/// +public class DbExceptionHandler : IDbExceptionHandler, + IDbExceptionHandler, + IDbExceptionHandler, + IDbExceptionHandler, + IDbExceptionHandler +{ + /// 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); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Handlers/NoopDbExceptionHandler.cs b/src/modules/Elsa.EntityFrameworkCore/Handlers/NoopDbExceptionHandler.cs new file mode 100644 index 000000000..7fb04ecb7 --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore/Handlers/NoopDbExceptionHandler.cs @@ -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, + IDbExceptionHandler, + IDbExceptionHandler, + IDbExceptionHandler, + IDbExceptionHandler +{ + /// Handles the given exception that occurs during database operations. + public void Handle(DbUpdateException exception) + { + throw exception; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs index 5a66c8fb4..d2d18bcd7 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs @@ -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. /// [DependsOn(typeof(AlterationsFeature))] -public class EFCoreAlterationsPersistenceFeature : PersistenceFeatureBase +public class EFCoreAlterationsPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - /// - public EFCoreAlterationsPersistenceFeature(IModule module) : base(module) - { - } + /// Delegate for determining the exception handler. + public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler(); /// public override void Configure() @@ -32,6 +32,7 @@ public class EFCoreAlterationsPersistenceFeature : PersistenceFeatureBase(); AddEntityStore(); diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/Feature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/Feature.cs index a31477233..c370c89d8 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/Feature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/Feature.cs @@ -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; /// [DependsOn(typeof(IdentityFeature))] [PublicAPI] -public class EFCoreIdentityPersistenceFeature : PersistenceFeatureBase +public class EFCoreIdentityPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - /// - public EFCoreIdentityPersistenceFeature(IModule module) : base(module) - { - } + /// Delegate for determining the exception handler. + public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler(); /// public override void Configure() @@ -36,6 +36,8 @@ public class EFCoreIdentityPersistenceFeature : PersistenceFeatureBase(); AddEntityStore(); AddEntityStore(); diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Labels/Feature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Labels/Feature.cs index 7dbe21ee1..462d55b3a 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Labels/Feature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Labels/Feature.cs @@ -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 +public class EFCoreLabelPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - public EFCoreLabelPersistenceFeature(IModule module) : base(module) - { - } + /// Delegate for determining the exception handler. + public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler(); public override void Configure() { @@ -28,6 +29,8 @@ public class EFCoreLabelPersistenceFeature : PersistenceFeatureBase(); AddEntityStore(); } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs index 812525368..6c4a28c54 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs @@ -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 feature with an Entity Framework Core persistence provider. /// [DependsOn(typeof(WorkflowManagementFeature))] -public class EFCoreWorkflowDefinitionPersistenceFeature : PersistenceFeatureBase +public class EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - /// - public EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : base(module) - { - } + /// Delegate for determining the exception handler. + public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler(); /// public override void Configure() @@ -32,6 +32,8 @@ public class EFCoreWorkflowDefinitionPersistenceFeature : PersistenceFeatureBase { base.Apply(); + Services.AddScoped(DbExceptionHandler); + AddEntityStore(); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs index d1188a726..aebdfc924 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs @@ -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 feature with an Entity Framework Core persistence provider. /// [DependsOn(typeof(WorkflowManagementFeature))] -public class EFCoreWorkflowInstancePersistenceFeature : PersistenceFeatureBase +public class EFCoreWorkflowInstancePersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - /// - public EFCoreWorkflowInstancePersistenceFeature(IModule module) : base(module) - { - } + /// Delegate for determining the exception handler. + public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler(); /// public override void Configure() @@ -29,6 +29,8 @@ public class EFCoreWorkflowInstancePersistenceFeature : PersistenceFeatureBase(); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs index 3f702d065..f73185bb7 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs @@ -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 +public class WorkflowManagementPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - /// - public WorkflowManagementPersistenceFeature(IModule module) : base(module) - { - } + /// Delegate for determining the exception handler. + public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler(); /// public override void Configure() @@ -34,6 +34,8 @@ public class WorkflowManagementPersistenceFeature : PersistenceFeatureBase(); AddEntityStore(); } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs index 3cf255f7e..caf87fb14 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs @@ -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. /// [DependsOn(typeof(WorkflowRuntimeFeature))] -public class EFCoreWorkflowRuntimePersistenceFeature : PersistenceFeatureBase +public class EFCoreWorkflowRuntimePersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - /// - public EFCoreWorkflowRuntimePersistenceFeature(IModule module) : base(module) - { - } + /// Delegate for determining the exception handler. + public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler(); /// public override void Configure() @@ -42,6 +42,8 @@ public class EFCoreWorkflowRuntimePersistenceFeature : PersistenceFeatureBase(); AddStore(); AddEntityStore(); diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index be3520236..93765f25c 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -165,7 +165,7 @@ public partial class WorkflowExecutionContext : IExecutionContext Action? statusUpdatedCallback = null, CancellationTokens cancellationTokens = default) { - // Setup a workflow execution context. + // Set up a workflow execution context. var workflowExecutionContext = new WorkflowExecutionContext( serviceProvider, workflowGraph, diff --git a/src/modules/Elsa.Workflows.Core/Exceptions/DataProcessingException.cs b/src/modules/Elsa.Workflows.Core/Exceptions/DataProcessingException.cs new file mode 100644 index 000000000..2af7ab1e2 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Exceptions/DataProcessingException.cs @@ -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; +}