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 794c139db..1097da421 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Common/EntityStore.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.Common/EntityStore.cs
@@ -1,4 +1,6 @@
using Elsa.Common.Entities;
+using Elsa.EntityFrameworkCore.Common.Contracts;
+using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore.Common;
@@ -7,13 +9,9 @@ namespace Elsa.EntityFrameworkCore.Common;
///
/// The type of the database context.
/// The type of the entity.
-public class EntityStore : Store where TDbContext : ElsaDbContextBase where TEntity : Entity, new()
+public class EntityStore(IDbContextFactory dbContextFactory, IDbExceptionHandler exceptionHandler)
+ : Store(dbContextFactory, exceptionHandler) where TDbContext : DbContext where TEntity : Entity, new()
{
- ///
- public EntityStore(IServiceProvider serviceProvider) : base(serviceProvider)
- {
- }
-
///
/// Saves the entity.
///
diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs
index a73f585d5..801aeaa89 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs
@@ -1,5 +1,6 @@
using Elsa.Common.Entities;
using Elsa.Common.Models;
+using Elsa.EntityFrameworkCore.Common.Contracts;
using Elsa.EntityFrameworkCore.Extensions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
@@ -16,7 +17,7 @@ namespace Elsa.EntityFrameworkCore.Common;
/// The type of the database context.
/// The type of the entity.
[PublicAPI]
-public class Store where TDbContext : ElsaDbContextBase where TEntity : class, new()
+public class Store(IDbContextFactory dbContextFactory, IDbExceptionHandler exceptionHandler) where TDbContext : DbContext where TEntity : class, new()
{
private readonly IServiceProvider _serviceProvider;
@@ -24,28 +25,12 @@ public class Store where TDbContext : ElsaDbContextBase whe
// 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);
- ///
- /// Initializes a new instance of the class.
- ///
- public Store(IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- }
-
///
/// Creates a new instance of the database context.
///
/// The cancellation token.
/// The database context.
- public async Task CreateDbContextAsync(CancellationToken cancellationToken = default)
- {
- var dbContextFactory = _serviceProvider.GetRequiredService>();
- var tenantResolver = _serviceProvider.GetRequiredService();
- var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
- var tenant = await tenantResolver.GetTenantAsync(cancellationToken);
- dbContext.TenantId = tenant?.Id;
- return dbContext;
- }
+ public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) => await dbContextFactory.CreateDbContextAsync(cancellationToken);
///
/// Adds the specified entity.
@@ -142,6 +127,10 @@ public class Store where TDbContext : ElsaDbContextBase whe
set.Entry(entity).State = exists ? EntityState.Modified : EntityState.Added;
await dbContext.SaveChangesAsync(cancellationToken);
}
+ catch (DbUpdateException ex)
+ {
+ exceptionHandler.Handle(ex);
+ }
finally
{
Semaphore.Release();
@@ -178,7 +167,14 @@ public class Store where TDbContext : ElsaDbContextBase whe
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 bf387b703..d27417ade 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs
@@ -2,6 +2,7 @@ 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;
@@ -12,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()
@@ -33,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 af6819c85..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 54cfbaeb1..67819b067 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;
@@ -12,9 +14,11 @@ namespace Elsa.EntityFrameworkCore.Modules.Labels;
/// Configures the feature with an Entity Framework Core persistence provider.
///
[DependsOn(typeof(LabelsFeature))]
-public class EFCoreLabelPersistenceFeature(IModule module) : PersistenceFeatureBase(module)
+public class EFCoreLabelPersistenceFeature(IModule module) : PersistenceFeatureBase(module)
{
- ///
+ /// Delegate for determining the exception handler.
+ public Func> DbExceptionHandler { get; set; } = _ => new NoopDbExceptionHandler();
+
public override void Configure()
{
Module.UseLabels(labels =>
@@ -29,6 +33,8 @@ public class EFCoreLabelPersistenceFeature(IModule module) : PersistenceFeatureB
{
base.Apply();
+ Services.AddScoped(DbExceptionHandler);
+
AddEntityStore