diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Extensions/BulkUpsertExtensions.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Extensions/BulkUpsertExtensions.cs
new file mode 100644
index 000000000..212edb898
--- /dev/null
+++ b/src/modules/Elsa.EntityFrameworkCore.Common/Extensions/BulkUpsertExtensions.cs
@@ -0,0 +1,384 @@
+
+
+using System.Text;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using System.Linq.Expressions;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.EntityFrameworkCore.Extensions;
+
+///
+/// Provides extension methods to perform bulk upsert operations for entities
+/// in an Entity Framework Core context, supporting multiple database providers.
+///
+public static class BulkUpsertExtensions
+{
+ ///
+ /// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector.
+ ///
+ /// The type of the database context.
+ /// The type of the entity being upserted.
+ /// The database context where the bulk upsert operation will be executed.
+ /// The list of entities to be upserted.
+ /// An expression used to determine the key for upsert operations.
+ /// A token to observe while waiting for the operation to complete.
+ public static async Task BulkUpsertAsync(
+ this TDbContext dbContext,
+ IList entities,
+ Expression> keySelector,
+ CancellationToken cancellationToken = default)
+ where TDbContext : DbContext
+ where TEntity : class, new()
+ {
+ await BulkUpsertAsync(dbContext, entities, keySelector, 50, cancellationToken);
+ }
+
+ ///
+ /// Performs a bulk upsert operation on a list of entities in the specified database context using a key selector and optional batch size.
+ ///
+ /// The type of the database context.
+ /// The type of the entity being upserted.
+ /// The database context where the bulk upsert operation will be executed.
+ /// The list of entities to be upserted.
+ /// An expression used to determine the key for upsert operations.
+ /// The size of each batch for processing the upsert operation. Defaults to 50.
+ /// A token to observe while waiting for the operation to complete.
+ /// Thrown if the database provider for the context is not supported.
+ public static async Task BulkUpsertAsync(
+ this TDbContext dbContext,
+ IList entities,
+ Expression> keySelector,
+ int batchSize = 50,
+ CancellationToken cancellationToken = default)
+ where TDbContext : DbContext
+ where TEntity : class, new()
+ {
+ if (entities.Count == 0)
+ return;
+
+ // Identify the current provider (e.g., "Microsoft.EntityFrameworkCore.SqlServer")
+ var providerName = dbContext.Database.ProviderName?.ToLowerInvariant() ?? string.Empty;
+
+ // Determine the method for generating SQL based on the provider
+ Func, Expression>, (string, object[])> generateSql = providerName switch
+ {
+ var pn when pn.Contains("sqlserver") => GenerateSqlServerUpsert,
+ var pn when pn.Contains("sqlite") => GenerateSqliteUpsert,
+ var pn when pn.Contains("postgres") => GeneratePostgresUpsert,
+ var pn when pn.Contains("mysql") => GenerateMySqlUpsert,
+ var pn when pn.Contains("oracle") => GenerateOracleUpsert,
+ _ => throw new NotSupportedException($"Provider '{providerName}' is not supported.")
+ };
+
+ // Loop through batched entities
+ foreach (var batch in entities.Chunk(batchSize))
+ {
+ // Generate SQL and parameters
+ var (sql, parameters) = generateSql(dbContext, batch, keySelector);
+
+ await dbContext.Database.ExecuteSqlRawAsync(sql, parameters, cancellationToken);
+ }
+ }
+
+ private static (string, object[]) GenerateSqlServerUpsert(
+ DbContext dbContext,
+ IList entities,
+ Expression> keySelector)
+ where TEntity : class
+ {
+ var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!;
+ var tableName = $"[{entityType.GetSchema()}].[{entityType.GetTableName()}]";
+ var storeObject = StoreObjectIdentifier.Table(entityType.GetTableName()!, entityType.GetSchema());
+
+ // Include shadow properties
+ var props = entityType.GetProperties().ToList();
+
+ var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!;
+ var keyColumnName = $"[{keyProp.GetColumnName(storeObject)}]";
+ var columnNames = props
+ .Select(p => $"[{p.GetColumnName(storeObject)}]")
+ .ToList();
+
+ var mergeSql = new StringBuilder();
+ mergeSql.AppendLine($"MERGE {tableName} AS Target");
+ mergeSql.AppendLine("USING (VALUES");
+
+ var parameters = new List