Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup Label="TargetFrameworks">
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup Label="Files">
<None Include="..\..\..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwait.Fody" PrivateAssets="All" />
<PackageReference Include="Fody" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CS0162;CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConfigureAwait.Fody" PrivateAssets="All"/>
<PackageReference Include="Fody" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All"/>
</ItemGroup>
</Project>
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Yarp.ReverseProxy"/>
</ItemGroup>
</Project>
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,5 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
await app.RunAsync();
@@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:28212",
"sslPort": 44310
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5272",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7036;http://localhost:5272",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,35 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"Match": {
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"cluster1": {
"LoadBalancingPolicy": "RoundRobin",
"Destinations": {
"cluster1/destination1": {
"Address": "https://localhost:5001/"
},
"cluster1/destination2": {
"Address": "https://localhost:5002/"
},
"cluster1/destination3": {
"Address": "https://localhost:5003/"
}
}
}
}
}
}
@@ -0,0 +1,49 @@
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Attributes;
using Elsa.Workflows.Attributes;
namespace Elsa.Server.Web.Activities;
[Activity("Elsa", "Authorization", "Authorizes a flow based on the configured policies.")]
[FlowNode("Authorized", "Unauthorized", "Error")]
public class AuthorizeFlow : Activity<string>
{
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var httpContext = context.GetRequiredService<IHttpContextAccessor>().HttpContext;
if (httpContext == null)
throw new InvalidOperationException("HttpContext is not available. Ensure that the activity is executed within an HTTP request context.");
var bookmark = context.CreateBookmark(new AuthorizeStimulus(), OnResumeAsync);
var redirectUrl = context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmark.Id);
Result.Set(context, redirectUrl);
return ValueTask.CompletedTask;
}
private async ValueTask OnResumeAsync(ActivityExecutionContext context)
{
if (!context.TryGetWorkflowInput<string>("Answer", out var response))
{
await context.CompleteActivityWithOutcomesAsync("Unauthorized");
return;
}
switch (response)
{
case "Authorized":
await context.CompleteActivityWithOutcomesAsync("Authorized");
return;
case "Error":
await context.CompleteActivityWithOutcomesAsync("Error");
return;
default:
await context.CompleteActivityWithOutcomesAsync("Unauthorized");
break;
}
}
}
public record AuthorizeStimulus;
@@ -0,0 +1,19 @@
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.UIHints.CheckList;
namespace Elsa.Server.Web.Activities;
public class CheckedActivity : CodeActivity<string[]>
{
[Input(UIHint = InputUIHints.CheckList, UIHandler = typeof(CustomCheckListOptionsProvider))]
public Input<string[]> CheckedItems { get; set; }
protected override void Execute(ActivityExecutionContext context)
{
context.SetResult(CheckedItems.GetOrDefault(context));
}
}
@@ -0,0 +1,19 @@
using Elsa.Extensions;
using Elsa.Workflows;
namespace Elsa.Server.Web.Activities;
public class CustomDelay : Activity
{
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
{
context.DelayFor(TimeSpan.FromSeconds(5), OnDelayElapsedAsync);
return default;
}
private async ValueTask OnDelayElapsedAsync(ActivityExecutionContext context)
{
Console.WriteLine("Delay elapsed");
await context.CompleteActivityAsync();
}
}
@@ -0,0 +1,18 @@
using Elsa.Expressions.Models;
using Elsa.Workflows;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Server.Web.Activities;
public class CustomEvent : EventBase<object>
{
protected override string GetEventName(ExpressionExecutionContext context)
{
return "MyEvent";
}
protected override void OnEventReceived(ActivityExecutionContext context, object? eventData)
{
Console.WriteLine("Event received with data: " + eventData);
}
}
@@ -0,0 +1,23 @@
using Elsa.Http;
using Elsa.Http.Extensions;
using Elsa.Workflows;
namespace Elsa.Server.Web.Activities;
public class CustomHttpEndpoint : HttpEndpointBase
{
protected override HttpEndpointOptions GetOptions()
{
return new()
{
Path = "my-path",
Methods = [HttpMethods.Get],
};
}
protected override async ValueTask OnHttpRequestReceivedAsync(ActivityExecutionContext context, HttpContext httpContext)
{
httpContext.Response.StatusCode = 200;
await httpContext.Response.WriteAsync("Hello World", context.CancellationToken);
}
}
@@ -0,0 +1,18 @@
using Elsa.Expressions.Models;
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
namespace Elsa.Server.Web.Activities;
public class CustomTimer : TimerBase
{
protected override TimeSpan GetInterval(ExpressionExecutionContext context)
{
return TimeSpan.FromSeconds(5);
}
protected override void OnTimerElapsed(ActivityExecutionContext context)
{
Console.WriteLine("Timer elapsed");
}
}
@@ -0,0 +1,83 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\..\modules\Elsa.Agents.Persistence.EntityFrameworkCore.MySql\Elsa.Agents.Persistence.EntityFrameworkCore.MySql.csproj" Condition=" '$(TargetFramework)' != 'net9.0' " />
<ProjectReference Include="..\..\modules\Elsa.Caching.Distributed.MassTransit\Elsa.Caching.Distributed.MassTransit.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Caching.Distributed.ProtoActor\Elsa.Caching.Distributed.ProtoActor.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.MySql\Elsa.EntityFrameworkCore.MySql.csproj" Condition=" '$(TargetFramework)' != 'net9.0' " />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.Oracle\Elsa.EntityFrameworkCore.Oracle.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Kafka\Elsa.Kafka.csproj" />
<ProjectReference Include="..\..\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj" />
<ProjectReference Include="..\..\modules\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz.EntityFrameworkCore.MySql\Elsa.Quartz.EntityFrameworkCore.MySql.csproj" Condition=" '$(TargetFramework)' != 'net9.0' " />
<ProjectReference Include="..\..\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Retention\Elsa.Retention.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Secrets.Api\Elsa.Secrets.Api.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql\Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite\Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer\Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Secrets.Scripting\Elsa.Secrets.Scripting.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Sql.MySql\Elsa.Sql.MySql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Sql.PostgreSql\Elsa.Sql.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Sql.Sqlite\Elsa.Sql.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Sql.SqlServer\Elsa.Sql.SqlServer.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Sql\Elsa.Sql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Tenants.AspNetCore\Elsa.Tenants.AspNetCore.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Webhooks\Elsa.Webhooks.csproj" />
<ProjectReference Include="..\..\modules\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\common\Elsa.DropIns\Elsa.DropIns.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Alterations\Elsa.Alterations.csproj" />
<ProjectReference Include="..\..\modules\Elsa.CSharp\Elsa.CSharp.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.SqlServer\Elsa.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\..\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Python\Elsa.Python.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.FileStorage\Elsa.FileStorage.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Dapper.Migrations\Elsa.Dapper.Migrations.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Dapper\Elsa.Dapper.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Email\Elsa.Email.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Environments\Elsa.Environments.csproj" />
<ProjectReference Include="..\..\modules\Elsa.MassTransit\Elsa.MassTransit.csproj" />
<ProjectReference Include="..\..\modules\Elsa.MongoDb\Elsa.MongoDb.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Workflows.Runtime.ProtoActor\Elsa.Workflows.Runtime.ProtoActor.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\modules\Elsa.WorkflowProviders.BlobStorage\Elsa.WorkflowProviders.BlobStorage.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Hangfire\Elsa.Hangfire.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Labels\Elsa.Labels.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz\Elsa.Quartz.csproj" />
<ProjectReference Include="..\..\modules\Elsa.WorkflowContexts\Elsa.WorkflowContexts.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Bogus" />
<PackageReference Include="DistributedLock.Postgres" />
<PackageReference Include="DistributedLock.Redis" />
<PackageReference Include="FluentStorage.Azure.Blobs" />
<PackageReference Include="Hangfire.PostgreSql" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" />
<PackageReference Include="Proto.Cluster.Kubernetes" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Proto.Persistence.Sqlite" />
<PackageReference Include="Proto.Persistence.SqlServer" />
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\" />
</ItemGroup>
</Project>
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=enums/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -0,0 +1,9 @@
namespace Elsa.Server.Web;
public enum ApplicationRole
{
Default,
Api,
Worker,
Monitor
}
@@ -0,0 +1,12 @@
namespace Elsa.Server.Web;
/// <summary>
/// Represents the transport options for distributed caching.
/// </summary>
public enum DistributedCachingTransport
{
None,
Memory,
MassTransit,
ProtoActor
}
@@ -0,0 +1,11 @@
namespace Elsa.Server.Web;
/// <summary>
/// Represents the type of messaging broker used in MassTransit.
/// </summary>
public enum MassTransitBroker
{
Memory,
AzureServiceBus,
RabbitMq
}
@@ -0,0 +1,9 @@
namespace Elsa.Server.Web;
public enum PersistenceProvider
{
Memory,
EntityFrameworkCore,
MongoDb,
Dapper
}
@@ -0,0 +1,13 @@
namespace Elsa.Server.Web;
public enum SqlDatabaseProvider
{
SqlServer,
Sqlite,
MySql,
PostgreSql,
Citus,
YugabyteDb,
Oracle,
CockroachDb
}
@@ -0,0 +1,8 @@
namespace Elsa.Server.Web;
public enum WorkflowRuntime
{
Default,
Distributed,
ProtoActor
}
@@ -0,0 +1,22 @@
using Elsa.Common.Multitenancy;
using Elsa.EntityFrameworkCore.Extensions;
namespace Elsa.Server.Web.Extensions;
public static class MultitenantConnectionStringServiceProviderExtensions
{
public static string GetSqliteConnectionString(this IServiceProvider serviceProvider) => serviceProvider.GetConnectionString("Sqlite")!;
public static string GetSqlServerConnectionString(this IServiceProvider serviceProvider) => serviceProvider.GetConnectionString("SqlServer")!;
public static string GetMySqlConnectionString(this IServiceProvider serviceProvider) => serviceProvider.GetConnectionString("MySql")!;
public static string GetPostgreSqlConnectionString(this IServiceProvider serviceProvider) => serviceProvider.GetConnectionString("PostgreSql")!;
public static string? GetConnectionString(this IServiceProvider serviceProvider, string name)
{
var tenantAccessor = serviceProvider.GetRequiredService<ITenantAccessor>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var tenant = tenantAccessor.Tenant;
var defaultConnectionString = configuration.GetConnectionString(name)!;
var connectionString = tenant?.GetConnectionString(name) ?? defaultConnectionString;
return connectionString;
}
}
@@ -0,0 +1,30 @@
using Elsa.Http;
using Elsa.Workflows;
using JetBrains.Annotations;
namespace Elsa.Server.Web.Filters;
/// <summary>
/// Mask the value of the input if it is a HttpRequest and the input name is "Authorization".
/// </summary>
[UsedImplicitly]
public class HttpRequestAuthenticationHeaderFilter : ActivityStateFilterBase
{
protected override ActivityStateFilterResult OnExecute(ActivityStateFilterContext context)
{
var activityExecutionContext = context.ActivityExecutionContext;
var activity = activityExecutionContext.Activity;
var inputDescriptor = context.InputDescriptor;
if (activity is not SendHttpRequestBase || inputDescriptor.Name is not nameof(SendHttpRequestBase.Authorization))
return ActivityStateFilterResult.Pass();
var contextValue = context.Value.GetString();
if (contextValue == null)
return ActivityStateFilterResult.Pass();
var maskedValue = new string('*', contextValue.Length);
return Filtered(maskedValue);
}
}
+3
View File
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,6 @@
namespace Elsa.Server.Web.Messages;
public class OrderReceived
{
public string OrderId { get; set; } = null!;
}
+828
View File
@@ -0,0 +1,828 @@
using System.Text.Encodings.Web;
using Elsa.Alterations.Extensions;
using Elsa.Alterations.MassTransit.Extensions;
using Elsa.Caching.Options;
using Elsa.Common.Codecs;
using Elsa.Common.DistributedHosting.DistributedLocks;
using Elsa.Common.RecurringTasks;
using Elsa.Common.Serialization;
using Elsa.Dapper.Extensions;
using Elsa.Dapper.Services;
using Elsa.DropIns.Extensions;
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Alterations;
using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.EntityFrameworkCore.Modules.Tenants;
using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.Features.Services;
using Elsa.Identity.Multitenancy;
using Elsa.Kafka;
using Elsa.Kafka.Factories;
using Elsa.MassTransit.Extensions;
using Elsa.MongoDb.Extensions;
using Elsa.MongoDb.Modules.Alterations;
using Elsa.MongoDb.Modules.Identity;
using Elsa.MongoDb.Modules.Management;
using Elsa.MongoDb.Modules.Runtime;
using Elsa.MongoDb.Modules.Tenants;
using Elsa.OpenTelemetry.Middleware;
using Elsa.Retention.Extensions;
using Elsa.Retention.Models;
using Elsa.Secrets.Extensions;
using Elsa.Secrets.Management.Tasks;
using Elsa.Secrets.Persistence;
using Elsa.Server.Web;
using Elsa.Server.Web.Extensions;
using Elsa.Server.Web.Filters;
using Elsa.Server.Web.Messages;
using Elsa.Sql.Extensions;
using Elsa.Sql.MySql;
using Elsa.Sql.PostgreSql;
using Elsa.Sql.Sqlite;
using Elsa.Sql.SqlServer;
using Elsa.Tenants.AspNetCore;
using Elsa.Tenants.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Api;
using Elsa.Workflows.CommitStates.Strategies;
using Elsa.Workflows.IncidentStrategies;
using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Stores;
using Elsa.Workflows.Options;
using Elsa.Workflows.Runtime.Distributed.Extensions;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.Stores;
using Elsa.Workflows.Runtime.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.PostgreSql;
using Hangfire.PostgreSql.Factories;
using Hangfire.SqlServer;
using Hangfire.Storage.SQLite;
using JetBrains.Annotations;
using Medallion.Threading.FileSystem;
using Medallion.Threading.Postgres;
using Medallion.Threading.Redis;
using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Proto.Cluster.Kubernetes;
using Proto.Persistence.Sqlite;
using Proto.Persistence.SqlServer;
using Proto.Remote;
using Proto.Remote.GrpcNet;
using StackExchange.Redis;
// ReSharper disable RedundantAssignment
const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore;
const bool useDbContextPooling = false;
const bool useHangfire = false;
const bool useQuartz = true;
const bool useMassTransit = false;
const bool useZipCompression = false;
const bool runEFCoreMigrations = true;
const bool useMemoryStores = false;
const bool useCaching = true;
const bool useKafka = false;
const bool useReadOnlyMode = false;
const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests.
const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.Memory;
const MassTransitBroker massTransitBroker = MassTransitBroker.Memory;
const bool useMultitenancy = false;
const bool useTenantsFromConfiguration = true;
const bool useSecrets = false;
const bool disableVariableWrappers = false;
const bool disableVariableCopying = false;
const bool useManualOtelInstrumentation = false;
ObjectConverter.StrictMode = true;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var identitySection = configuration.GetSection("Identity");
var identityTokenSection = identitySection.GetSection("Tokens");
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var sqlServerConnectionString = configuration.GetConnectionString("SqlServer")!;
var postgresConnectionString = configuration.GetConnectionString("PostgreSql")!;
var citusConnectionString = configuration.GetConnectionString("Citus")!;
var yugabyteDbConnectionString = configuration.GetConnectionString("YugabyteDb")!;
var oracleConnectionString = configuration.GetConnectionString("Oracle")!;
var mySqlConnectionString = configuration.GetConnectionString("MySql")!;
var cockroachDbConnectionString = configuration.GetConnectionString("CockroachDb")!;
var mongoDbConnectionString = configuration.GetConnectionString("MongoDb")!;
var azureServiceBusConnectionString = configuration.GetConnectionString("AzureServiceBus")!;
var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!;
var redisConnectionString = configuration.GetConnectionString("Redis")!;
var distributedLockProviderName = configuration.GetSection("Runtime:DistributedLocking")["Provider"];
var appRole = Enum.Parse<ApplicationRole>(configuration["AppRole"] ?? "Default");
var sqlDatabaseProvider = Enum.Parse<SqlDatabaseProvider>(configuration["DatabaseProvider"] ?? "Sqlite");
// Optionally create type aliases for easier configuration.
TypeAliasRegistry.RegisterAlias("OrderReceivedProducerFactory", typeof(GenericProducerFactory<string, OrderReceived>));
TypeAliasRegistry.RegisterAlias("OrderReceivedConsumerFactory", typeof(GenericConsumerFactory<string, OrderReceived>));
if (useManualOtelInstrumentation)
{
services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("elsa-workflows", serviceVersion: "3.5.0").AddTelemetrySdk())
.WithTracing(tracing =>
{
tracing
.AddSource("*")
.SetSampler(new AlwaysOnSampler())
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddConsoleExporter()
.AddOtlpExporter()
;
})
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddConsoleExporter()
.AddOtlpExporter()
;
});
// Enable OpenTelemetry Logging (optional)
builder.Logging.AddOpenTelemetry(options =>
{
options.IncludeFormattedMessage = true;
options.IncludeScopes = true;
options.ParseStateValues = true;
});
}
// Add Elsa services.
services.AddScoped<IPropertyUIHandler, CustomCheckListOptionsProvider>();
services
.AddElsa(elsa =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
elsa.UseMongoDb(mongoDbConnectionString);
if (persistenceProvider == PersistenceProvider.Dapper)
elsa.UseDapper(dapper =>
{
dapper.UseMigrations(feature =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
feature.UseSqlServer();
else
feature.UseSqlite();
});
dapper.DbConnectionProvider = sp =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
return new SqlServerDbConnectionProvider(sqlServerConnectionString);
else
return new SqliteDbConnectionProvider(sqliteConnectionString);
};
});
if (useHangfire)
{
JobStorage jobStorage;
if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
{
jobStorage = new PostgreSqlStorage(new NpgsqlConnectionFactory(postgresConnectionString, new()
{
QueuePollInterval = TimeSpan.FromSeconds(1)
}));
}
else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite)
{
jobStorage = new SQLiteStorage(sqliteConnectionString, new()
{
QueuePollInterval = TimeSpan.FromSeconds(1)
});
}
else if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
{
jobStorage = new SqlServerStorage(sqlServerConnectionString, new()
{
QueuePollInterval = TimeSpan.FromSeconds(1)
});
}
else
{
jobStorage = new MemoryStorage();
}
elsa.UseHangfire(hangfire => hangfire.UseJobStorage(jobStorage));
}
elsa
.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>()
.UseFluentStorageProvider()
.UseFileStorage()
.UseIdentity(identity =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
identity.UseMongoDb();
else if (persistenceProvider == PersistenceProvider.Dapper)
identity.UseDapper();
else
identity.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
ef.UseSqlServer(sqlServerConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Citus)
ef.UsePostgreSql(citusConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.YugabyteDb)
ef.UsePostgreSql(yugabyteDbConnectionString);
#if !NET9_0
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
#endif
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle)
ef.UseOracle(oracleConnectionString, new()
{
SchemaName = "ELSA"
});
else
ef.UseSqlite(sp => sp.GetSqliteConnectionString());
ef.RunMigrations = runEFCoreMigrations;
});
identity.TokenOptions = options => identityTokenSection.Bind(options);
identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseWorkflows(workflows =>
{
workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing());
workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing());
workflows.UseCommitStrategies(strategies =>
{
strategies.AddStandardStrategies();
strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10)));
});
})
.UseWorkflowManagement(management =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
management.UseMongoDb();
else if (persistenceProvider == PersistenceProvider.Dapper)
management.UseDapper();
else
management.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
ef.UseSqlServer(sqlServerConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Citus)
ef.UsePostgreSql(citusConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.YugabyteDb)
ef.UsePostgreSql(yugabyteDbConnectionString);
#if !NET9_0
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
#endif
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle)
ef.UseOracle(oracleConnectionString, new()
{
SchemaName = "ELSA"
});
else
ef.UseSqlite(sp => sp.GetSqliteConnectionString());
ef.RunMigrations = runEFCoreMigrations;
});
if (useZipCompression)
management.SetCompressionAlgorithm(nameof(Zstd));
if (useMemoryStores)
management.UseWorkflowInstances(feature => feature.WorkflowInstanceStore = sp => sp.GetRequiredService<MemoryWorkflowInstanceStore>());
if (useMassTransit)
management.UseMassTransitDispatcher();
if (useCaching)
management.UseCache();
management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit);
management.UseReadOnlyMode(useReadOnlyMode);
management.AddVariableTypeAndAlias<OrderReceived>("Application");
})
.UseWorkflowRuntime(runtime =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
runtime.UseMongoDb();
else if (persistenceProvider == PersistenceProvider.Dapper)
runtime.UseDapper();
else
runtime.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
{
//ef.UseSqlServer(sqlServerConnectionString, new ElsaDbContextOptions);
var migrationsAssembly = typeof(Elsa.EntityFrameworkCore.SqlServer.IdentityDbContextFactory).Assembly;
var connectionString = sqlServerConnectionString;
ef.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlServer(migrationsAssembly, connectionString, null, configure => configure.CommandTimeout(60000));
}
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Citus)
ef.UsePostgreSql(citusConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.YugabyteDb)
ef.UsePostgreSql(yugabyteDbConnectionString);
#if !NET9_0
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
#endif
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle)
ef.UseOracle(oracleConnectionString, new()
{
SchemaName = "ELSA"
});
else
ef.UseSqlite(sp => sp.GetSqliteConnectionString());
ef.RunMigrations = runEFCoreMigrations;
});
if (workflowRuntime == WorkflowRuntime.Distributed)
{
runtime.UseDistributedRuntime();
}
if (workflowRuntime == WorkflowRuntime.ProtoActor)
{
runtime.UseProtoActor();
}
if (useMassTransit)
runtime.UseMassTransitDispatcher();
runtime.WorkflowDispatcherOptions = options => configuration.GetSection("Runtime:WorkflowDispatcher").Bind(options);
if (useMemoryStores)
{
runtime.ActivityExecutionLogStore = sp => sp.GetRequiredService<MemoryActivityExecutionStore>();
runtime.WorkflowExecutionLogStore = sp => sp.GetRequiredService<MemoryWorkflowExecutionLogStore>();
}
if (useCaching)
runtime.UseCache();
runtime.DistributedLockingOptions = options => configuration.GetSection("Runtime:DistributedLocking").Bind(options);
runtime.DistributedLockProvider = _ =>
{
switch (distributedLockProviderName)
{
case "Postgres":
return new PostgresDistributedSynchronizationProvider(postgresConnectionString, options =>
{
options.KeepaliveCadence(TimeSpan.FromMinutes(5));
options.UseMultiplexing();
});
case "Redis":
{
var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConnectionString);
var database = connectionMultiplexer.GetDatabase();
return new RedisDistributedSynchronizationProvider(database);
}
case "File":
return new FileDistributedSynchronizationProvider(new(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks")));
case "Noop":
default:
return new NoopDistributedSynchronizationProvider();
}
};
})
.UseEnvironments(environments => environments.EnvironmentsOptions = options => configuration.GetSection("Environments").Bind(options))
.UseScheduling(scheduling =>
{
if (useHangfire)
scheduling.UseHangfireScheduler();
if (useQuartz)
scheduling.UseQuartzScheduler();
})
.UseWorkflowsApi(api =>
{
api.AddFastEndpointsAssembly<Program>();
})
.UseCSharp(options =>
{
options.DisableWrappers = disableVariableWrappers;
options.AppendScript("string Greet(string name) => $\"Hello {name}!\";");
options.AppendScript("string SayHelloWorld() => Greet(\"World\");");
})
.UseJavaScript(options =>
{
options.AllowClrAccess = true;
options.DisableWrappers = disableVariableWrappers;
options.DisableVariableCopying = disableVariableCopying;
options.RegisterType<OrderReceived>();
options.ConfigureEngine(engine =>
{
engine.Execute("function greet(name) { return `Hello ${name}!`; }");
engine.Execute("function sayHelloWorld() { return greet('World'); }");
});
})
.UsePython(python =>
{
python.PythonOptions += options =>
{
// Make sure to configure the path to the python DLL. E.g. /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11
// alternatively, you can set the PYTHONNET_PYDLL environment variable.
configuration.GetSection("Scripting:Python").Bind(options);
options.AddScript(sb =>
{
sb.AppendLine("def greet():");
sb.AppendLine(" return \"Hello, welcome to Python!\"");
});
};
})
.UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default)
.UseHttp(http =>
{
http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options);
if (useCaching)
http.UseCache();
})
.UseSql(options =>
{
options.Clients = client =>
{
client.Register<MySqlClient>("MySql");
client.Register<PostgreSqlClient>("PostgreSql");
client.Register<SqliteClient>("Sqlite");
client.Register<SqlServerClient>("Sql Server");
};
})
.UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options))
.UseAlterations(alterations =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
{
alterations.UseMongoDb();
}
else if (persistenceProvider == PersistenceProvider.Dapper)
{
// TODO: alterations.UseDapper();
}
else
{
alterations.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
ef.UseSqlServer(sqlServerConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Citus)
ef.UsePostgreSql(citusConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.YugabyteDb)
ef.UsePostgreSql(yugabyteDbConnectionString);
#if !NET9_0
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
#endif
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle)
ef.UseOracle(oracleConnectionString, new()
{
SchemaName = "ELSA"
});
else
ef.UseSqlite(sp => sp.GetSqliteConnectionString());
ef.RunMigrations = runEFCoreMigrations;
});
}
if (useMassTransit)
{
alterations.UseMassTransitDispatcher();
}
})
.UseOpenTelemetry(otel => otel.UseNewRootActivityForRemoteParent = true)
.UseWorkflowContexts();
if (useQuartz)
{
elsa.UseQuartz(quartz =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite)
quartz.UseSqlite(sqliteConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
quartz.UsePostgreSql(postgresConnectionString);
});
}
if (useSignalR)
{
elsa.UseRealTimeWorkflows();
}
if (useMassTransit)
{
elsa.UseMassTransit(massTransit =>
{
massTransit.DisableConsumers = appRole == ApplicationRole.Api;
massTransit.AddMessageType<OrderReceived>();
if (massTransitBroker == MassTransitBroker.AzureServiceBus)
{
massTransit.UseAzureServiceBus(azureServiceBusConnectionString, serviceBusFeature => serviceBusFeature.ConfigureTransportBus = (context, bus) =>
{
bus.PrefetchCount = 50;
bus.LockDuration = TimeSpan.FromMinutes(5);
bus.MaxConcurrentCalls = 32;
bus.MaxDeliveryCount = 8;
// etc.
});
}
if (massTransitBroker == MassTransitBroker.RabbitMq)
{
massTransit.UseRabbitMq(rabbitMqConnectionString, rabbit => rabbit.ConfigureTransportBus = (context, bus) =>
{
bus.PrefetchCount = 50;
bus.Durable = true;
bus.AutoDelete = false;
bus.ConcurrentMessageLimit = 32;
// etc.
});
}
});
}
if (distributedCachingTransport != DistributedCachingTransport.None)
{
elsa.UseDistributedCache(distributedCaching =>
{
if (distributedCachingTransport == DistributedCachingTransport.MassTransit) distributedCaching.UseMassTransit();
if (distributedCachingTransport == DistributedCachingTransport.ProtoActor) distributedCaching.UseProtoActor();
});
}
if (distributedCachingTransport == DistributedCachingTransport.ProtoActor || workflowRuntime == WorkflowRuntime.ProtoActor)
{
elsa.UseProtoActor(proto =>
{
proto
.EnableMetrics()
.EnableTracing();
proto.PersistenceProvider = _ =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
return new SqlServerProvider(sqlServerConnectionString, true, "", "proto_actor");
return new SqliteProvider(new(sqliteConnectionString));
};
if (configuration["KUBERNETES_SERVICE_HOST"] != null)
{
var kubernetesConfig = new KubernetesProviderConfig();
var clusterProvider = new KubernetesProvider(kubernetesConfig);
var remoteConfig = GrpcNetRemoteConfig
.BindToAllInterfaces(advertisedHost: configuration["ProtoActor:AdvertisedHost"]) // Environment variable to be provided by Kubernetes using pod.status.podIP.
.WithLogLevelForDeserializationErrors(LogLevel.Critical)
.WithRemoteDiagnostics(true);
proto.CreateClusterProvider = _ => clusterProvider;
proto.ConfigureRemoteConfig = _ => remoteConfig;
}
});
}
if (useKafka)
{
elsa.UseKafka(kafka =>
{
kafka.ConfigureOptions(options => configuration.GetSection("Kafka").Bind(options));
});
}
if (useSecrets)
{
elsa
.UseSecrets()
.UseSecretsManagement(management =>
{
management.ConfigureOptions(options => configuration.GetSection("Secrets:Management").Bind(options));
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
management.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
ef.UseSqlServer(sqlServerConnectionString);
});
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
management.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
ef.UsePostgreSql(postgresConnectionString);
});
else
management.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
ef.UseSqlite(sp => sp.GetSqliteConnectionString());
});
})
.UseSecretsApi()
.UseSecretsScripting()
;
}
elsa.UseRetention(r =>
{
r.SweepInterval = TimeSpan.FromHours(5);
r.AddDeletePolicy("Delete all finished workflows", sp =>
{
var filter = new RetentionWorkflowInstanceFilter
{
WorkflowStatus = WorkflowStatus.Finished
};
return filter;
});
});
if (useMultitenancy)
{
elsa.UseTenants(tenants =>
{
tenants.ConfigureMultitenancy(options =>
{
options.TenantResolverPipelineBuilder
.Append<HostTenantResolver>()
.Append<RoutePrefixTenantResolver>()
.Append<HeaderTenantResolver>()
.Append<ClaimsTenantResolver>();
});
if (useTenantsFromConfiguration)
{
tenants.UseConfigurationBasedTenantsProvider(options => configuration.GetSection("Multitenancy").Bind(options));
}
else
{
tenants.UseStoreBasedTenantsProvider();
tenants.UseTenantManagement(management =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
management.UseMongoDb();
if (persistenceProvider == PersistenceProvider.Dapper)
throw new NotSupportedException("Dapper is not supported for tenant management.");
if (persistenceProvider == PersistenceProvider.EntityFrameworkCore)
{
management.UseEntityFrameworkCore(ef =>
{
ef.UseContextPooling = useDbContextPooling;
if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) ef.UseSqlite(sqliteConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.Citus) ef.UsePostgreSql(citusConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.YugabyteDb) ef.UsePostgreSql(yugabyteDbConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle)
ef.UseOracle(oracleConnectionString, new()
{
SchemaName = "ELSA"
});
#if !NET9_0
if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
#endif
if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString);
});
}
});
tenants.UseTenantManagementEndpoints();
}
});
elsa.UseTenantHttpRouting(tenantHttpRouting =>
{
// Override the tenant header name with a custom one.
tenantHttpRouting.WithTenantHeader("X-Tenant-ID");
});
}
elsa.UseWebhooks(webhooks => webhooks.ConfigureSinks += options => builder.Configuration.GetSection("Webhooks").Bind(options));
elsa.InstallDropIns(options => options.DropInRootDirectory = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "DropIns"));
elsa.AddSwagger();
elsa.AddFastEndpointsAssembly<Program>();
ConfigureForTest?.Invoke(elsa);
});
// Obfuscate HTTP request headers.
services.AddActivityStateFilter<HttpRequestAuthenticationHeaderFilter>();
// Optionally configure recurring tasks using alternative schedules.
services.Configure<RecurringTaskOptions>(options =>
{
options.Schedule.ConfigureTask<TriggerBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
options.Schedule.ConfigureTask<PurgeBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
options.Schedule.ConfigureTask<UpdateExpiredSecretsRecurringTask>(TimeSpan.FromHours(4));
options.Schedule.ConfigureTask<RestartInterruptedWorkflowsTask>(TimeSpan.FromSeconds(15));
});
services.Configure<RuntimeOptions>(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); });
services.Configure<BookmarkQueuePurgeOptions>(options => options.Ttl = TimeSpan.FromSeconds(10));
services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));
services.Configure<IncidentOptions>(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy));
services.AddHealthChecks();
services.AddControllers();
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*")));
// Build the web application.
var app = builder.Build();
// Configure the pipeline.
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
// CORS.
app.UseCors();
// Health checks.
app.MapHealthChecks("/");
// Routing used for SignalR.
app.UseRouting();
// Security.
app.UseAuthentication();
app.UseAuthorization();
// Multitenancy.
if (useMultitenancy)
app.UseTenants();
// Elsa API endpoints for designer.
var routePrefix = app.Services.GetRequiredService<IOptions<ApiEndpointOptions>>().Value.RoutePrefix;
app.UseWorkflowsApi(routePrefix);
// Captures unhandled exceptions and returns a JSON response.
app.UseJsonSerializationErrorHandler();
// Elsa HTTP Endpoint activities.
app.UseWorkflows();
app.MapControllers();
// Swagger API documentation.
if (app.Environment.IsDevelopment())
{
app.UseSwaggerUI();
}
// SignalR.
if (useSignalR)
{
app.UseWorkflowsSignalRHubs();
}
// Run.
await app.RunAsync();
/// <summary>
/// The main entry point for the application made public for end to end testing.
/// </summary>
[UsedImplicitly]
public partial class Program
{
/// <summary>
/// Set by the test runner to configure the module for testing.
/// </summary>
public static Action<IModule>? ConfigureForTest { get; set; }
}
@@ -0,0 +1,28 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:16508",
"sslPort": 44323
}
},
"profiles": {
"Elsa.WorkflowServer.Web": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5072",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
# Server
This project represents an Elsa application that hosts workflows and exposes API endpoints to manage & execute workflows.
## Secrets
The following are the secrets stored in hashed form in appsettings.json:
**API key**: `48587230567A646D394B435A6277734A-4802fa49-e91e-45e8-b00f-b5492377e20b`
**Admin user**: `admin`
**Admin password**: `password`
## OpenTelemetry (MacOS)
COR_ENABLE_PROFILING=1
COR_PROFILER={918728DD-259F-4A6A-AC2B-B85E1B658318}
CORECLR_PROFILER_PATH=$INSTALL_DIR/osx-x64/OpenTelemetry.AutoInstrumentation.Native.dylib
DOTNET_ADDITIONAL_DEPS=$INSTALL_DIR/AdditionalDeps
DOTNET_EnableDiagnostics=1
DOTNET_SHARED_STORE=$INSTALL_DIR/store
DOTNET_STARTUP_HOOKS=OpenTelemetry.AutoInstrumentation.StartupHook
OTEL_DOTNET_AUTO_HOME=$INSTALL_DIR
OTEL_DOTNET_AUTO_LOGS_CONSOLE_EXPORTER_ENABLED=true
OTEL_DOTNET_AUTO_METRICS_CONSOLE_EXPORTER_ENABLED=true
OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES=Proto.Actor,Elsa.Workflows
OTEL_DOTNET_AUTO_TRACES_CONSOLE_EXPORTER_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_RESOURCE_ATTRIBUTES=service.name=Elsa Server,service.version=3.3.0,service.instance.id=instance-123,deployment.environment=development
@@ -0,0 +1,22 @@
using System.Reflection;
using Elsa.Workflows.UIHints.CheckList;
namespace Elsa.Server.Web;
/// <summary>
/// Provides static drop-down options for a given property.
/// </summary>
public class CustomCheckListOptionsProvider : CheckListOptionsProviderBase
{
/// <inheritdoc />
protected override ValueTask<ICollection<CheckListItem>> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken)
{
var items = new[]
{
"Apple", "Banana", "Cherry"
};
var selectListItems = items.Select(x => new CheckListItem(x, x)).ToList();
return new(selectListItems);
}
}
+290
View File
@@ -0,0 +1,290 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"HostBuilder": {
"reloadConfigOnChange": true
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;",
"SqlServer": "Server=localhost,1433;Initial Catalog=Elsa;User=sa;Password=!Elsa2025@;Encrypt=false;PersistSecurityInfo=false",
"MySql": "Server=localhost;Database=elsa;Uid=admin;Pwd=password;",
"PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60",
"Citus": "Server=localhost;Username=citus;Database=citus;Port=9700;Password=citus;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60",
"YugabyteDb": "Host=localhost;Port=5433;Database=elsa;Username=yugabyte;Password=yugabyte;Pooling=true",
"Oracle": "Data Source=(DESCRIPTION = (ADDRESS_LIST = (FAILOVER =ON) (LOAD_BALANCE = OFF) (ADDRESS = (PROTOCOL =TCP)(HOST=localhost)(PORT=1521))) (CONNECT_DATA = (SID= FREE) ));User Id=ELSA;Password=elsa;",
"CockroachDb": "Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true",
"MongoDb": "mongodb://localhost:27017/elsa-workflows",
"AzureServiceBus": "",
"RabbitMq": "amqp://guest:guest@localhost:5672",
"Redis": "localhost:6379,abortConnect=false"
},
"DatabaseProvider": "Sqlite",
"Multitenancy": {
"Tenants": [
{
"Id": null,
"Name": "Default",
"Configuration": {
"Http": {
"Prefix": "",
"Host": "localhost:5001"
},
"ConnectionStrings": {
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;"
}
}
},
{
"Id": "tenant-1",
"Name": "Tenant 1",
"Configuration": {
"Http": {
"Prefix": "tenant-1",
"Host": "localhost:5001"
},
"ConnectionStrings": {
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;"
}
}
}
]
},
"Smtp": {
"Host": "localhost",
"Port": 2525,
"DefaultSender": "noreply@crmservices.com"
},
"Http": {
"BaseUrl": "https://localhost:5001",
"BasePath": "/workflows",
"ApiRoutePrefix": "elsa/api",
"AvailableContentTypes": [
"application/json",
"application/xml",
"text/plain",
"text/html"
]
},
"Identity": {
"Tokens": {
"SigningKey": "sufficiently-large-secret-signing-key",
"AccessTokenLifetime": "1:00:00:00",
"RefreshTokenLifetime": "7:00:00:00"
},
"Roles": [
{
"Id": "admin",
"Name": "Administrator",
"Permissions": [
"*"
],
"TenantId": "default"
}
],
"Users": [
{
"Id": "a2323f46-42db-4e15-af8b-94238717d817",
"Name": "admin",
"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=",
"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=",
"Roles": [
"admin"
],
"TenantId": ""
},
{
"Id": "b0cd0e506e713a9d",
"Name": "alice",
"Roles": [
"admin"
],
"HashedPassword": "8B0fFK/f/kk9GkVtzXfRJ2Y6cNyYVvLTfKouWcAcuPg=",
"HashedPasswordSalt": "xlNWvEng8fRvo0McyJopbRJ2MJ9NIYV/4IY5dOZeiiw=",
"TenantId": "tenant-2"
},
{
"Id": "789d5056cb3bb8e9",
"Name": "bob",
"Roles": [
"admin"
],
"HashedPassword": "GMHHvUw7RfIIBq+GAWZ/AlHR50TbgONYfGQ92Xge0EI=",
"HashedPasswordSalt": "V4uHKg1PFBc1bsngCQoptibOu14faj9obSn5B23h1wE=",
"TenantId": "tenant-1"
}
],
"Applications": [
{
"Id": "d57030226341448daff5a2935aba2d3f",
"Name": "Postman",
"Roles": [
"admin"
],
"ClientId": "HXr0Vzdm9KCZbwsJ",
"ClientSecret": "a<~QGGHTEA%u4;CU&'Wga5ED:_&Gd1C)",
"HashedApiKey": "Z5ClHs3mbzx8Pnw3+PxbMq8A/Y+VKMCCDTGYtax8JFM=",
"HashedApiKeySalt": "kBisa1X8FwBfN2zmyGMFRgIVVBleghhQAJ4WGyTkaD0=",
"HashedClientSecret": "jEv58d0SVbGQ3nBZM0lkzHghG4Y+lMKW80wipz+9vHk=",
"HashedClientSecretSalt": "xRKy14Ok1/tU3kLf/8V1fcbLIegy9vcM90Peu2tzohU=",
"TenantId": "default"
}
]
},
"Environments": {
"Environments": [
{
"Name": "Local",
"ServerUrl": "https://localhost:5001/elsa/api"
},
{
"Name": "Staging",
"ServerUrl": "https://staging.acme.com/elsa/api"
},
{
"Name": "Production",
"ServerUrl": "https://production.acme.com/elsa/api"
}
]
},
"AppRole": "Default",
"Runtime": {
"DistributedLocking": {
"Provider": "File",
"LockAcquisitionTimeout": "00:00:10"
},
"DistributedLockProvider": "File",
"WorkflowDispatcher": {
"Channels": [
{
"Name": "Low"
},
{
"Name": "Medium"
},
{
"Name": "High"
}
]
}
},
"Scripting": {
"Python": {
"PythonDllPath": "",
"Scripts": [
"def greet(name): return f'Hello {name}!'",
"def say_hello_world(): return greet('World')"
]
}
},
"AzureServiceBus": {
"Queues": [
{
"Name": "queue1"
},
{
"Name": "queue2"
}
],
"Topics": [
{
"Name": "topic1",
"Subscriptions": [
{
"Name": "subscription1"
},
{
"Name": "subscription2"
}
]
}
]
},
"Kafka": {
"Topics": [
{
"Id": "topic-1",
"Name": "topic-1"
},
{
"Id": "topic-2",
"Name": "topic-2"
}
],
"SchemaRegistries": [
{
"Id": "schema-registry-1",
"Name": "Schema Registry 1",
"Config": {
"Url": "http://localhost:8081"
}
}
],
"Producers": [
{
"Id": "producer-1",
"Name": "Producer 1",
"FactoryType": "Elsa.Kafka.Factories.GenericProducerFactory`2[[System.String, System.Private.CoreLib], [Elsa.Server.Web.Messages.OrderReceived, Elsa.Server.Web]], Elsa.Kafka",
"Config": {
"BootstrapServers": "localhost:9092"
},
"SchemaRegistryId": "schema-registry-1"
}
],
"Consumers": [
{
"Id": "consumer-1",
"Name": "Consumer 1",
"FactoryType": "Elsa.Kafka.Factories.GenericConsumerFactory`2[[System.String, System.Private.CoreLib], [Elsa.Server.Web.Messages.OrderReceived, Elsa.Server.Web]], Elsa.Kafka",
"Config": {
"BootstrapServers": "localhost:9092",
"GroupId": "group-1",
"AutoOffsetReset": "earliest",
"EnableAutoCommit": "true"
},
"SchemaRegistryId": "schema-registry-1"
}
]
},
"Secrets": {
"Management": {
"SweepInterval": "04:00:00"
}
},
"Webhooks": {
"Sinks": [
{
"Id": "1",
"Name": "Run Task",
"Filters": [
{
"EventType": "Elsa.RunTask"
}
],
"Url": "https://localhost:6001/webhooks/run-task"
}
]
},
"Resilience": {
"Strategies": [
{
"$type": "HttpResilienceStrategy",
"Id": "1",
"DisplayName": "Retry 3 times, exponential",
"MaxRetryAttempts": 3,
"BackoffType": "Exponential"
},
{
"$type": "HttpResilienceStrategy",
"Id": "2",
"DisplayName": "Retry 10 times, linear",
"MaxRetryAttempts": 10,
"BackoffType": "Linear"
}
]
}
}
@@ -0,0 +1,26 @@
using Elsa.Extensions;
using Elsa.Workflows;
namespace Elsa.ServerAndStudio.Web.Activities;
public class CustomDelay : Activity
{
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
{
context.DelayFor(TimeSpan.FromSeconds(5), FailAsync);
context.DelayFor(TimeSpan.FromSeconds(500), OnDelayElapsedAsync);
return default;
}
private ValueTask FailAsync(ActivityExecutionContext context)
{
Console.WriteLine("Failing!");
throw new("Failing!");
}
private async ValueTask OnDelayElapsedAsync(ActivityExecutionContext context)
{
Console.WriteLine("Delay elapsed");
await context.CompleteActivityAsync();
}
}
@@ -0,0 +1,18 @@
using Elsa.Expressions.Models;
using Elsa.Workflows;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.ServerAndStudio.Web.Activities;
public class CustomEvent : EventBase<object>
{
protected override string GetEventName(ExpressionExecutionContext context)
{
return "MyEvent";
}
protected override void OnEventReceived(ActivityExecutionContext context, object? eventData)
{
Console.WriteLine("Event received with data: " + eventData);
}
}
@@ -0,0 +1,22 @@
using Elsa.Http;
using Elsa.Workflows;
namespace Elsa.ServerAndStudio.Web.Activities;
public class CustomHttpEndpoint : HttpEndpointBase
{
protected override HttpEndpointOptions GetOptions()
{
return new()
{
Path = "my-path",
Methods = [HttpMethods.Get],
};
}
protected override async ValueTask OnHttpRequestReceivedAsync(ActivityExecutionContext context, HttpContext httpContext)
{
httpContext.Response.StatusCode = 200;
await httpContext.Response.WriteAsync("Hello World", context.CancellationToken);
}
}
@@ -0,0 +1,18 @@
using Elsa.Expressions.Models;
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
namespace Elsa.ServerAndStudio.Web.Activities;
public class CustomTimer : TimerBase
{
protected override TimeSpan GetInterval(ExpressionExecutionContext context)
{
return TimeSpan.FromSeconds(5);
}
protected override void OnTimerElapsed(ActivityExecutionContext context)
{
Console.WriteLine("Timer elapsed");
}
}
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Elsa.Agents.Persistence.EntityFrameworkCore.MySql\Elsa.Agents.Persistence.EntityFrameworkCore.MySql.csproj" Condition=" '$(TargetFramework)' != 'net9.0' " />
<ProjectReference Include="..\..\modules\Elsa.Connections.Api\Elsa.Connections.Api.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Connections.Core\Elsa.Connections.Core.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Connections.Persistence.EntityFrameworkCore\Elsa.Connections.Persistence.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.SqlServer\Elsa.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\..\modules\Elsa.IO.Http\Elsa.IO.Http.csproj" />
<ProjectReference Include="..\..\modules\Elsa.JavaScript.Libraries\Elsa.JavaScript.Libraries.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz.EntityFrameworkCore.SqlServer\Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql\Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa\Elsa.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Caching.Distributed.MassTransit\Elsa.Caching.Distributed.MassTransit.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Caching.Distributed\Elsa.Caching.Distributed.csproj" />
<ProjectReference Include="..\..\modules\Elsa.CSharp\Elsa.CSharp.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Email\Elsa.Email.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.MySql\Elsa.EntityFrameworkCore.MySql.csproj" Condition=" '$(TargetFramework)' != 'net9.0' " />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Hangfire\Elsa.Hangfire.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Http\Elsa.Http.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.IO.Compression\Elsa.IO.Compression.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.MassTransit\Elsa.MassTransit.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Workflows.Runtime.ProtoActor\Elsa.Workflows.Runtime.ProtoActor.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Python\Elsa.Python.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Quartz\Elsa.Quartz.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Webhooks\Elsa.Webhooks.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj"/>
<ProjectReference Include="..\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server"/>
<PackageReference Include="Proto.Persistence.Sqlite"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Elsa.Studio"/>
<PackageReference Include="Elsa.Studio.Login.BlazorWasm"/>
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
namespace Elsa.ServerAndStudio.Web.Enums;
/// <summary>
/// Represents the transport options for distributed caching.
/// </summary>
public enum DistributedCachingTransport
{
None,
Memory,
MassTransit
}
@@ -0,0 +1,11 @@
namespace Elsa.ServerAndStudio.Web.Enums;
/// <summary>
/// Represents the type of messaging broker used in MassTransit.
/// </summary>
public enum MassTransitBroker
{
Memory,
AzureServiceBus,
RabbitMq
}
@@ -0,0 +1,10 @@
namespace Elsa.ServerAndStudio.Web.Enums;
public enum SqlDatabaseProvider
{
SqlServer,
Sqlite,
MySql,
PostgreSql,
CockroachDb
}
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Cors.Infrastructure;
namespace Elsa.ServerAndStudio.Web.Extensions;
public static class CorsOptionsExtensions
{
private static bool AllowAny(IList<string> values) => values.Count == 0 || values[0] == "*";
public static void Configure(this CorsOptions options, IConfigurationSection section)
{
var corsPolicy = section.Get<CorsPolicy>() ?? new CorsPolicy();
options.AddDefaultPolicy(policy => {
var headers = corsPolicy.Headers;
var origins = corsPolicy.Origins;
var methods = corsPolicy.Methods;
if (AllowAny(headers))
policy.AllowAnyHeader();
else
policy.WithHeaders(headers.ToArray());
if (AllowAny(origins))
policy.AllowAnyOrigin();
else
policy.WithOrigins(origins.ToArray());
if (AllowAny(methods))
policy.AllowAnyMethod();
else
policy.WithMethods(methods.ToArray());
});
}
}
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,61 @@
@page "/"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject IConfiguration Configuration;
@{
var baseUrl = Configuration["Hosting:BaseUrl"] ?? Configuration["Http:BaseUrl"]!; // HttpBaseUrl is for backward compatibility.
var apiUrl = $"{baseUrl.TrimEnd('/')}/{(Configuration["Hosting:ApiPrefix"] ?? "elsa/api").TrimStart('/')}";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<title>Elsa Studio 3.5</title>
<base href="/"/>
<link rel="apple-touch-icon" sizes="180x180" href="@baseUrl/_content/Elsa.Studio.Shell/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="@baseUrl/_content/Elsa.Studio.Shell/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="@baseUrl/_content/Elsa.Studio.Shell/favicon-16x16.png">
<link rel="manifest" href="@baseUrl/_content/Elsa.Studio.Shell/site.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Grandstander:wght@100&display=swap" rel="stylesheet">
<link href="@baseUrl/_content/MudBlazor/MudBlazor.min.css" rel="stylesheet"/>
<link href="@baseUrl/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet"/>
<link href="@baseUrl/_content/Radzen.Blazor/css/material-base.css" rel="stylesheet">
<link href="@baseUrl/_content/Elsa.Studio.Shell/css/shell.css" rel="stylesheet">
<link href="@baseUrl/_content/Elsa.Studio.Workflows.Designer/designer.css" rel="stylesheet">
<link href="ElsaStudioWebAssembly.styles.css" rel="stylesheet">
</head>
<body>
<div id="app">
<div class="loading-splash mud-container mud-container-maxwidth-false">
<h5 class="mud-typography mud-typography-h5 mud-primary-text my-6">Loading...</h5>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="@baseUrl/_content/BlazorMonaco/jsInterop.js"></script>
<script src="@baseUrl/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
<script src="@baseUrl/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="@baseUrl/_content/MudBlazor/MudBlazor.min.js"></script>
<script src="@baseUrl/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
<script src="@baseUrl/_content/Radzen.Blazor/Radzen.Blazor.js"></script>
<script>
window.getClientConfig = function() { return {
"apiUrl": "@apiUrl"
} };
</script>
<script src="@baseUrl/_framework/blazor.webassembly.js"></script>
</body>
</html>
@@ -0,0 +1,223 @@
using Elsa.Agents;
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.MassTransit.Options;
using Elsa.Extensions;
using Elsa.JavaScript.Libraries.Extensions;
using Elsa.ServerAndStudio.Web.Extensions;
using Elsa.MassTransit.Extensions;
using Elsa.ServerAndStudio.Web.Enums;
using Medallion.Threading.FileSystem;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using WebhooksCore.Options;
using Elsa.Connections.Middleware;
using Elsa.IO.Http.Features;
using Proto.Persistence.Sqlite;
const bool useMassTransit = true;
const bool useProtoActor = true;
const bool useCaching = true;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit;
const MassTransitBroker useMassTransitBroker = MassTransitBroker.Memory;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStaticWebAssets();
var services = builder.Services;
var configuration = builder.Configuration;
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var mySqlConnectionString = configuration.GetConnectionString("MySql")!;
var sqlServerConnectionString = configuration.GetConnectionString("SqlServer")!;
var postgreSqlConnectionString = configuration.GetConnectionString("PostgreSql")!;
var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!;
var azureServiceBusConnectionString = configuration.GetConnectionString("AzureServiceBus")!;
var identitySection = configuration.GetSection("Identity");
var identityTokenSection = identitySection.GetSection("Tokens");
var massTransitSection = configuration.GetSection("MassTransit");
var massTransitDispatcherSection = configuration.GetSection("MassTransit.Dispatcher");
var heartbeatSection = configuration.GetSection("Heartbeat");
var sqlDatabaseProvider = Enum.Parse<SqlDatabaseProvider>(configuration["DatabaseProvider"] ?? "Sqlite");
services.Configure<MassTransitOptions>(massTransitSection);
services.Configure<MassTransitWorkflowDispatcherOptions>(massTransitDispatcherSection);
// Add Elsa services.
services
.AddElsa(elsa =>
{
elsa
.UseSasTokens()
.UseIdentity(identity =>
{
identity.TokenOptions = options => identityTokenSection.Bind(options);
identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseApplicationCluster(x => x.HeartbeatOptions = settings => heartbeatSection.Bind(settings))
.UseWorkflowManagement(management =>
{
if (useMassTransit)
{
management.UseMassTransitDispatcher();
}
if (useCaching)
management.UseCache();
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
management.UseEntityFrameworkCore(ef => ef.UseSqlServer(sqlServerConnectionString));
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
management.UseEntityFrameworkCore(ef => ef.UsePostgreSql(postgreSqlConnectionString));
else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite)
management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
#if !NET9_0
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
management.UseEntityFrameworkCore(ef => ef.UseMySql(mySqlConnectionString));
#endif
})
.UseWorkflowRuntime(runtime =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
runtime.UseEntityFrameworkCore(ef => ef.UseSqlServer(sqlServerConnectionString));
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
runtime.UseEntityFrameworkCore(ef => ef.UsePostgreSql(postgreSqlConnectionString));
else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite)
runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
#if !NET9_0
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
runtime.UseEntityFrameworkCore(ef => ef.UseMySql(mySqlConnectionString));
#endif
if (useMassTransit)
{
runtime.UseMassTransitDispatcher();
}
if (useProtoActor)
{
runtime.UseProtoActor();
}
if (useCaching)
runtime.UseCache();
runtime.DistributedLockProvider = _ => new FileDistributedSynchronizationProvider(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks")));
runtime.WorkflowInboxCleanupOptions = options => configuration.GetSection("Runtime:WorkflowInboxCleanup").Bind(options);
runtime.WorkflowDispatcherOptions = options => configuration.GetSection("Runtime:WorkflowDispatcher").Bind(options);
})
.UseScheduling()
.UseJavaScript(javaScriptFeature =>
{
javaScriptFeature
.ConfigureJintOptions(jintOptions => jintOptions.AllowClrAccess = true)
.UseLodashFp()
.UseMoment();
})
.UseLiquid()
.UseCSharp()
.UsePython(python =>
{
python.PythonOptions += options =>
{
// Make sure to configure the path to the python DLL. E.g. /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11
// alternatively, you can set the PYTHONNET_PYDLL environment variable.
configuration.GetSection("Scripting:Python").Bind(options);
};
})
.UseHttp(http =>
{
if (useCaching)
http.UseCache();
http.ConfigureHttpOptions = options =>
{
options.BaseUrl = new Uri(configuration["Hosting:BaseUrl"] ?? configuration["Http:BaseUrl"]!); // HttpBaseUrl is for backward compatibility.
options.BasePath = configuration["Http:BasePath"];
};
})
.UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options))
.UseWebhooks(webhooks => webhooks.ConfigureSinks = options => builder.Configuration.GetSection("Webhooks:Sinks").Bind(options))
.UseWorkflowsApi()
.UseCompression()
.Use<IOHttpFeature>()
.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>();
if (useProtoActor)
{
elsa.UseProtoActor(proto => proto.PersistenceProvider = _ =>
{
return new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString));
});
}
if (useMassTransit)
{
elsa.UseMassTransit(massTransit =>
{
switch (useMassTransitBroker)
{
case MassTransitBroker.AzureServiceBus:
massTransit.UseAzureServiceBus(azureServiceBusConnectionString, asb =>
{
asb.SubscriptionCleanupOptions = options => options.Interval = TimeSpan.FromMinutes(5);
asb.EnableAutomatedSubscriptionCleanup = true;
});
break;
case MassTransitBroker.RabbitMq:
massTransit.UseRabbitMq(rabbitMqConnectionString);
break;
}
}
);
}
if (distributedCachingTransport != DistributedCachingTransport.None)
{
elsa.UseDistributedCache(distributedCaching =>
{
if (distributedCachingTransport == DistributedCachingTransport.MassTransit) distributedCaching.UseMassTransit();
});
}
elsa.UseConnections(
configure=> configure.AddConnectionsFrom<Program>());
elsa.UseConnectionPersistence(ef=> ef.UseEntityFrameworkCore(f=>f.UseSqlite()));
elsa.UseConnectionsApi();
});
services.Configure<WebhookSinksOptions>(options => configuration.GetSection("Webhooks").Bind(options));
services.AddHealthChecks();
services.AddCors(cors => cors.Configure(configuration.GetSection("CorsPolicy")));
// Razor Pages.
services.AddRazorPages(options => options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()));
// Configure middleware pipeline.
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.MapHealthChecks("/health");
app.UseRouting();
app.UseCors();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.UseWorkflows();
app.MapFallbackToPage("/_Host");
await app.RunAsync();
@@ -0,0 +1,28 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:8080",
"sslPort": 44363
}
},
"profiles": {
"Elsa.AllInOne.Web": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:8080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,11 @@
# Server
This project represents an Elsa application that hosts workflows and exposes API endpoints to manage & execute workflows.
It also hosts the Elsa Studio Web Assembly project, which replaces the Stencil implementation.
## Secrets
The following are the secrets stored in hashed form in appsettings.json:
**API key**: `48587230567A646D394B435A6277734A-4802fa49-e91e-45e8-b00f-b5492377e20b`
**Admin user**: `admin`
**Admin password**: `password`
@@ -0,0 +1,135 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"OpenTelemetry": "Debug"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;",
"RabbitMq": "rabbitmq://guest:guest@localhost:5672",
"MySql": "Server=localhost;Database=elsa;Uid=user;Pwd=password;",
"SqlServer": "Server=.;Database=WorkflowDb;User Id=sa;Password=123456;TrustServerCertificate=True;",
"PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60"
},
"DatabaseProvider": "Sqlite",
"Hosting": {
"BaseUrl": "https://localhost:8080",
"ApiPrefix": "/elsa/api"
},
"Identity": {
"Tokens": {
"SigningKey": "c7dc81876a782d502084763fa322429fca015941eac90ce8ca7ad95fc8752035",
"AccessTokenLifetime": "1:00:00:00",
"RefreshTokenLifetime": "1:00:10:00"
},
"Roles": [
{
"Id": "admin",
"Name": "Administrator",
"Permissions": [
"*"
]
}
],
"Users": [
{
"Id": "a2323f46-42db-4e15-af8b-94238717d817",
"Name": "admin",
"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=",
"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=",
"Roles": [
"admin"
]
}
],
"Applications": [
{
"id": "d57030226341448daff5a2935aba2d3f",
"name": "Postman",
"roles": [
"admin"
],
"clientId": "HXr0Vzdm9KCZbwsJ",
"clientSecret": "a<~QGGHTEA%u4;CU&'Wga5ED:_&Gd1C)",
"hashedApiKey": "Z5ClHs3mbzx8Pnw3+PxbMq8A/Y+VKMCCDTGYtax8JFM=",
"hashedApiKeySalt": "kBisa1X8FwBfN2zmyGMFRgIVVBleghhQAJ4WGyTkaD0=",
"hashedClientSecret": "jEv58d0SVbGQ3nBZM0lkzHghG4Y+lMKW80wipz+9vHk=",
"hashedClientSecretSalt": "xRKy14Ok1/tU3kLf/8V1fcbLIegy9vcM90Peu2tzohU="
}
]
},
"Heartbeat": {
"Interval": "00:00:01:00",
"Timeout": "00:00:05:00"
},
"MassTransit": {
"TemporaryQueueTtl": "00:00:05:00",
"ConcurrentMessageLimit": 4,
"PrefetchCount": 4,
"MaxAutoRenewDuration": "00:00:00:30",
"Dispatcher": {
"ConcurrentMessageLimit": 4
}
},
"Smtp": {
"Host": "localhost",
"Port": 2525,
"DefaultSender": "noreply@crmservices.com"
},
"Http": {
"BasePath": "/api/workflows"
},
"Webhooks": {
"Sinks": []
},
"CorsPolicy": {
"Origins": ["*"],
"Methods": ["*"],
"Headers": ["*"]
},
"Runtime": {
"WorkflowInboxCleanup": {
"SweepInterval": "00:00:10:00",
"BatchSize": 1000
},
"WorkflowDispatcher": {
"Channels": [
{
"Name": "Low"
},
{
"Name": "Medium"
},
{
"Name": "High"
}
]
}
},
"Scripting": {
"Python": {
"PythonDllPath": "",
"Scripts": [
"def greet(name): return f'Hello {name}!'",
"def say_hello_world(): return greet('World')"
]
}
},
"AzureServiceBus": {
"ConnectionStringOrName": "AzureServiceBus",
"Queues": [
{
"name": "order-created"
},
{
"name": "order-completed"
}
],
"Topics": [],
"Subscriptions": []
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Elsa.Studio" />
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>
+3
View File
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,67 @@
@page "/"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject IConfiguration Configuration;
@{
var baseUrl = Configuration["ElsaServer:Url"]!;
var basePath = Configuration["Hosting:BasePath"]!;
if (string.IsNullOrWhiteSpace(baseUrl))
{
baseUrl = "https://localhost:5001/elsa/api";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<title>Elsa Studio 3.5</title>
<base href="/"/>
<link rel="apple-touch-icon" sizes="180x180" href="@basePath/_content/Elsa.Studio.Shell/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="@basePath/_content/Elsa.Studio.Shell/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="@basePath/_content/Elsa.Studio.Shell/favicon-16x16.png">
<link rel="manifest" href="@basePath/_content/Elsa.Studio.Shell/site.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Grandstander:wght@100&display=swap" rel="stylesheet">
<link href="@basePath/_content/MudBlazor/MudBlazor.min.css" rel="stylesheet"/>
<link href="@basePath/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet"/>
<link href="@basePath/_content/Radzen.Blazor/css/material-base.css" rel="stylesheet">
<link href="@basePath/_content/Elsa.Studio.Shell/css/shell.css" rel="stylesheet">
<link href="@basePath/_content/Elsa.Studio.Workflows.Designer/designer.css" rel="stylesheet">
<link href="ElsaStudioWebAssembly.styles.css" rel="stylesheet">
</head>
<body>
<div id="app">
<div class="loading-splash mud-container mud-container-maxwidth-false">
<h5 class="mud-typography mud-typography-h5 mud-primary-text my-6">Loading...</h5>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="@basePath/_content/BlazorMonaco/jsInterop.js"></script>
<script src="@basePath/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
<script src="@basePath/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="@basePath/_content/MudBlazor/MudBlazor.min.js"></script>
<script src="@basePath/_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
<script src="@basePath/_content/Radzen.Blazor/Radzen.Blazor.js"></script>
<script>
window.getClientConfig = function() { return {
"apiUrl": "@baseUrl",
"basePath": "@basePath"
} };
</script>
<script src="@basePath/_framework/blazor.webassembly.js"></script>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStaticWebAssets();
var services = builder.Services;
// Razor Pages.
services.AddRazorPages(options => options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()));
// Configure middleware pipeline.
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseRouting();
app.UseStaticFiles();
app.MapFallbackToPage("/_Host");
await app.RunAsync();
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:64391",
"sslPort": 44336
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5161",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7030;http://localhost:5161",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Elsa.Studio" />
<PackageReference Include="Elsa.Studio.Core.BlazorWasm" />
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" />
<PackageReference Include="Elsa.Studio.Agents" />
<PackageReference Include="Microsoft.AspNetCore.Components" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,5 @@
@inherits LayoutComponentBase
<main>
@Body
</main>
+58
View File
@@ -0,0 +1,58 @@
using System.Text.Json;
using Elsa.Studio.Dashboard.Extensions;
using Elsa.Studio.Shell;
using Elsa.Studio.Shell.Extensions;
using Elsa.Studio.Workflows.Extensions;
using Elsa.Studio.Contracts;
using Elsa.Studio.Core.BlazorWasm.Extensions;
using Elsa.Studio.Extensions;
using Elsa.Studio.Login.BlazorWasm.Extensions;
using Elsa.Studio.Login.Extensions;
using Elsa.Studio.Login.HttpMessageHandlers;
using Elsa.Studio.Models;
using Elsa.Studio.Options;
using Elsa.Studio.Workflows.Designer.Extensions;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
// Build the host.
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Register root components.
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.RootComponents.RegisterCustomElsaStudioElements();
// Register shell services and modules.
var backendApiConfig = new BackendApiConfig
{
ConfigureBackendOptions = options => builder.Configuration.GetSection("Backend").Bind(options),
ConfigureHttpClientBuilder = options => options.AuthenticationHandler = typeof(AuthenticatingApiHttpMessageHandler)
};
builder.Services.AddCore();
builder.Services.AddShell();
builder.Services.AddRemoteBackend(backendApiConfig);
builder.Services.AddLoginModule();
builder.Services.UseElsaIdentity();
builder.Services.AddDashboardModule();
builder.Services.AddWorkflowsModule();
builder.Services.AddAgentsModule(backendApiConfig);
// Build the application.
var app = builder.Build();
// Apply client config.
var js = app.Services.GetRequiredService<IJSRuntime>();
var clientConfig = await js.InvokeAsync<JsonElement>("getClientConfig");
var apiUrl = clientConfig.GetProperty("apiUrl").GetString()!;
app.Services.GetRequiredService<IOptions<BackendOptions>>().Value.Url = new Uri(apiUrl);
// Run each startup task.
var startupTaskRunner = app.Services.GetRequiredService<IStartupTaskRunner>();
await startupTaskRunner.RunStartupTasksAsync();
// Run the application.
await app.RunAsync();
@@ -0,0 +1,38 @@
{
"iisSettings": {
"iisExpress": {
"applicationUrl": "http://localhost:18109",
"sslPort": 44371
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5059",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7020;http://localhost:5059",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# Server
This project represents the Elsa Studio Blazor web assembly application and is hosted by Elsa.AllInOne.Web.
@@ -0,0 +1,6 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@@ -0,0 +1,56 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Api.Client.Extensions;
using JetBrains.Annotations;
namespace Elsa.Api.Client.Converters;
/// <summary>
/// Converts <see cref="Type"/> objects to and from their assembly-qualified name.
/// </summary>
[UsedImplicitly]
public class TypeJsonConverter : JsonConverter<Type>
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(Type) || typeToConvert.FullName == "System.RuntimeType";
}
/// <inheritdoc />
public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var typeName = reader.GetString()!;
// Handle collection types.
if (typeName.EndsWith("[]"))
{
var elementTypeName = typeName[..^"[]".Length];
var elementType = Type.GetType(elementTypeName)!;
return typeof(List<>).MakeGenericType(elementType);
}
return Type.GetType(typeName);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options)
{
// Handle collection types.
if (value is { IsGenericType: true, GenericTypeArguments.Length: 1 })
{
var elementType = value.GenericTypeArguments.First();
var typedEnumerable = typeof(IEnumerable<>).MakeGenericType(elementType);
if (typedEnumerable.IsAssignableFrom(value))
{
var elementTypeName = value.GetSimpleAssemblyQualifiedName();
JsonSerializer.Serialize(writer, $"{elementTypeName}[]", options);
return;
}
}
var typeName = value.GetSimpleAssemblyQualifiedName();
JsonSerializer.Serialize(writer, typeName, options);
}
}
@@ -0,0 +1,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Api.Client.Shared.Models;
namespace Elsa.Api.Client.Converters;
/// <summary>
/// A JSON converter that serializes <see cref="VersionOptions"/>.
/// </summary>
public class VersionOptionsJsonConverter : JsonConverter<VersionOptions>
{
/// <inheritdoc />
public override VersionOptions Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var textValue = reader.GetString();
return string.IsNullOrWhiteSpace(textValue) ? VersionOptions.Published : VersionOptions.FromString(textValue);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, VersionOptions value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
@@ -0,0 +1,17 @@
using System.ComponentModel;
using System.Globalization;
using Elsa.Api.Client.Shared.Models;
namespace Elsa.Api.Client.Converters;
/// <summary>
/// A type converter that converts <see cref="VersionOptions"/> to and from strings.
/// </summary>
public class VersionOptionsTypeConverter : TypeConverter
{
/// <inheritdoc />
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
/// <inheritdoc />
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => value is string text ? VersionOptions.FromString(text) : base.ConvertFrom(context, culture, value)!;
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides HTTP clients for the Elsa Workflows REST API.
</Description>
<PackageTags>elsa api client</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" />
<PackageReference Include="Polly" />
<PackageReference Include="Polly.Extensions.Http" />
<PackageReference Include="Refit" />
<PackageReference Include="Refit.HttpClientFactory" />
</ItemGroup>
</Project>
@@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resources_005Clogpersistencestrategies_005Ccontracts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resources_005Clogpersistencestrategies_005Cmodels/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -0,0 +1,216 @@
using System.Text.Json.Nodes;
using Elsa.Api.Client.Resources.Resilience.Models;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
using Elsa.Api.Client.Shared.Models;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides extension methods for <see cref="JsonObject"/>.
/// </summary>
public static class ActivityExtensions
{
/// <summary>
/// Gets the type name of the specified activity.
/// </summary>
public static string GetTypeName(this JsonObject activity) => activity.GetProperty<string>("type")!;
/// <summary>
/// Gets the type name of the specified activity.
/// </summary>
public static void SetTypeName(this JsonObject activity, string value) => activity.SetProperty(JsonValue.Create(value), "type");
/// <summary>
/// Gets the version of the specified activity.
/// </summary>
public static int GetVersion(this JsonObject activity) => activity.GetProperty<int>("version");
/// <summary>
/// Gets the version of the specified activity.
/// </summary>
public static void SetVersion(this JsonObject activity, int value) => activity.SetProperty(JsonValue.Create(value), "version");
/// <summary>
/// Gets the ID of the specified activity.
/// </summary>
public static string GetId(this JsonObject activity) => activity.GetProperty<string>("id")!;
/// <summary>
/// Sets the ID of the specified activity.
/// </summary>
public static void SetId(this JsonObject activity, string value) => activity.SetProperty(JsonValue.Create(value), "id");
/// <summary>
/// Gets the node ID of the specified activity.
/// </summary>
public static string GetNodeId(this JsonObject activity) => activity.GetProperty<string>("nodeId")!;
/// <summary>
/// Sets the node ID of the specified activity.
/// </summary>
public static void SetNodeId(this JsonObject activity, string value) => activity.SetProperty(JsonValue.Create(value), "nodeId");
/// <summary>
/// Gets the name of the specified activity.
/// </summary>
public static string? GetName(this JsonObject activity) => activity.GetProperty<string>("name");
/// <summary>
/// Sets the name of the specified activity.
/// </summary>
public static void SetName(this JsonObject activity, string? value) => activity.SetProperty(JsonValue.Create(value), "name");
/// <summary>
/// Gets the designer metadata for the specified activity.
/// </summary>
public static JsonObject? GetMetadata(this JsonObject activity) => activity.GetProperty("metadata")?.AsObject();
/// <summary>
/// Sets the designer metadata for the specified activity.
/// </summary>
public static void SetDesignerMetadata(this JsonObject activity, ActivityDesignerMetadata designerMetadata) => activity.SetProperty(designerMetadata.SerializeToNode(), "metadata", "designer");
/// <summary>
/// Gets the designer metadata for the specified activity.
/// </summary>
public static ActivityDesignerMetadata GetDesignerMetadata(this JsonObject activity) => activity.GetProperty<ActivityDesignerMetadata>("metadata", "designer") ?? new ActivityDesignerMetadata();
/// <summary>
/// Gets the display text for the specified activity.
/// </summary>
public static string? GetDisplayText(this JsonObject activity) => activity.GetProperty("metadata", "displayText")?.GetValue<string>();
/// <summary>
/// Sets the display text for the specified activity.
/// </summary>
public static void SetDisplayText(this JsonObject activity, string? value)
{
activity.SetProperty(JsonValue.Create(value), "metadata", "displayText");
}
/// <summary>
/// Gets the description for the specified activity.
/// </summary>
public static string? GetDescription(this JsonObject activity) => activity.GetProperty("metadata", "description")?.GetValue<string>();
/// <summary>
/// Sets the description for the specified activity.
/// </summary>
public static void SetDescription(this JsonObject activity, string value) => activity.SetProperty(JsonValue.Create(value), "metadata", "description");
/// <summary>
/// Gets a value indicating whether the description for the specified activity should be shown.
/// </summary>
public static bool? GetShowDescription(this JsonObject activity) => activity.GetProperty<bool>("metadata", "showDescription");
/// <summary>
/// Sets a value indicating whether the description for the specified activity should be shown.
/// </summary>
public static void SetShowDescription(this JsonObject activity, bool value) => activity.SetProperty(JsonValue.Create(value), "metadata", "showDescription");
/// <summary>
/// Gets a value indicating whether the specified activity can trigger the workflow.
/// </summary>
public static bool? GetCanStartWorkflow(this JsonObject activity) => activity.GetProperty<bool>("customProperties", "canStartWorkflow");
/// <summary>
/// Sets a value indicating whether the specified activity can trigger the workflow.
/// </summary>
public static void SetCanStartWorkflow(this JsonObject activity, bool value) => activity.SetProperty(JsonValue.Create(value), "customProperties", "canStartWorkflow");
/// <summary>
/// Gets the activities in the specified flowchart.
/// </summary>
public static IEnumerable<JsonObject> GetActivities(this JsonObject flowchart) => flowchart.GetProperty("activities")?.AsArray().AsEnumerable().Cast<JsonObject>() ?? Array.Empty<JsonObject>();
/// <summary>
/// Sets the activities in the specified flowchart.
/// </summary>
public static void SetActivities(this JsonObject flowchart, JsonArray activities) => flowchart.SetProperty(activities, "activities");
/// <summary>
/// Gets the connections in the specified flowchart.
/// </summary>
public static IEnumerable<Connection> GetConnections(this JsonObject flowchart) => flowchart.GetProperty<ICollection<Connection>>("connections") ?? new List<Connection>();
/// <summary>
/// Sets the connections in the specified flowchart.
/// </summary>
public static void SetConnections(this JsonObject flowchart, IEnumerable<Connection> connections) => flowchart.SetProperty(JsonValue.Create(connections), "connections");
/// <summary>
/// Gets the root activity in the specified activity.
/// </summary>
public static JsonObject? GetRoot(this JsonObject activity) => activity.GetProperty("root") as JsonObject;
/// <summary>
/// Sets the root activity in the specified activity.
/// </summary>
public static void SetRoot(this JsonObject container, JsonObject root) => container.SetProperty(root, "root");
/// <summary>
/// Gets the root activity in the specified activity.
/// </summary>
public static string? GetWorkflowDefinitionId(this JsonObject activity)
{
// Determine if this activity represents a workflow definition activity based on the presence of the following properties.
var workflowDefinitionIdNode = activity.GetProperty("workflowDefinitionId");
var workflowDefinitionVersionId = activity.GetProperty("workflowDefinitionVersionId");
var latestAvailablePublishedVersion = activity.GetProperty("latestAvailablePublishedVersion");
var latestAvailablePublishedVersionId = activity.GetProperty("latestAvailablePublishedVersionId");
if (workflowDefinitionIdNode == null || workflowDefinitionVersionId == null || latestAvailablePublishedVersion == null || latestAvailablePublishedVersionId == null)
return null; // Not a workflow definition activity.
if (workflowDefinitionIdNode is JsonValue value)
return value.ToString();
return null; // Not a workflow definition activity.
}
/// <summary>
/// Determines whether the given activity is a workflow definition activity.
/// </summary>
/// <param name="activity">The JsonObject representing the activity.</param>
/// <returns>Returns true if the activity is a workflow definition activity; otherwise, false.</returns>
public static bool GetIsWorkflowDefinitionActivity(this JsonObject activity) => activity.ContainsKey("workflowDefinitionId") && activity.ContainsKey("workflowDefinitionVersionId");
/// <summary>
/// Gets a value indicating whether the specified activity can trigger the workflow.
/// </summary>
public static bool? GetRunAsynchronously(this JsonObject activity) => activity.GetProperty<bool>("customProperties", "runAsynchronously");
/// <summary>
/// Sets a value indicating whether the specified activity can trigger the workflow.
/// </summary>
public static void SetRunAsynchronously(this JsonObject activity, bool value) => activity.SetProperty(JsonValue.Create(value), "customProperties", "runAsynchronously");
/// <summary>
/// Gets the commit state behavior for the specified activity.
/// </summary>
public static string? GetCommitStrategy(this JsonObject activity) => activity.TryGetProperty<string?>("customProperties", "commitStrategyName");
/// <summary>
/// Sets the commit state behavior for the specified activity.
/// </summary>
public static void SetCommitStrategy(this JsonObject activity, string? name) => activity.SetProperty(JsonValue.Create(name), "customProperties", "commitStrategyName");
/// <summary>
/// Gets the resilience strategy for the specified activity.
/// </summary>
public static ResilienceStrategyConfig? GetResilienceStrategy(this JsonObject activity)
{
var node = activity.GetProperty("customProperties", "resilienceStrategy");
return ResilienceStrategyConfig.Deserialize(node);
}
/// <summary>
/// Sets the resilience strategy for the specified activity.
/// </summary>
public static void SetResilienceStrategy(this JsonObject activity, ResilienceStrategyConfig? config)
{
var node = config?.SerializeToNode();
activity.SetProperty(node, "customProperties", "resilienceStrategy");
}
}
@@ -0,0 +1,28 @@
using Refit;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides extension methods for <see cref="IApiResponse"/>.
/// </summary>
public static class ApiResponseExtensions
{
/// <summary>
/// Returns the downloaded file name or the specified default file name.
/// </summary>
public static string GetDownloadedFileNameOrDefault(this IApiResponse response, string defaultFileName = "download.bin")
{
var fileName = defaultFileName;
if (response.Headers.TryGetValues("content-disposition", out var contentDispositionHeader)) // Only available if the Elsa Server exposes the "Content-Disposition" header.
{
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
var values = contentDispositionHeader.ToList() ?? [];
if (values.Count >= 2)
fileName = values[1].Split('=')[1];
}
return fileName;
}
}
@@ -0,0 +1,186 @@
using Elsa.Api.Client.Options;
using Elsa.Api.Client.Resources.ActivityDescriptorOptions.Contracts;
using Elsa.Api.Client.Resources.ActivityDescriptors.Contracts;
using Elsa.Api.Client.Resources.ActivityExecutions.Contracts;
using Elsa.Api.Client.Resources.Alterations.Contracts;
using Elsa.Api.Client.Resources.CommitStrategies.Contracts;
using Elsa.Api.Client.Resources.Features.Contracts;
using Elsa.Api.Client.Resources.Identity.Contracts;
using Elsa.Api.Client.Resources.IncidentStrategies.Contracts;
using Elsa.Api.Client.Resources.LogPersistenceStrategies;
using Elsa.Api.Client.Resources.Resilience.Contracts;
using Elsa.Api.Client.Resources.Scripting.Contracts;
using Elsa.Api.Client.Resources.StorageDrivers.Contracts;
using Elsa.Api.Client.Resources.Tasks.Contracts;
using Elsa.Api.Client.Resources.VariableTypes.Contracts;
using Elsa.Api.Client.Resources.WorkflowActivationStrategies.Contracts;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Api.Client.Resources.WorkflowExecutionContexts.Contracts;
using Elsa.Api.Client.Resources.WorkflowInstances.Contracts;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Refit;
using static Elsa.Api.Client.RefitSettingsHelper;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides extension methods for dependency injection.
/// </summary>
[PublicAPI]
public static class DependencyInjectionExtensions
{
/// <summary>
/// Adds default Elsa API clients configured to use an API key.
/// </summary>
public static IServiceCollection AddDefaultApiClientsUsingApiKey(this IServiceCollection services, Action<ElsaClientOptions> configureOptions)
{
var options = new ElsaClientOptions();
configureOptions(options);
return services.AddDefaultApiClients(client =>
{
client.BaseAddress = options.BaseAddress;
client.ApiKey = options.ApiKey;
client.ConfigureHttpClient = options.ConfigureHttpClient;
});
}
/// <summary>
/// Adds default Elsa API clients.
/// </summary>
public static IServiceCollection AddDefaultApiClients(this IServiceCollection services, Action<ElsaClientBuilderOptions>? configureClient = null)
{
return services.AddApiClients(configureClient, builderOptions =>
{
var builderOptionsWithoutRetryPolicy = new ElsaClientBuilderOptions
{
ApiKey = builderOptions.ApiKey,
AuthenticationHandler = builderOptions.AuthenticationHandler,
BaseAddress = builderOptions.BaseAddress,
ConfigureHttpClient = builderOptions.ConfigureHttpClient,
ConfigureHttpClientBuilder = builderOptions.ConfigureHttpClientBuilder,
ConfigureRetryPolicy = null
};
services.AddApi<IWorkflowDefinitionsApi>(builderOptions);
services.AddApi<IExecuteWorkflowApi>(builderOptionsWithoutRetryPolicy);
services.AddApi<IWorkflowInstancesApi>(builderOptions);
services.AddApi<IActivityDescriptorsApi>(builderOptions);
services.AddApi<IActivityDescriptorOptionsApi>(builderOptions);
services.AddApi<IActivityExecutionsApi>(builderOptions);
services.AddApi<IStorageDriversApi>(builderOptions);
services.AddApi<IVariableTypesApi>(builderOptions);
services.AddApi<IWorkflowActivationStrategiesApi>(builderOptions);
services.AddApi<IIncidentStrategiesApi>(builderOptions);
services.AddApi<ILogPersistenceStrategiesApi>(builderOptions);
services.AddApi<ICommitStrategiesApi>(builderOptions);
services.AddApi<IResilienceStrategiesApi>(builderOptions);
services.AddApi<IRetryAttemptsApi>(builderOptions);
services.AddApi<ILoginApi>(builderOptions);
services.AddApi<IFeaturesApi>(builderOptions);
services.AddApi<IJavaScriptApi>(builderOptions);
services.AddApi<IExpressionDescriptorsApi>(builderOptions);
services.AddApi<IWorkflowContextProviderDescriptorsApi>(builderOptions);
services.AddApi<IAlterationsApi>(builderOptions);
services.AddApi<ITasksApi>(builderOptions);
});
}
/// <summary>
/// Adds an API client to the service collection. Requires AddElsaClient to be called exactly once.
/// </summary>
public static IServiceCollection AddApiClient<T>(this IServiceCollection services, Action<ElsaClientBuilderOptions>? configureClient = null) where T : class
{
return services.AddApiClients(configureClient, builderOptions => services.AddApi<T>(builderOptions));
}
/// <summary>
/// Adds the Elsa client to the service collection.
/// </summary>
public static IServiceCollection AddApiClients(this IServiceCollection services, Action<ElsaClientBuilderOptions>? configureClient = null, Action<ElsaClientBuilderOptions>? configureServices = null)
{
var builderOptions = new ElsaClientBuilderOptions();
configureClient?.Invoke(builderOptions);
builderOptions.ConfigureHttpClientBuilder += builder => builder.AddHttpMessageHandler(sp => (DelegatingHandler)sp.GetRequiredService(builderOptions.AuthenticationHandler));
services.TryAddScoped(builderOptions.AuthenticationHandler);
services.Configure<ElsaClientOptions>(options =>
{
options.BaseAddress = builderOptions.BaseAddress;
options.ConfigureHttpClient = builderOptions.ConfigureHttpClient;
options.ApiKey = builderOptions.ApiKey;
});
configureServices?.Invoke(builderOptions);
return services;
}
/// <summary>
/// Adds a refit client for the specified API type.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="httpClientBuilderOptions">An options object that can be used to configure the HTTP client builder.</param>
/// <typeparam name="T">The type representing the API.</typeparam>
public static IServiceCollection AddApi<T>(this IServiceCollection services, ElsaClientBuilderOptions? httpClientBuilderOptions = default) where T : class
{
return services.AddApi(typeof(T), httpClientBuilderOptions);
}
/// <summary>
/// Adds a refit client for the specified API type.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="apiType">The type representing the API</param>
/// <param name="httpClientBuilderOptions">An options object that can be used to configure the HTTP client builder.</param>
public static IServiceCollection AddApi(this IServiceCollection services, Type apiType, ElsaClientBuilderOptions? httpClientBuilderOptions = default)
{
var builder = services.AddRefitClient(apiType, sp => CreateRefitSettings(sp, httpClientBuilderOptions?.ConfigureJsonSerializerOptions), apiType.Name).ConfigureHttpClient(ConfigureElsaApiHttpClient);
httpClientBuilderOptions?.ConfigureHttpClientBuilder(builder);
httpClientBuilderOptions?.ConfigureRetryPolicy?.Invoke(builder);
return services;
}
/// <summary>
/// Adds a refit client for the specified API type.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="httpClientBuilderOptions">An options object that can be used to configure the HTTP client builder.</param>
/// <typeparam name="T">The type representing the API.</typeparam>
public static void AddApiWithoutRetryPolicy<T>(this IServiceCollection services, ElsaClientBuilderOptions? httpClientBuilderOptions = default) where T : class
{
var builder = services
.AddRefitClient<T>(sp => CreateRefitSettings(sp), typeof(T).Name)
.ConfigureHttpClient(ConfigureElsaApiHttpClient);
httpClientBuilderOptions?.ConfigureHttpClientBuilder(builder);
}
/// <summary>
/// Creates an API client for the specified API type.
/// </summary>
public static T CreateApi<T>(this IServiceProvider serviceProvider, Uri baseAddress) where T : class
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient(typeof(T).Name);
httpClient.BaseAddress = baseAddress;
return CreateApi<T>(serviceProvider, httpClient);
}
/// <summary>
/// Creates an API client for the specified API type.
/// </summary>
public static T CreateApi<T>(this IServiceProvider serviceProvider, HttpClient httpClient) where T : class
{
return RestService.For<T>(httpClient, CreateRefitSettings(serviceProvider));
}
private static void ConfigureElsaApiHttpClient(IServiceProvider serviceProvider, HttpClient httpClient)
{
var options = serviceProvider.GetRequiredService<IOptions<ElsaClientOptions>>().Value;
httpClient.BaseAddress = options.BaseAddress;
options.ConfigureHttpClient?.Invoke(serviceProvider, httpClient);
}
}
@@ -0,0 +1,66 @@
using System.Text.Json;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides extension methods for <see cref="JsonElement"/>.
/// </summary>
public static class DictionaryExtensions
{
/// <summary>
/// Returns the value of the specified property if it exists, otherwise the default value.
/// </summary>
public static T TryGetValue<T>(this IDictionary<string, object> dictionary, string key, Func<T>? defaultValue = default, JsonSerializerOptions? serializerOptions = default)
{
var caseInsensitiveDictionary = ToCaseInsensitiveDictionary(dictionary);
if (caseInsensitiveDictionary.TryGetValue(key, out var value) && value is not JsonElement { ValueKind: JsonValueKind.Undefined })
{
var convertedValue = value.ConvertTo<T>(new ObjectConverterOptions(serializerOptions));
if (convertedValue != null)
return convertedValue;
}
if (defaultValue == null)
return default!;
var defaultVal = defaultValue()!;
dictionary[key] = defaultVal;
return defaultVal;
}
/// <summary>
/// Returns the value of the specified property if it exists, otherwise the default value.
/// </summary>
public static object? TryGetValue(this IDictionary<string, object> dictionary, string key, Func<object>? defaultValue = default)
{
var caseInsensitiveDictionary = ToCaseInsensitiveDictionary(dictionary);
if (caseInsensitiveDictionary.TryGetValue(key, out var value) && value is not JsonElement { ValueKind: JsonValueKind.Undefined })
return value;
if (defaultValue == null)
return default;
var defaultVal = defaultValue()!;
dictionary[key] = defaultVal;
return defaultVal;
}
// A method that de-duplicates the keys in the dictionary, taking into account that there may already be multiple keys that would be considered the same if the case was ignored.
private static Dictionary<string, object> ToCaseInsensitiveDictionary(this IDictionary<string, object> dictionary)
{
var caseInsensitiveDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
foreach (var key in dictionary.Keys)
{
if (caseInsensitiveDictionary.ContainsKey(key))
continue;
caseInsensitiveDictionary[key] = dictionary[key]!;
}
return caseInsensitiveDictionary;
}
}
@@ -0,0 +1,14 @@
using Elsa.Api.Client.Shared;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Contains extension methods for the <see cref="HttpResponseMessage"/> class.
/// </summary>
public static class HttpResponseMessageExtensions
{
/// <summary>
/// Gets the workflow instance ID from the response.
/// </summary>
public static string? GetWorkflowInstanceId(this HttpResponseMessage response) => response.Headers.TryGetValues(HeaderNames.WorkflowInstanceId, out var values) ? values.FirstOrDefault() : default;
}
@@ -0,0 +1,26 @@
using System.Text.Json;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides extension methods for <see cref="JsonElement"/>.
/// </summary>
public static class JsonElementExtensions
{
/// <summary>
/// Returns the value of the specified property if it exists, otherwise the default value.
/// </summary>
public static bool TryGetPropertySafe(this JsonElement element, string propertyName, out JsonElement value)
{
value = default;
if(element.ValueKind != JsonValueKind.Object)
return false;
if (!element.TryGetProperty(propertyName, out var property))
return false;
value = property;
return true;
}
}
@@ -0,0 +1,207 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides extension methods for <see cref="JsonObject"/>.
/// </summary>
public static class JsonObjectExtensions
{
/// <summary>
/// Returns true if the specified <see cref="JsonObject"/> represents an activity.
/// </summary>
public static bool IsActivity(this JsonObject obj)
{
return obj.ContainsKey("type") && obj.ContainsKey("id") && obj.ContainsKey("version");
}
/// <summary>
/// Serializes the specified value to a <see cref="JsonObject"/>.
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <returns>A <see cref="JsonObject"/> representing the specified value.</returns>
public static JsonNode SerializeToNode(this object value, JsonSerializerOptions? options = null)
{
options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
return JsonSerializer.SerializeToNode(value, options)!;
}
/// <summary>
/// Serializes the specified value to a <see cref="JsonArray"/>.
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <returns>A <see cref="JsonObject"/> representing the specified value.</returns>
public static JsonArray SerializeToArray(this object value, JsonSerializerOptions? options = null)
{
options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
return JsonSerializer.SerializeToNode(value, options)!.AsArray();
}
/// <summary>
/// Serializes the specified value to a <see cref="JsonArray"/>.
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <returns>A <see cref="JsonObject"/> representing the specified value.</returns>
public static JsonArray SerializeToArray<T>(this IEnumerable<T> value, JsonSerializerOptions? options = null)
{
options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
return JsonSerializer.SerializeToNode(value, options)!.AsArray();
}
/// <summary>
/// Deserializes the specified <see cref="JsonNode"/> to the specified type.
/// </summary>
/// <param name="value">The <see cref="JsonNode"/> to deserialize.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <returns>The deserialized value.</returns>
public static T Deserialize<T>(this JsonNode value, JsonSerializerOptions? options = null)
{
options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
if (value is JsonObject jsonObject)
return JsonSerializer.Deserialize<T>(jsonObject, options)!;
if (value is JsonArray jsonArray)
return JsonSerializer.Deserialize<T>(jsonArray, options)!;
if (typeof(T).IsEnum || (Nullable.GetUnderlyingType(typeof(T))?.IsEnum ?? false))
{
if (value.GetValueKind() == JsonValueKind.Null)
return default!;
return (T)Enum.Parse(Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T), value.ToString());
}
if (value is JsonValue jsonValue)
return jsonValue.GetValue<T>();
throw new NotSupportedException($"Cannot deserialize {value.GetType()} to {typeof(T)}.");
}
/// <summary>
/// Sets the property value of the specified model.
/// </summary>
/// <param name="model">The model to set the property value on.</param>
/// <param name="value">The value to set.</param>
/// <param name="path">The path to the property.</param>
public static void SetProperty(this JsonObject model, JsonNode? value, params string[] path)
{
model = GetPropertyContainer(model, path);
model[path.Last()] = value?.SerializeToNode();
}
/// <summary>
/// Sets the property value of the specified model.
/// </summary>
/// <param name="model">The model to set the property value on.</param>
/// <param name="value">The value to set.</param>
/// <param name="path">The path to the property.</param>
public static void SetProperty(this JsonObject model, JsonArray? value, params string[] path)
{
model = GetPropertyContainer(model, path);
model[path.Last()] = value?.SerializeToNode();
}
/// <summary>
/// Sets the property value of the specified model.
/// </summary>
/// <param name="model">The model to set the property value on.</param>
/// <param name="value">The value to set.</param>
/// <param name="path">The path to the property.</param>
public static void SetProperty(this JsonObject model, IEnumerable<JsonNode> value, params string[] path)
{
model = GetPropertyContainer(model, path);
model[path.Last()] = new JsonArray(value.Select(x => x.SerializeToNode()).ToArray());
}
/// <summary>
/// Gets the property value of the specified model.
/// </summary>
/// <param name="model">The model to get the property value from.</param>
/// <param name="path">The path to the property.</param>
/// <returns>The property value.</returns>
public static JsonNode? GetProperty(this JsonObject model, params string[] path)
{
var currentModel = model;
foreach (var prop in path.SkipLast(1))
{
if (currentModel[prop] is not JsonObject value)
return null;
currentModel = value;
}
return currentModel[path.Last()];
}
/// <summary>
/// Gets the property value of the specified model.
/// </summary>
/// <param name="model">The model to get the property value from.</param>
/// <param name="path">The path to the property.</param>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <returns>The property value.</returns>
public static T? TryGetProperty<T>(this JsonObject model, params string[] path)
{
try
{
return model.GetProperty<T>(path);
}
catch (Exception e)
{
return default;
}
}
/// <summary>
/// Gets the property value of the specified model.
/// </summary>
/// <param name="model">The model to get the property value from.</param>
/// <param name="path">The path to the property.</param>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <returns>The property value.</returns>
public static T? GetProperty<T>(this JsonObject model, params string[] path)
{
var property = GetProperty(model, path);
return property != null ? property.Deserialize<T>() : default;
}
/// <summary>
/// Gets the property value of the specified model.
/// </summary>
/// <param name="model">The model to get the property value from.</param>
/// <param name="options">The <see cref="JsonSerializerOptions"/> to use when deserializing.</param>
/// <param name="path">The path to the property.</param>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <returns>The property value.</returns>
public static T? GetProperty<T>(this JsonObject model, JsonSerializerOptions options, params string[] path)
{
var property = GetProperty(model, path);
return property != null ? property.Deserialize<T>(options) : default;
}
/// <summary>
/// Returns the property container of the specified model.
/// </summary>
/// <param name="model">The model to set the property value on.</param>
/// <param name="path">The path to the property.</param>
private static JsonObject GetPropertyContainer(this JsonObject model, params string[] path)
{
foreach (var prop in path.SkipLast(1))
{
var property = model[prop] as JsonObject ?? new JsonObject();
model[prop] = property;
model = property;
}
return model;
}
}
@@ -0,0 +1,229 @@
using System.Collections;
using System.ComponentModel;
using System.Dynamic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides options to the conversion method.
/// </summary>
public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions = default)
{
/// <inheritdoc />
public ObjectConverterOptions(Action<JsonSerializerOptions> configureSerializerOptions) : this()
{
SerializerOptions = new JsonSerializerOptions();
configureSerializerOptions(SerializerOptions);
}
}
/// <summary>
/// A helper that attempts many strategies to try and convert the source value into the destination type.
/// </summary>
public static class ObjectConverter
{
/// <summary>
/// Attempts to convert the source value into the destination type.
/// </summary>
public static T? ConvertTo<T>(this object? value, ObjectConverterOptions? serializerOptions = null) => value != null ? (T?)value.ConvertTo(typeof(T), serializerOptions) : default;
/// <summary>
/// Attempts to convert the source value into the destination type.
/// </summary>
public static object? ConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null)
{
if (value == null)
return default!;
var sourceType = value.GetType();
if (sourceType == targetType)
return value;
var options = converterOptions?.SerializerOptions != null ? new JsonSerializerOptions(converterOptions.SerializerOptions) : new JsonSerializerOptions();
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.ReferenceHandler = ReferenceHandler.Preserve;
options.PropertyNameCaseInsensitive = true;
options.Converters.Add(new JsonStringEnumConverter());
var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
if (value is JsonElement { ValueKind: JsonValueKind.Number } jsonNumber && underlyingTargetType == typeof(string))
return jsonNumber.ToString().ConvertTo(underlyingTargetType);
if (value is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
return jsonElement.GetString().ConvertTo(underlyingTargetType);
return jsonElement.Deserialize(targetType, options);
}
if (value is JsonNode jsonNode)
return jsonNode.Deserialize(targetType, options);
if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeof(object))
{
var stringValue = (string)value;
try
{
var firstChar = stringValue.TrimStart().FirstOrDefault();
if (firstChar is '{' or '[')
return JsonSerializer.Deserialize(stringValue, underlyingTargetType, options);
}
catch (Exception)
{
throw new Exception($"Failed to deserialize {stringValue} to {underlyingTargetType}");
}
}
if (targetType == typeof(object))
return value;
if (underlyingTargetType.IsInstanceOfType(value))
return value;
if (underlyingSourceType == underlyingTargetType)
return value;
if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType))
return ConvertAnyDateType(value, underlyingTargetType);
if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
{
if (typeof(ExpandoObject) == underlyingTargetType)
{
var expandoJson = JsonSerializer.Serialize(value);
return ConvertTo(expandoJson, underlyingTargetType, converterOptions);
}
if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingTargetType))
return new Dictionary<string, object>((IDictionary<string, object>)value);
if (typeof(ExpandoObject) == underlyingSourceType)
{
// Parse ExpandoObject into target type.
var expandoObject = (IDictionary<string, object>)value;
var json = JsonSerializer.Serialize(expandoObject);
return ConvertTo(json, underlyingTargetType, converterOptions);
}
}
var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
return targetTypeConverter.IsValid(value)
? targetTypeConverter.ConvertFrom(null!, CultureInfo.InvariantCulture, value)
: targetType.GetDefaultValue();
var sourceTypeConverter = TypeDescriptor.GetConverter(underlyingSourceType);
if (sourceTypeConverter.CanConvertTo(underlyingTargetType))
return sourceTypeConverter.ConvertTo(value, underlyingTargetType);
if (underlyingTargetType.IsEnum)
{
if (underlyingSourceType == typeof(string))
return Enum.Parse(underlyingTargetType, (string)value);
if (underlyingSourceType == typeof(int))
return Enum.ToObject(underlyingTargetType, value);
if (underlyingSourceType == typeof(double))
return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantCulture));
}
if (value is string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
if (underlyingTargetType == typeof(Type))
return Type.GetType(s);
// Perhaps it's a bit of a leap, but if the input is a string and the target type is IEnumerable<string>, then let's assume the string is a comma-separated list of strings.
if (typeof(IEnumerable<string>).IsAssignableFrom(underlyingTargetType))
return new[] { s };
}
if (value is IEnumerable enumerable)
{
if (underlyingTargetType is { IsGenericType: true })
{
var desiredCollectionItemType = targetType.GenericTypeArguments[0];
var desiredCollectionType = typeof(ICollection<>).MakeGenericType(desiredCollectionItemType);
if (underlyingTargetType.IsAssignableFrom(desiredCollectionType) || desiredCollectionType.IsAssignableFrom(underlyingTargetType))
{
var collectionType = typeof(List<>).MakeGenericType(desiredCollectionItemType);
var collection = (IList)Activator.CreateInstance(collectionType)!;
foreach (var item in enumerable)
{
var convertedItem = ConvertTo(item, desiredCollectionItemType);
collection.Add(convertedItem);
}
return collection;
}
}
}
try
{
return Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
}
catch (InvalidCastException)
{
throw new Exception($"Failed to convert an object of type {sourceType} to {underlyingTargetType}");
}
}
/// <summary>
/// Returns true if the specified type is date-like type, false otherwise.
/// </summary>
private static bool IsDateType(Type type)
{
var dateTypes = new[]
{
typeof(DateTime),
typeof(DateTimeOffset)
};
return dateTypes.Contains(type);
}
/// <summary>
/// Converts any date type to the specified target type.
/// </summary>
/// <param name="value">Any of <see cref="DateTime"/> or <see cref="DateTimeOffset"/>.</param>
/// <param name="targetType">Any of <see cref="DateTime"/> or <see cref="DateTimeOffset"/>.</param>
/// <returns>The converted value.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is not of type <see cref="DateTime"/> or <see cref="DateTimeOffset"/>.</exception>
private static object ConvertAnyDateType(object value, Type targetType)
{
return targetType switch
{
{ } t when t == typeof(DateTime) => value switch
{
DateTime dateTime => dateTime,
DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime,
_ => throw new ArgumentException("Invalid value type.")
},
{ } t when t == typeof(DateTimeOffset) => value switch
{
DateTime dateTime => new DateTimeOffset(dateTime),
DateTimeOffset dateTimeOffset => dateTimeOffset,
_ => throw new ArgumentException("Invalid value type.")
},
_ => throw new ArgumentException("Invalid target type.")
};
}
}
@@ -0,0 +1,45 @@
using System.Text.Json;
using Elsa.Api.Client.Shared.Models;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Provides extension methods for the PropertyBag class.
/// </summary>
public static class PropertyBagExtensions
{
/// <summary>
/// Tries to retrieve a value from the PropertyBag based on the provided key.
/// If the specified key does not exist in the PropertyBag, the method will return the default value obtained from the defaultValue function.
/// </summary>
/// <typeparam name="T">The type of the value to retrieve.</typeparam>
/// <param name="propertyBag">The PropertyBag to retrieve the value from.</param>
/// <param name="key">The key of the value to retrieve.</param>
/// <param name="defaultValue">A function that returns the default value to be returned if the key does not exist in the PropertyBag.</param>
/// <returns>The value associated with the key, or the default value if the key does not exist.</returns>
public static T? TryGetValueOrDefault<T>(this PropertyBag propertyBag, string key, Func<T> defaultValue)
{
if (!propertyBag.TryGetValue(key, out var value))
return defaultValue();
var json = value.ToString();
if (string.IsNullOrWhiteSpace(json))
return defaultValue();
return JsonSerializer.Deserialize<T>(json);
}
/// <summary>
/// Sets a value in the PropertyBag based on the provided key.
/// The value is serialized using JSON.
/// </summary>
/// <param name="propertyBag">The PropertyBag to set the value in.</param>
/// <param name="key">The key to associate with the value.</param>
/// <param name="value">The value to store in the PropertyBag.</param>
public static void SetValue(this PropertyBag propertyBag, string key, object value)
{
var json = JsonSerializer.Serialize(value);
propertyBag[key] = json;
}
}
@@ -0,0 +1,16 @@
using System.Text.Json.Nodes;
namespace Elsa.Api.Client.Extensions;
public static class ResilienceStrategyExtensions
{
public static string GetResilienceStrategyId(this JsonObject strategyJsonObject)
{
return strategyJsonObject.GetProperty<string>("id")!;
}
public static string GetResilienceStrategyDisplayName(this JsonObject strategyJsonObject)
{
return strategyJsonObject.GetProperty<string>("displayName")!;
}
}
@@ -0,0 +1,98 @@
using System.Collections.Concurrent;
namespace Elsa.Api.Client.Extensions;
/// <summary>
/// Adds extension methods to <see cref="Type"/>.
/// </summary>
public static class TypeExtensions
{
private static readonly ConcurrentDictionary<Type, string> SimpleAssemblyQualifiedTypeNameCache = new();
/// <summary>
/// Gets the assembly-qualified name of the type, without any version info etc.
/// E.g. "System.String, System.Private.CoreLib"
/// </summary>
public static string GetSimpleAssemblyQualifiedName(this Type type)
{
if (type is null) throw new ArgumentNullException(nameof(type));
return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, BuildSimplifiedName);
}
/// <summary>
/// Returns the default value for the specified type.
/// </summary>
public static object? GetDefaultValue(this Type type) => type.IsClass ? null : Activator.CreateInstance(type);
/// <summary>
/// Returns the element type of the specified type representing an array or generic enumerable.
/// </summary>
public static Type GetEnumerableElementType(this Type type)
{
if (type.IsArray)
return type.GetElementType()!;
var elementType = FindIEnumerable(type);
return elementType == null ? type : elementType.GetGenericArguments()[0];
}
/// <summary>
/// Searches for the first implemented IEnumerable interface in the given type hierarchy, and returns the generic type argument of the interface.
/// </summary>
/// <param name="sequenceType">The type to search for the IEnumerable interface.</param>
/// <returns>The generic type argument of the first implemented IEnumerable interface found in the type hierarchy, or null if none is found.</returns>
private static Type? FindIEnumerable(Type? sequenceType)
{
if (sequenceType == null || sequenceType == typeof(string))
return null;
if (sequenceType.IsArray)
return typeof(IEnumerable<>).MakeGenericType(sequenceType.GetElementType()!);
if (sequenceType.IsGenericType)
{
foreach (var arg in sequenceType.GetGenericArguments())
{
var enumerable = typeof(IEnumerable<>).MakeGenericType(arg);
if (enumerable.IsAssignableFrom(sequenceType))
return enumerable;
}
}
var interfaces = sequenceType.GetInterfaces();
if (interfaces is { Length: > 0 })
{
foreach (var interfaceType in interfaces)
{
var enumerable = FindIEnumerable(interfaceType);
if (enumerable != null) return enumerable;
}
}
if (sequenceType.BaseType != null && sequenceType.BaseType != typeof(object))
return FindIEnumerable(sequenceType.BaseType);
return null;
}
private static string BuildSimplifiedName(Type type)
{
var assemblyName = type.Assembly.GetName().Name;
if (type.IsGenericType)
{
var genericTypeName = type.GetGenericTypeDefinition().FullName!;
var backtickIndex = genericTypeName.IndexOf('`');
var typeNameWithoutArity = genericTypeName[..backtickIndex];
var arity = genericTypeName[backtickIndex..];
var genericArguments = type.GetGenericArguments();
var simplifiedGenericArguments = genericArguments.Select(BuildSimplifiedName);
return $"{typeNameWithoutArity}{arity}[[{string.Join("],[", simplifiedGenericArguments)}]], {assemblyName}";
}
var typeName = type.FullName;
return $"{typeName}, {assemblyName}";
}
}
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,44 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Api.Client.Converters;
using Refit;
namespace Elsa.Api.Client;
/// <summary>
/// Contains helper methods for configuring Refit settings.
/// </summary>
public static class RefitSettingsHelper
{
/// <summary>
/// Creates a <see cref="RefitSettings"/> instance configured for Elsa.
/// </summary>
public static RefitSettings CreateRefitSettings(IServiceProvider serviceProvider, Action<IServiceProvider, JsonSerializerOptions>? configureJsonSerializerOptions = null)
{
var settings = new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(CreateJsonSerializerOptions(serviceProvider, configureJsonSerializerOptions))
};
return settings;
}
/// <summary>
/// Creates a <see cref="JsonSerializerOptions"/> instance configured for Elsa.
/// </summary>
public static JsonSerializerOptions CreateJsonSerializerOptions(IServiceProvider serviceProvider, Action<IServiceProvider, JsonSerializerOptions>? configureJsonSerializerOptions = null)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new VersionOptionsJsonConverter());
options.Converters.Add(new TypeJsonConverter());
configureJsonSerializerOptions?.Invoke(serviceProvider, options);
return options;
}
}
@@ -0,0 +1,30 @@
using System.Net.Http.Headers;
using Elsa.Api.Client.Options;
using Microsoft.Extensions.Options;
namespace Elsa.Api.Client.HttpMessageHandlers;
/// <summary>
/// An <see cref="HttpMessageHandler"/> that configures the outgoing HTTP request to use an API key as the authorization header.
/// </summary>
public class ApiKeyHttpMessageHandler : DelegatingHandler
{
private readonly ElsaClientOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyHttpMessageHandler"/> class.
/// </summary>
public ApiKeyHttpMessageHandler(IOptions<ElsaClientOptions> options)
{
_options = options.Value;
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var apiKey = _options.ApiKey;
request.Headers.Authorization = new AuthenticationHeaderValue("ApiKey", apiKey);
return await base.SendAsync(request, cancellationToken);
}
}
@@ -0,0 +1,7 @@
using System.ComponentModel;
// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices;
[EditorBrowsable(EditorBrowsableState.Never)]
internal sealed class IsExternalInit;
@@ -0,0 +1,48 @@
using System.Text.Json;
using Elsa.Api.Client.HttpMessageHandlers;
using Microsoft.Extensions.DependencyInjection;
using Polly;
namespace Elsa.Api.Client.Options;
/// <summary>
/// Represents options for building an Elsa API client.
/// </summary>
public class ElsaClientBuilderOptions
{
/// <summary>
/// Gets or sets the base address of the Elsa server.
/// </summary>
public Uri BaseAddress { get; set; } = default!;
/// <summary>
/// Gets or sets the API key function to use when authenticating with the Elsa server.
/// </summary>
public string? ApiKey { get; set; }
/// <summary>
/// A <see cref="DelegatingHandler"/> type that can be used to authenticate with the Elsa server.
/// Defaults to <see cref="ApiKeyHttpMessageHandler"/>.
/// </summary>
public Type AuthenticationHandler { get; set; } = typeof(ApiKeyHttpMessageHandler);
/// <summary>
/// Gets or sets a delegate that can be used to configure the HTTP client.
/// </summary>
public Action<IServiceProvider, HttpClient>? ConfigureHttpClient { get; set; }
/// <summary>
/// Gets or sets a delegate that can be used to configure the HTTP client builder.
/// </summary>
public Action<IHttpClientBuilder> ConfigureHttpClientBuilder { get; set; } = _ => { };
/// <summary>
/// Gets or sets a delegate that can be used to configure the retry policy.
/// </summary>
public Action<IHttpClientBuilder>? ConfigureRetryPolicy { get; set; } = builder => builder.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
/// <summary>
/// Gets or sets a delegate that can be used to configure the JSON serializer options.
/// </summary>
public Action<IServiceProvider, JsonSerializerOptions>? ConfigureJsonSerializerOptions { get; set; }
}
@@ -0,0 +1,22 @@
namespace Elsa.Api.Client.Options;
/// <summary>
/// Represents options for the Elsa client.
/// </summary>
public class ElsaClientOptions
{
/// <summary>
/// Gets or sets the base address of the Elsa server.
/// </summary>
public Uri BaseAddress { get; set; } = default!;
/// <summary>
/// Gets or sets the API key function to use when authenticating with the Elsa server.
/// </summary>
public string? ApiKey { get; set; }
/// <summary>
/// Gets or sets a delegate that can be used to configure the HTTP client.
/// </summary>
public Action<IServiceProvider, HttpClient>? ConfigureHttpClient { get; set; }
}
@@ -0,0 +1,10 @@
using Elsa.Api.Client.Resources.ActivityExecutions.Models;
namespace Elsa.Api.Client.RealTime.Messages;
/// <summary>
/// Contains information about the activity executed event.
/// </summary>
/// <param name="ActivityId">The ID of the activity.</param>
/// <param name="Stats">Execution stats about the activity.</param>
public record ActivityExecutedMessage(string ActivityId, ActivityExecutionStats Stats);
@@ -0,0 +1,10 @@
using Elsa.Api.Client.Resources.ActivityExecutions.Models;
namespace Elsa.Api.Client.RealTime.Messages;
/// <summary>
/// Contains information about the activity executing event.
/// </summary>
/// <param name="ActivityId">The ID of the activity.</param>
/// <param name="Stats">Execution stats about the activity.</param>
public record ActivityExecutingMessage(string ActivityId, ActivityExecutionStats Stats);
@@ -0,0 +1,9 @@
using Elsa.Api.Client.Resources.ActivityExecutions.Models;
namespace Elsa.Api.Client.RealTime.Messages;
/// <summary>
/// Contains information about the activity execution logs updated event.
/// </summary>
/// <param name="Stats">Execution stats about a set of activities.</param>
public record ActivityExecutionLogUpdatedMessage(ICollection<ActivityExecutionStats> Stats);
@@ -0,0 +1,6 @@
namespace Elsa.Api.Client.RealTime.Messages;
/// <summary>
/// Represents a message that is sent when the workflow execution log is updated.
/// </summary>
public record WorkflowExecutionLogUpdatedMessage();
@@ -0,0 +1,7 @@
namespace Elsa.Api.Client.RealTime.Messages;
/// <summary>
/// Represents a message that is sent when the workflow instance is updated.
/// </summary>
/// <param name="WorkflowInstanceId">The ID of the workflow instance that was updated.</param>
public record WorkflowInstanceUpdatedMessage(string WorkflowInstanceId);
@@ -0,0 +1,22 @@
using Elsa.Api.Client.Resources.ActivityDescriptorOptions.Requests;
using Elsa.Api.Client.Resources.ActivityDescriptorOptions.Responses;
using Refit;
namespace Elsa.Api.Client.Resources.ActivityDescriptorOptions.Contracts;
/// <summary>
/// Represents a client for the workflow definitions API.
/// </summary>
public interface IActivityDescriptorOptionsApi
{
/// <summary>
/// Lists activity descriptors options.
/// </summary>
/// <param name="activityTypeName">The TypeName of the activity </param>
/// <param name="propertyName">The name of the property</param>
/// <param name="request">The context request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The response containing the activity descriptors.</returns>
[Post("/descriptors/activities/{activityTypeName}/options/{propertyName}")]
Task<GetActivityDescriptorOptionsResponse> GetAsync(string activityTypeName, string propertyName, [Body]GetActivityDescriptorOptionsRequest request, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,12 @@
namespace Elsa.Api.Client.Resources.ActivityDescriptorOptions.Requests;
/// <summary>
/// Represents a request to Get Activity Descriptor Options.
/// </summary>
public class GetActivityDescriptorOptionsRequest
{
/// <summary>
/// Object context use to pass custom information
/// </summary>
public object? Context { get; set; }
}
@@ -0,0 +1,6 @@
namespace Elsa.Api.Client.Resources.ActivityDescriptorOptions.Responses;
/// <summary>
/// Represents a response from get activity descriptors Options.
/// </summary>
/// <param name="Items">The options elements.</param>
public record GetActivityDescriptorOptionsResponse(IDictionary<string,object> Items);
@@ -0,0 +1,20 @@
using Elsa.Api.Client.Resources.ActivityDescriptors.Requests;
using Elsa.Api.Client.Resources.ActivityDescriptors.Responses;
using Refit;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Contracts;
/// <summary>
/// Represents a client for the workflow definitions API.
/// </summary>
public interface IActivityDescriptorsApi
{
/// <summary>
/// Lists activity descriptors.
/// </summary>
/// <param name="request">The request containing options for listing activity descriptors.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The response containing the activity descriptors.</returns>
[Get("/descriptors/activities")]
Task<ListActivityDescriptorsResponse> ListAsync([Query]ListActivityDescriptorsRequest request, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,27 @@
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Enums;
/// <summary>
/// Represents the kind of an activity, which affects how it is executed.
/// </summary>
public enum ActivityKind
{
/// <summary>
/// Always run synchronously.
/// </summary>
Action,
/// <summary>
/// Can be used to trigger new workflows.
/// </summary>
Trigger,
/// <summary>
/// Always run asynchronously.
/// </summary>
Job,
/// <summary>
/// Run synchronously by default (like <see cref="Action"/>, but can be configured to run asynchronously (like <see cref="Job"/>).
/// </summary>
Task
}
@@ -0,0 +1,17 @@
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Enums;
/// <summary>
/// The type of a port.
/// </summary>
public enum PortType
{
/// <summary>
/// A port that is embedded in the activity.
/// </summary>
Embedded,
/// <summary>
/// A flow port.
/// </summary>
Flow
}
@@ -0,0 +1,94 @@
using Elsa.Api.Client.Resources.ActivityDescriptors.Enums;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Models;
/// <summary>
/// A descriptor of an activity type.
/// </summary>
public record ActivityDescriptor
{
/// <summary>
/// The fully qualified name of the activity type.
/// </summary>
public string TypeName { get; init; } = default!;
/// <summary>
/// The namespace of the activity type.
/// </summary>
public string Namespace { get; init; } = default!;
/// <summary>
/// The name of the activity type.
/// </summary>
public string Name { get; init; } = default!;
/// <summary>
/// The version of the activity type.
/// </summary>
public int Version { get; init; }
/// <summary>
/// The category of the activity type.
/// </summary>
public string Category { get; init; } = default!;
/// <summary>
/// The display name of the activity type.
/// </summary>
public string? DisplayName { get; init; }
/// <summary>
/// The description of the activity type.
/// </summary>
public string? Description { get; init; }
/// <summary>
/// The input properties of the activity type.
/// </summary>
public IReadOnlyCollection<InputDescriptor> Inputs { get; init; } = new List<InputDescriptor>();
/// <summary>
/// The output properties of the activity type.
/// </summary>
public IReadOnlyCollection<OutputDescriptor> Outputs { get; init; } = new List<OutputDescriptor>();
/// <summary>
/// The kind of activity.
/// </summary>
public ActivityKind Kind { get; set; } = ActivityKind.Action;
/// <summary>
/// The ports of the activity type.
/// </summary>
public IReadOnlyCollection<Port> Ports { get; init; } = new List<Port>();
/// <summary>
/// The custom properties of the activity type.
/// </summary>
public IReadOnlyDictionary<string, object> CustomProperties { get; set; } = new Dictionary<string, object>();
/// <summary>
/// The properties to set when constructing an activity in the designer.
/// </summary>
public IDictionary<string, object> ConstructionProperties { get; set; } = new Dictionary<string, object>();
/// <summary>
/// A value indicating whether this activity is a container of child activities.
/// </summary>
public bool IsContainer { get; set; }
/// <summary>
/// Whether this activity type is selectable from activity pickers.
/// </summary>
public bool IsBrowsable { get; set; }
/// <summary>
/// Whether this activity type is a start activity.
/// </summary>
public bool IsStart { get; set; }
/// <summary>
/// Whether this activity type is a terminal activity.
/// </summary>
public bool IsTerminal { get; set; }
}
@@ -0,0 +1,56 @@
using JetBrains.Annotations;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Models;
/// <summary>
/// A descriptor of an activity's input property.
/// </summary>
[PublicAPI]
public class InputDescriptor : PropertyDescriptor
{
/// <inheritdoc />
public InputDescriptor()
{
}
/// <summary>
/// True if the property is wrapped with an <c>"Input{T}"</c> type, false otherwise.
/// </summary>
public bool IsWrapped { get; set; }
/// <summary>
/// A string value that hints at what UI control might be used to render in a UI tool.
/// </summary>
public string UIHint { get; set; } = default!;
/// <summary>
/// The category to which this input belongs. Can be used by UI to e.g. render different inputs in different tabs.
/// </summary>
public string? Category { get; set; }
/// <summary>
/// The default value.
/// </summary>
public object? DefaultValue { get; set; }
/// <summary>
/// The default syntax.
/// </summary>
public string? DefaultSyntax { get; set; }
/// <summary>
/// True if the input is readonly, false otherwise.
/// </summary>
public bool? IsReadOnly { get; set; }
/// <summary>
/// The storage driver type to use for persistence.
/// If no driver is specified, the referenced memory block will remain in memory for as long as the expression execution context exists.
/// </summary>
public string? StorageDriverType { get; set; }
/// <summary>
/// A dictionary of UI specifications to be used by the UI.
/// </summary>
public IDictionary<string, object>? UISpecifications { get; set; }
}
@@ -0,0 +1,11 @@
using JetBrains.Annotations;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Models;
/// <summary>
/// A descriptor of an activity's output property.
/// </summary>
[PublicAPI]
public class OutputDescriptor : PropertyDescriptor
{
}
@@ -0,0 +1,29 @@
using Elsa.Api.Client.Resources.ActivityDescriptors.Enums;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Models;
/// <summary>
/// Represents a port on an activity.
/// </summary>
public class Port
{
/// <summary>
/// Gets or sets the name of the port.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// Gets or sets the display name of the port.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// Gets or sets the type of the port.
/// </summary>
public PortType Type { get; set; }
/// <summary>
/// Gets or sets the visibility of the port.
/// </summary>
public bool? IsBrowsable { get; set; } = true;
}
@@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Models;
/// <summary>
/// A base type for <see cref="InputDescriptor"/> and <see cref="OutputDescriptor"/>.
/// </summary>
[PublicAPI]
public abstract class PropertyDescriptor
{
/// <summary>
/// The name.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// The .NET type.
/// </summary>
[JsonPropertyName("typeName")]
public string TypeName { get; set; } = default!;
/// <summary>
/// The user friendly name of the input. Used by UI tools.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// The user friendly description of the input. Used by UI tools.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The order in which this input should be displayed by UI tools.
/// </summary>
public float Order { get; set; }
/// <summary>
/// True if this input should be displayed by UI tools, false otherwise.
/// </summary>
public bool? IsBrowsable { get; set; }
/// <summary>
/// True if this input property is synthetic, which means it does not exist physically on the activity's .NET type.
/// </summary>
public bool IsSynthetic { get; set; }
}
@@ -0,0 +1,14 @@
using Refit;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Requests;
/// <summary>
/// Represents a request to list activity descriptors.
/// </summary>
public class ListActivityDescriptorsRequest
{
/// <summary>
/// Whether to refresh the activity descriptors or not.
/// </summary>
[Query] public bool Refresh { get; set; }
}
@@ -0,0 +1,10 @@
using Elsa.Api.Client.Resources.ActivityDescriptors.Models;
namespace Elsa.Api.Client.Resources.ActivityDescriptors.Responses;
/// <summary>
/// Represents a response from listing activity descriptors.
/// </summary>
/// <param name="Items">The activity descriptors.</param>
/// <param name="Count">The total number of activity descriptors.</param>
public record ListActivityDescriptorsResponse(ICollection<ActivityDescriptor> Items, int Count);
@@ -0,0 +1,48 @@
using Elsa.Api.Client.Resources.ActivityExecutions.Models;
using Elsa.Api.Client.Resources.ActivityExecutions.Requests;
using Elsa.Api.Client.Shared.Models;
using Refit;
namespace Elsa.Api.Client.Resources.ActivityExecutions.Contracts;
/// <summary>
/// Represents a client for the activity executions API.
/// </summary>
public interface IActivityExecutionsApi
{
/// <summary>
/// Gets a report of activity executions for a given workflow instance.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The response containing the activity execution report.</returns>
[Post("/activity-executions/report")]
Task<ActivityExecutionReport> GetReportAsync([Body] GetActivityExecutionReportRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Lists activity executions for a given activity in a workflow instance.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The response containing a list of activity executions.</returns>
[Get("/activity-executions/list")]
Task<ListResponse<ActivityExecutionRecord>> ListAsync(ListActivityExecutionsRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Lists activity execution summaries for a given activity in a workflow instance.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The response containing a list of activity execution summaries.</returns>
[Get("/activity-execution-summaries/list")]
Task<ListResponse<ActivityExecutionRecordSummary>> ListSummariesAsync(ListActivityExecutionsRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a single activity execution by ID.
/// </summary>
/// <param name="id">The ID of the activity execution.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The activity execution.</returns>
[Get("/activity-executions/{id}")]
Task<ActivityExecutionRecord?> GetAsync(string id, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,93 @@
using Elsa.Api.Client.Resources.WorkflowInstances.Models;
using Elsa.Api.Client.Shared.Models;
using JetBrains.Annotations;
namespace Elsa.Api.Client.Resources.ActivityExecutions.Models;
/// <summary>
/// Represents a single workflow execution, associated with an individual activity instance.
/// </summary>
[UsedImplicitly]
public class ActivityExecutionRecord : Entity
{
/// <summary>
/// Gets or sets the workflow instance ID.
/// </summary>
public string WorkflowInstanceId { get; set; } = null!;
/// <summary>
/// Gets or sets the activity ID.
/// </summary>
public string ActivityId { get; set; } = null!;
/// <summary>
/// Gets or sets the activity node ID.
/// </summary>
public string ActivityNodeId { get; set; } = null!;
/// <summary>
/// The type of the activity.
/// </summary>
public string ActivityType { get; set; } = null!;
/// <summary>
/// The version of the activity type.
/// </summary>
public int ActivityTypeVersion { get; set; }
/// <summary>
/// The name of the activity.
/// </summary>
public string? ActivityName { get; set; } = null!;
/// <summary>
/// The state of the activity at the time this record is created or last updated.
/// </summary>
public IDictionary<string, object?>? ActivityState { get; set; }
/// <summary>
/// Any additional payload associated with the log record.
/// </summary>
public IDictionary<string, object?>? Payload { get; set; }
/// <summary>
/// Any outputs provided by the activity.
/// </summary>
public IDictionary<string, object?>? Outputs { get; set; }
/// <summary>
/// Any properties provided by the activity.
/// </summary>
public IDictionary<string, object?> Properties { get; set; } = new Dictionary<string, object?>();
/// <summary>
/// Any metadata provided by the activity. In contrast to the <see cref="Properties"/> property, this metadata is also persisted as part of the activity execution summary record.
/// It is therefore not suitable for larger payloads.
/// </summary>
public IDictionary<string, object?> Metadata { get; set; } = new Dictionary<string, object?>();
/// <summary>
/// Gets or sets the exception that occurred during the activity execution.
/// </summary>
public ExceptionState? Exception { get; set; }
/// <summary>
/// Gets or sets the time at which the activity execution began.
/// </summary>
public DateTimeOffset StartedAt { get; set; } = default!;
/// <summary>
/// Gets or sets whether the activity has any bookmarks.
/// </summary>
public bool HasBookmarks { get; set; }
/// <summary>
/// Gets or sets the status of the activity.
/// </summary>
public ActivityStatus Status { get; set; }
/// <summary>
/// Gets or sets the time at which the activity execution completed.
/// </summary>
public DateTimeOffset? CompletedAt { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More