diff --git a/Directory.Packages.props b/Directory.Packages.props
index f994e73c3..7c500f677 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -15,6 +15,7 @@
+
@@ -84,7 +85,7 @@
-
+
diff --git a/Elsa.sln b/Elsa.sln
index 19e4b2aa6..619e260be 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -91,6 +91,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54
docker\ElsaStudio.Dockerfile = docker\ElsaStudio.Dockerfile
docker\init-db.sh = docker\init-db.sh
docker\otel-collector-config.yaml = docker\otel-collector-config.yaml
+ docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Elasticsearch", "src\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj", "{3246883E-2FA7-4B4A-BDC5-99039A2869BC}"
@@ -384,6 +385,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Tenants.AspNetCore", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql\Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql.csproj", "{2B939AC9-03A4-479E-AA0D-CB58F4A7F480}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Kafka", "src\modules\Elsa.Kafka\Elsa.Kafka.csproj", "{BF934627-F531-44FB-BEC2-ECA801FF31E7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -816,6 +819,10 @@ Global
{2B939AC9-03A4-479E-AA0D-CB58F4A7F480}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B939AC9-03A4-479E-AA0D-CB58F4A7F480}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B939AC9-03A4-479E-AA0D-CB58F4A7F480}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -960,6 +967,7 @@ Global
{D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912}
{2B939AC9-03A4-479E-AA0D-CB58F4A7F480} = {50470834-4CD8-479A-8B58-0A1869BA5D37}
{2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {BF934627-F531-44FB-BEC2-ECA801FF31E7} = {DD089B8B-DA73-492A-9010-F772D1C178DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/docker/docker-compose-kafka.yml b/docker/docker-compose-kafka.yml
new file mode 100644
index 000000000..106510d77
--- /dev/null
+++ b/docker/docker-compose-kafka.yml
@@ -0,0 +1,167 @@
+---
+version: '2'
+services:
+
+ broker:
+ image: confluentinc/cp-kafka:7.7.1
+ hostname: broker
+ container_name: broker
+ ports:
+ - "9092:9092"
+ - "9101:9101"
+ environment:
+ KAFKA_NODE_ID: 1
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
+ KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092'
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+ KAFKA_JMX_PORT: 9101
+ KAFKA_JMX_HOSTNAME: localhost
+ KAFKA_PROCESS_ROLES: 'broker,controller'
+ KAFKA_CONTROLLER_QUORUM_VOTERS: '1@broker:29093'
+ KAFKA_LISTENERS: 'PLAINTEXT://broker:29092,CONTROLLER://broker:29093,PLAINTEXT_HOST://0.0.0.0:9092'
+ KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
+ KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
+ KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
+ # Replace CLUSTER_ID with a unique base64 UUID using "bin/kafka-storage.sh random-uuid"
+ # See https://docs.confluent.io/kafka/operations-tools/kafka-tools.html#kafka-storage-sh
+ CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk'
+
+ schema-registry:
+ image: confluentinc/cp-schema-registry:7.7.1
+ hostname: schema-registry
+ container_name: schema-registry
+ depends_on:
+ - broker
+ ports:
+ - "8081:8081"
+ environment:
+ SCHEMA_REGISTRY_HOST_NAME: schema-registry
+ SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'broker:29092'
+ SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081
+
+ connect:
+ image: cnfldemos/cp-server-connect-datagen:0.6.4-7.6.0
+ hostname: connect
+ container_name: connect
+ depends_on:
+ - broker
+ - schema-registry
+ ports:
+ - "8083:8083"
+ environment:
+ CONNECT_BOOTSTRAP_SERVERS: 'broker:29092'
+ CONNECT_REST_ADVERTISED_HOST_NAME: connect
+ CONNECT_GROUP_ID: compose-connect-group
+ CONNECT_CONFIG_STORAGE_TOPIC: docker-connect-configs
+ CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1
+ CONNECT_OFFSET_FLUSH_INTERVAL_MS: 10000
+ CONNECT_OFFSET_STORAGE_TOPIC: docker-connect-offsets
+ CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1
+ CONNECT_STATUS_STORAGE_TOPIC: docker-connect-status
+ CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1
+ CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter
+ CONNECT_VALUE_CONVERTER: io.confluent.connect.avro.AvroConverter
+ CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry:8081
+ # CLASSPATH required due to CC-2422
+ CLASSPATH: /usr/share/java/monitoring-interceptors/monitoring-interceptors-7.7.1.jar
+ CONNECT_PRODUCER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringProducerInterceptor"
+ CONNECT_CONSUMER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringConsumerInterceptor"
+ CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components"
+ CONNECT_LOG4J_LOGGERS: org.apache.zookeeper=ERROR,org.I0Itec.zkclient=ERROR,org.reflections=ERROR
+
+ control-center:
+ image: confluentinc/cp-enterprise-control-center:7.7.1
+ hostname: control-center
+ container_name: control-center
+ depends_on:
+ - broker
+ - schema-registry
+ - connect
+ - ksqldb-server
+ ports:
+ - "9021:9021"
+ environment:
+ CONTROL_CENTER_BOOTSTRAP_SERVERS: 'broker:29092'
+ CONTROL_CENTER_CONNECT_CONNECT-DEFAULT_CLUSTER: 'connect:8083'
+ CONTROL_CENTER_CONNECT_HEALTHCHECK_ENDPOINT: '/connectors'
+ CONTROL_CENTER_KSQL_KSQLDB1_URL: "http://ksqldb-server:8088"
+ CONTROL_CENTER_KSQL_KSQLDB1_ADVERTISED_URL: "http://localhost:8088"
+ CONTROL_CENTER_SCHEMA_REGISTRY_URL: "http://schema-registry:8081"
+ CONTROL_CENTER_REPLICATION_FACTOR: 1
+ CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1
+ CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1
+ CONFLUENT_METRICS_TOPIC_REPLICATION: 1
+ PORT: 9021
+
+ ksqldb-server:
+ image: confluentinc/cp-ksqldb-server:7.7.1
+ hostname: ksqldb-server
+ container_name: ksqldb-server
+ depends_on:
+ - broker
+ - connect
+ ports:
+ - "8088:8088"
+ environment:
+ KSQL_CONFIG_DIR: "/etc/ksql"
+ KSQL_BOOTSTRAP_SERVERS: "broker:29092"
+ KSQL_HOST_NAME: ksqldb-server
+ KSQL_LISTENERS: "http://0.0.0.0:8088"
+ KSQL_CACHE_MAX_BYTES_BUFFERING: 0
+ KSQL_KSQL_SCHEMA_REGISTRY_URL: "http://schema-registry:8081"
+ KSQL_PRODUCER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringProducerInterceptor"
+ KSQL_CONSUMER_INTERCEPTOR_CLASSES: "io.confluent.monitoring.clients.interceptor.MonitoringConsumerInterceptor"
+ KSQL_KSQL_CONNECT_URL: "http://connect:8083"
+ KSQL_KSQL_LOGGING_PROCESSING_TOPIC_REPLICATION_FACTOR: 1
+ KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: 'true'
+ KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: 'true'
+
+ ksqldb-cli:
+ image: confluentinc/cp-ksqldb-cli:7.7.1
+ container_name: ksqldb-cli
+ depends_on:
+ - broker
+ - connect
+ - ksqldb-server
+ entrypoint: /bin/sh
+ tty: true
+
+ ksql-datagen:
+ image: confluentinc/ksqldb-examples:7.7.1
+ hostname: ksql-datagen
+ container_name: ksql-datagen
+ depends_on:
+ - ksqldb-server
+ - broker
+ - schema-registry
+ - connect
+ command: "bash -c 'echo Waiting for Kafka to be ready... && \
+ cub kafka-ready -b broker:29092 1 40 && \
+ echo Waiting for Confluent Schema Registry to be ready... && \
+ cub sr-ready schema-registry 8081 40 && \
+ echo Waiting a few seconds for topic creation to finish... && \
+ sleep 11 && \
+ tail -f /dev/null'"
+ environment:
+ KSQL_CONFIG_DIR: "/etc/ksql"
+ STREAMS_BOOTSTRAP_SERVERS: broker:29092
+ STREAMS_SCHEMA_REGISTRY_HOST: schema-registry
+ STREAMS_SCHEMA_REGISTRY_PORT: 8081
+
+ rest-proxy:
+ image: confluentinc/cp-kafka-rest:7.7.1
+ depends_on:
+ - broker
+ - schema-registry
+ ports:
+ - 8082:8082
+ hostname: rest-proxy
+ container_name: rest-proxy
+ environment:
+ KAFKA_REST_HOST_NAME: rest-proxy
+ KAFKA_REST_BOOTSTRAP_SERVERS: 'broker:29092'
+ KAFKA_REST_LISTENERS: "http://0.0.0.0:8082"
+ KAFKA_REST_SCHEMA_REGISTRY_URL: 'http://schema-registry:8081'
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 51cb37e4e..40ee95d18 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -11,6 +11,19 @@
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh # This will initialize the 'tracelens' database
ports:
- "5432:5432"
+
+ mysql:
+ image: mysql:9.1.0
+ container_name: mysql
+ environment:
+ MYSQL_ROOT_PASSWORD: password
+ MYSQL_DATABASE: elsa
+ MYSQL_USER: admin
+ MYSQL_PASSWORD: password
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
mongodb:
image: mongo:latest
@@ -91,5 +104,6 @@
volumes:
postgres-data:
+ mysql_data:
cockroachdb-data:
mongodb_data:
diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
index accb12903..0b5e96890 100644
--- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
+++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
@@ -3,13 +3,17 @@
+
+
+
+
diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index e3d33877c..1f3d9e85d 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -16,6 +16,7 @@ using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Features.Services;
using Elsa.Identity.Multitenancy;
+using Elsa.Kafka;
using Elsa.MassTransit.Extensions;
using Elsa.MongoDb.Extensions;
using Elsa.MongoDb.Modules.Alterations;
@@ -30,6 +31,7 @@ using Elsa.Server.Web;
using Elsa.Server.Web.Extensions;
using Elsa.Server.Web.Filters;
using Elsa.Server.Web.Messages;
+using Elsa.Server.Web.WorkflowContextProviders;
using Elsa.Tenants.AspNetCore;
using Elsa.Tenants.Extensions;
using Elsa.Workflows.Api;
@@ -63,6 +65,7 @@ const bool runEFCoreMigrations = true;
const bool useMemoryStores = false;
const bool useCaching = true;
const bool useAzureServiceBus = false;
+const bool useKafka = true;
const bool useReadOnlyMode = false;
const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests.
const WorkflowRuntime workflowRuntime = WorkflowRuntime.ProtoActor;
@@ -81,6 +84,7 @@ var identityTokenSection = identitySection.GetSection("Tokens");
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var sqlServerConnectionString = configuration.GetConnectionString("SqlServer")!;
var postgresConnectionString = configuration.GetConnectionString("PostgreSql")!;
+var mySqlConnectionString = configuration.GetConnectionString("MySql")!;
var cockroachDbConnectionString = configuration.GetConnectionString("CockroachDb")!;
var mongoDbConnectionString = configuration.GetConnectionString("MongoDb")!;
var azureServiceBusConnectionString = configuration.GetConnectionString("AzureServiceBus")!;
@@ -136,6 +140,8 @@ services
ef.UseSqlServer(sqlServerConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
+ else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
+ ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
@@ -168,6 +174,8 @@ services
ef.UseSqlServer(sqlServerConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
+ else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
+ ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
@@ -236,6 +244,8 @@ services
}
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
+ else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
+ ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
@@ -359,6 +369,8 @@ services
ef.UseSqlServer(sqlServerConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString);
+ else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
+ ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
@@ -435,6 +447,16 @@ services
asb.AzureServiceBusOptions = options => configuration.GetSection("AzureServiceBus").Bind(options);
});
}
+
+ if(useKafka)
+ {
+ elsa.UseKafka(kafka =>
+ {
+ kafka.ConfigureOptions(options => configuration.GetSection("Kafka").Bind(options));
+ });
+
+ services.AddWorkflowContextProvider();
+ }
if (useAgents)
{
diff --git a/src/apps/Elsa.Server.Web/WorkflowContextProviders/ConsumerConfigWorkflowContextProvider.cs b/src/apps/Elsa.Server.Web/WorkflowContextProviders/ConsumerConfigWorkflowContextProvider.cs
new file mode 100644
index 000000000..5acf2f7c6
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/WorkflowContextProviders/ConsumerConfigWorkflowContextProvider.cs
@@ -0,0 +1,22 @@
+using Elsa.Common.Multitenancy;
+using Elsa.Kafka;
+using Elsa.WorkflowContexts.Abstractions;
+using Elsa.Workflows;
+
+namespace Elsa.Server.Web.WorkflowContextProviders;
+
+public class ConsumerDefinitionWorkflowContextProvider(IConsumerDefinitionEnumerator consumerDefinitionEnumerator, ITenantAccessor tenantAccessor) : WorkflowContextProvider
+{
+ protected override async ValueTask LoadAsync(WorkflowExecutionContext workflowExecutionContext)
+ {
+ var tenant = tenantAccessor.Tenant;
+ var tenantId = tenant?.Id;
+ var definitionId = workflowExecutionContext.Workflow.Identity.DefinitionId;
+
+ // Load specific setting here.
+
+ // For now, just return the first consumer definition.
+ var consumerDefinitions = await consumerDefinitionEnumerator.EnumerateAsync(workflowExecutionContext.CancellationToken);
+ return consumerDefinitions.FirstOrDefault();
+ }
+}
\ No newline at end of file
diff --git a/src/apps/Elsa.Server.Web/Workflows/ConsumerWorkflow.cs b/src/apps/Elsa.Server.Web/Workflows/ConsumerWorkflow.cs
new file mode 100644
index 000000000..2f7627367
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Workflows/ConsumerWorkflow.cs
@@ -0,0 +1,33 @@
+using System.Dynamic;
+using System.Text.Json;
+using Elsa.JavaScript.Models;
+using Elsa.Kafka.Activities;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities;
+
+namespace Elsa.Server.Web.Workflows;
+
+public class ConsumerWorkflow : WorkflowBase
+{
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ var message = builder.WithVariable();
+ builder.Name = "Consumer Workflow";
+ builder.Root = new Sequence
+ {
+ Activities =
+ {
+ new MessageReceived
+ {
+ ConsumerDefinitionId = new("consumer-1"),
+ MessageType = new(typeof(ExpandoObject)),
+ Topics = new(["topic-1"]),
+ Predicate = new(JavaScriptExpression.Create("message => message.OrderId == '1'")),
+ Result = new(message),
+ CanStartWorkflow = true
+ },
+ new WriteLine(c => JsonSerializer.Serialize(message.Get(c)))
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/apps/Elsa.Server.Web/Workflows/ProducerWorkflow.cs b/src/apps/Elsa.Server.Web/Workflows/ProducerWorkflow.cs
new file mode 100644
index 000000000..7f6d0958a
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Workflows/ProducerWorkflow.cs
@@ -0,0 +1,22 @@
+using Elsa.Kafka.Activities;
+using Elsa.Workflows;
+
+namespace Elsa.Server.Web.Workflows;
+
+public class ProducerWorkflow : WorkflowBase
+{
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ builder.Name = "Producer Workflow";
+ builder.Root = new SendMessage
+ {
+ Topic = new("topic-2"),
+ ProducerDefinitionId = new("producer-1"),
+ Content = new(() => new
+ {
+ OrderId = "1",
+ CustomerId = "1"
+ })
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json
index 90fe55c1b..6db77cdfa 100644
--- a/src/apps/Elsa.Server.Web/appsettings.json
+++ b/src/apps/Elsa.Server.Web/appsettings.json
@@ -9,6 +9,7 @@
"ConnectionStrings": {
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;",
"PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60",
+ "MySql": "Server=localhost;Port=3306;Database=elsa;User=admin;Password=password;",
"CockroachDb": "Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true",
"MongoDb": "mongodb://localhost:27017/elsa-workflows",
"AzureServiceBus": "",
@@ -196,6 +197,61 @@
}
]
},
+ "Kafka": {
+ "Topics": [
+ {
+ "Id": "topic-1",
+ "Name": "topic-1"
+ },
+ {
+ "Id": "topic-2",
+ "Name": "topic-2"
+ },
+ {
+ "Id": "topic-3",
+ "Name": "topic-3"
+ },
+ {
+ "Id": "topic-4",
+ "Name": "topic-4"
+ }
+ ],
+ "Producers": [
+ {
+ "Id": "producer-1",
+ "Name": "Producer 1",
+ "BootstrapServers": [
+ "localhost:9092"
+ ]
+ }
+ ],
+ "Consumers": [
+ {
+ "Id": "consumer-1",
+ "Name": "Consumer 1",
+ "BootstrapServers": [
+ "localhost:9092"
+ ],
+ "GroupId": "group-1",
+ "AutoOffsetReset": "earliest",
+ "EnableAutoCommit": "true",
+ "CorrelatingFields": [
+ "orderId",
+ "customerId"
+ ]
+ },
+ {
+ "Id": "consumer-2",
+ "Name": "Consumer 2",
+ "BootstrapServers": [
+ "localhost:9092"
+ ],
+ "GroupId": "group-1",
+ "AutoOffsetReset": "earliest",
+ "EnableAutoCommit": "true"
+ }
+ ]
+ },
"Secrets": {
"Management": {
"SweepInterval": "04:00:00"
diff --git a/src/modules/Elsa.CSharp/Features/CSharpFeature.cs b/src/modules/Elsa.CSharp/Features/CSharpFeature.cs
index 9111e5011..48c8b3f96 100644
--- a/src/modules/Elsa.CSharp/Features/CSharpFeature.cs
+++ b/src/modules/Elsa.CSharp/Features/CSharpFeature.cs
@@ -1,4 +1,5 @@
using Elsa.Common.Features;
+using Elsa.CSharp.Activities;
using Elsa.CSharp.Contracts;
using Elsa.CSharp.Options;
using Elsa.CSharp.Providers;
@@ -8,6 +9,7 @@ using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
+using Elsa.Workflows;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.CSharp.Features;
@@ -45,5 +47,8 @@ public class CSharpFeature : FeatureBase
// Activities.
Module.AddActivitiesFrom();
+
+ // UI property handlers.
+ Services.AddScoped();
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs b/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs
index 7d19e9adf..1c5ff2806 100644
--- a/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs
+++ b/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs
@@ -6,49 +6,37 @@ namespace Elsa.Expressions.Models;
///
/// Provides context to workflow expressions.
///
-public class ExpressionExecutionContext
+public class ExpressionExecutionContext(
+ IServiceProvider serviceProvider,
+ MemoryRegister memory,
+ ExpressionExecutionContext? parentContext = default,
+ IDictionary