Merge remote-tracking branch 'origin/rc/3.4.0'
This commit is contained in:
commit
2eec542f50
2
.github/workflows/elsa-server-and-studio.yml
vendored
2
.github/workflows/elsa-server-and-studio.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elsaworkflows/elsa-server-and-studio-v3-3-0-preview
|
||||
elsaworkflows/elsa-server-and-studio-v3-4-0-preview
|
||||
flavor: |
|
||||
latest=true
|
||||
# generate Docker tags based on the following events/attributes
|
||||
|
|
|
|||
2
.github/workflows/elsa-server.yml
vendored
2
.github/workflows/elsa-server.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elsaworkflows/elsa-server-v3-3-0-preview
|
||||
elsaworkflows/elsa-server-v3-4-0-preview
|
||||
flavor: |
|
||||
latest=true
|
||||
# generate Docker tags based on the following events/attributes
|
||||
|
|
|
|||
2
.github/workflows/elsa-studio.yml
vendored
2
.github/workflows/elsa-studio.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elsaworkflows/elsa-studio-v3-3-0-preview
|
||||
elsaworkflows/elsa-studio-v3-4-0-preview
|
||||
flavor: |
|
||||
latest=true
|
||||
# generate Docker tags based on the following events/attributes
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ namespace Elsa.Server.Web.Messages;
|
|||
|
||||
public class OrderReceived
|
||||
{
|
||||
public string OrderId { get; set; } = default!;
|
||||
public string OrderId { get; set; } = null!;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -51,7 +52,6 @@ using Elsa.Workflows.CommitStates.Strategies;
|
|||
using Elsa.Workflows.IncidentStrategies;
|
||||
using Elsa.Workflows.LogPersistence;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Compression;
|
||||
using Elsa.Workflows.Management.Stores;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Options;
|
||||
|
|
@ -86,7 +86,7 @@ using StackExchange.Redis;
|
|||
|
||||
// ReSharper disable RedundantAssignment
|
||||
const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore;
|
||||
const bool useDbContextPooling = true;
|
||||
const bool useDbContextPooling = false;
|
||||
const bool useHangfire = false;
|
||||
const bool useQuartz = true;
|
||||
const bool useMassTransit = true;
|
||||
|
|
@ -107,7 +107,7 @@ const bool disableVariableWrappers = false;
|
|||
const bool disableVariableCopying = false;
|
||||
const bool useManualOtelInstrumentation = false;
|
||||
|
||||
ObjectConverter.StrictMode = true; // Default.
|
||||
ObjectConverter.StrictMode = false;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var services = builder.Services;
|
||||
|
|
@ -560,6 +560,7 @@ services
|
|||
elsa.UseMassTransit(massTransit =>
|
||||
{
|
||||
massTransit.DisableConsumers = appRole == ApplicationRole.Api;
|
||||
massTransit.AddMessageType<OrderReceived>();
|
||||
|
||||
if (massTransitBroker == MassTransitBroker.AzureServiceBus)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -80,6 +81,7 @@ public static class DependencyInjectionExtensions
|
|||
services.AddApi<IJavaScriptApi>(builderOptions);
|
||||
services.AddApi<IExpressionDescriptorsApi>(builderOptions);
|
||||
services.AddApi<IWorkflowContextProviderDescriptorsApi>(builderOptions);
|
||||
services.AddApi<IAlterationsApi>(builderOptions);
|
||||
services.AddApi<ITasksApi>(builderOptions);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Elsa.Api.Client.Resources.Alterations.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for all alteration classes
|
||||
/// </summary>
|
||||
public interface IAlteration;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Elsa.Api.Client.Resources.Alterations.Contracts;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.Alterations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A base class for all IAlterations.
|
||||
/// </summary>
|
||||
public abstract class AlterationBase : IAlteration;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Elsa.Api.Client.Resources.Alterations.Contracts;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Api.Client.Resources.Alterations.Enums;
|
||||
using Elsa.Api.Client.Shared.Models;
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ public class AlterationPlan : Entity
|
|||
/// <summary>
|
||||
/// The alterations to be applied.
|
||||
/// </summary>
|
||||
public ICollection<IAlteration> Alterations { get; set; } = new List<IAlteration>();
|
||||
public ICollection<JsonObject> Alterations { get; set; } = new List<JsonObject>();
|
||||
|
||||
/// <summary>
|
||||
/// The IDs of the workflow instances that this plan applies to.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Elsa.Api.Client.Resources.Alterations.Contracts;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.Alterations.Models;
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ public class AlterationPlanParams
|
|||
/// <summary>
|
||||
/// The alterations to be applied.
|
||||
/// </summary>
|
||||
public ICollection<IAlteration> Alterations { get; set; } = new List<IAlteration>();
|
||||
public ICollection<JsonObject> Alterations { get; set; } = new List<JsonObject>();
|
||||
|
||||
/// <summary>
|
||||
/// The IDs of the workflow instances that this plan applies to.
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Elsa.Api.Client.Resources.Alterations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the workflow instances in an alteration plan.
|
||||
/// </summary>
|
||||
public class Cancel : AlterationBase;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
namespace Elsa.Api.Client.Resources.Alterations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a workflow instance activity during an alteration
|
||||
/// </summary>
|
||||
public class CancelActivity : AlterationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the activity to be cancelled. If not specified, the activity instance ID will be used.
|
||||
/// </summary>
|
||||
public string? ActivityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the activity instance to be cancelled. If specified, overrides <see cref="ActivityId"/>.
|
||||
/// </summary>
|
||||
public string? ActivityInstanceId { get; set; }
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
namespace Elsa.Api.Client.Resources.Alterations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Migrates a workflow instance to a newer version in an alteration.
|
||||
/// </summary>
|
||||
public class Migrate : AlterationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The target version to upgrade to.
|
||||
/// </summary>
|
||||
public int TargetVersion { get; set; }
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
namespace Elsa.Api.Client.Resources.Alterations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Modifies a variable in a workflow instance alteration
|
||||
/// </summary>
|
||||
public class ModifyVariable : AlterationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the variable to modify.
|
||||
/// </summary>
|
||||
public string VariableId { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The new value of the variable.
|
||||
/// </summary>
|
||||
public object? Value { get; set; }
|
||||
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
namespace Elsa.Api.Client.Resources.Alterations.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Schedules an activity for execution in an alteration.
|
||||
/// </summary>
|
||||
public class ScheduleActivity : AlterationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the next activity to be scheduled. If not specified, the ActivityInstanceId will be used.
|
||||
/// </summary>
|
||||
public string? ActivityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the activity instance to be scheduled. If not specified, the ActivityId will be used.
|
||||
/// </summary>
|
||||
public string? ActivityInstanceId { get; set; }
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Elsa.Api.Client.Resources.Alterations.Contracts;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.Alterations.Responses;
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ public class RunRequest
|
|||
/// <summary>
|
||||
/// The alterations to be applied.
|
||||
/// </summary>
|
||||
public ICollection<IAlteration> Alterations { get; set; } = new List<IAlteration>();
|
||||
public ICollection<JsonObject> Alterations { get; set; } = new List<JsonObject>();
|
||||
|
||||
/// <summary>
|
||||
/// The IDs of the workflow instances that this plan applies to.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
using Elsa.Alterations.Bookmarks;
|
||||
using Elsa.Alterations.Core.Notifications;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Requests;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Alterations.Handlers;
|
||||
|
|
@ -13,7 +12,7 @@ namespace Elsa.Alterations.Handlers;
|
|||
/// Handles <see cref="AlterationPlanCompleted"/> notifications and triggers any workflows that are waiting for the plan to complete.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatcher) : INotificationHandler<AlterationPlanCompleted>
|
||||
public class AlterationPlanCompletedHandler(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler<AlterationPlanCompleted>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(AlterationPlanCompleted notification, CancellationToken cancellationToken)
|
||||
|
|
@ -21,7 +20,12 @@ public class AlterationPlanCompletedHandler(IWorkflowDispatcher workflowDispatch
|
|||
// Trigger any workflow instances that are waiting for the plan to complete.
|
||||
var planId = notification.Plan.Id;
|
||||
var bookmarkPayload = new AlterationPlanCompletedPayload(planId);
|
||||
var triggerRequest = new DispatchTriggerWorkflowsRequest(ActivityTypeNameHelper.GenerateTypeName<Activities.AlterationPlanCompleted>(), bookmarkPayload);
|
||||
await workflowDispatcher.DispatchAsync(triggerRequest, cancellationToken);
|
||||
var activityTypeName = ActivityTypeNameHelper.GenerateTypeName<Activities.AlterationPlanCompleted>();
|
||||
var item = new NewBookmarkQueueItem
|
||||
{
|
||||
ActivityTypeName = activityTypeName,
|
||||
StimulusHash = stimulusHasher.Hash(activityTypeName, bookmarkPayload)
|
||||
};
|
||||
await bookmarkQueue.EnqueueAsync(item, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ public class AlterationPlanManager(IAlterationPlanStore planStore, IAlterationJo
|
|||
var jobFilter = new AlterationJobFilter
|
||||
{
|
||||
PlanId = planId,
|
||||
Statuses = new[] { AlterationJobStatus.Pending, AlterationJobStatus.Running }
|
||||
Statuses = [AlterationJobStatus.Pending, AlterationJobStatus.Running]
|
||||
};
|
||||
|
||||
return await jobStore.CountAsync(jobFilter, cancellationToken) == 0;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class DefaultAlterationPlanScheduler : IAlterationPlanScheduler
|
|||
var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Published, cancellationToken);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new Exception($"Workflow definition with ID '{definitionId}' not found");
|
||||
throw new($"Workflow definition with ID '{definitionId}' not found");
|
||||
|
||||
var serializedPlan = _jsonSerializer.Serialize(planParams);
|
||||
var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
|
||||
namespace Elsa.Workflows.Management.Compression;
|
||||
namespace Elsa.Common.Codecs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a GZip compression strategy.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management.Compression;
|
||||
namespace Elsa.Common.Codecs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a compression strategy that does not compress or decompress the input.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Text;
|
||||
using IronCompress;
|
||||
|
||||
namespace Elsa.Workflows.Management.Compression;
|
||||
namespace Elsa.Common.Codecs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a ZSTD compression strategy.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management;
|
||||
namespace Elsa.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a compression strategy.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Workflows.Management;
|
||||
namespace Elsa.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a <see cref="ICompressionCodec"/> from its name.
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Cronos" />
|
||||
<PackageReference Include="DistributedLock.Core" />
|
||||
<PackageReference Include="IronCompress" />
|
||||
<PackageReference Include="LinqKit.Core" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
|
|
|
|||
21
src/modules/Elsa.Common/Features/CompressionFeature.cs
Normal file
21
src/modules/Elsa.Common/Features/CompressionFeature.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Elsa.Common.Codecs;
|
||||
using Elsa.Common.Services;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Common.Features;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class CompressionFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
public override void Apply()
|
||||
{
|
||||
Services
|
||||
.AddSingleton<ICompressionCodecResolver, CompressionCodecResolver>()
|
||||
.AddSingleton<ICompressionCodec, None>()
|
||||
.AddSingleton<ICompressionCodec, GZip>()
|
||||
.AddSingleton<ICompressionCodec, Zstd>();
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ public class Tenant : Entity
|
|||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration.
|
||||
|
|
@ -22,7 +22,7 @@ public class Tenant : Entity
|
|||
|
||||
public static readonly Tenant Default = new()
|
||||
{
|
||||
Id = string.Empty,
|
||||
Id = null!,
|
||||
Name = "Default"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Common.Multitenancy;
|
||||
|
|
@ -19,7 +20,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
public async Task<Tenant?> FindAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
|
||||
return dictionary.TryGetValue(id, out var tenant) ? tenant : null;
|
||||
return dictionary.TryGetValue(id.EmptyIfNull(), out var tenant) ? tenant : null;
|
||||
}
|
||||
|
||||
public async Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
|
||||
|
|
@ -31,7 +32,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
public async Task<Tenant> GetAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
|
||||
return dictionary[id];
|
||||
return dictionary[id.EmptyIfNull()];
|
||||
}
|
||||
|
||||
public async Task<Tenant> GetAsync(TenantFilter filter, CancellationToken cancellationToken = default)
|
||||
|
|
@ -76,7 +77,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
var tenantsProvider = scope.ServiceProvider.GetRequiredService<ITenantsProvider>();
|
||||
var currentTenants = await GetTenantsDictionaryAsync(cancellationToken);
|
||||
var currentTenantIds = currentTenants.Keys;
|
||||
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id);
|
||||
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id.EmptyIfNull());
|
||||
var newTenantIds = newTenants.Keys;
|
||||
var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray();
|
||||
var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray();
|
||||
|
|
@ -129,7 +130,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scope = tenantScopeFactory.CreateScope(tenant);
|
||||
_tenantsDictionary![tenant.Id] = tenant;
|
||||
_tenantsDictionary![tenant.Id.EmptyIfNull()] = tenant;
|
||||
_tenantScopesDictionary![tenant] = scope;
|
||||
|
||||
using (tenantAccessor.PushContext(tenant))
|
||||
|
|
@ -140,7 +141,7 @@ public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScop
|
|||
{
|
||||
if (_tenantScopesDictionary!.Remove(tenant, out var scope))
|
||||
{
|
||||
_tenantsDictionary!.Remove(tenant.Id, out _);
|
||||
_tenantsDictionary!.Remove(tenant.Id.EmptyIfNull(), out _);
|
||||
|
||||
using (tenantAccessor.PushContext(tenant))
|
||||
await tenantEvents.TenantDeactivatedAsync(new(tenant, scope, cancellationToken));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public class TenantFilter
|
|||
/// <summary>
|
||||
/// Gets or sets the tenant ID to filter for.
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Applies the filter to the specified queryable.
|
||||
|
|
@ -20,7 +20,7 @@ public class TenantFilter
|
|||
/// <returns>The filtered queryable.</returns>
|
||||
public IQueryable<Tenant> Apply(IQueryable<Tenant> queryable)
|
||||
{
|
||||
if (Id != null) queryable = queryable.Where(x => x.Id == Id);
|
||||
queryable = queryable.Where(x => x.Id == Id);
|
||||
|
||||
return queryable;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Elsa.Workflows.Management.Compression;
|
||||
using Elsa.Common.Codecs;
|
||||
|
||||
namespace Elsa.Workflows.Management.Services;
|
||||
namespace Elsa.Common.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class CompressionCodecResolver : ICompressionCodecResolver
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Open.Linq.AsyncExtensions" />
|
||||
<PackageReference Include="Scrutor" />
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using Elsa.Common.Entities;
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.EntityFrameworkCore;
|
||||
|
|
@ -50,7 +50,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema
|
|||
var tenantId = tenantAccessor?.Tenant?.Id;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tenantId))
|
||||
TenantId = tenantId;
|
||||
TenantId = tenantId.NullIfEmpty();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,5 @@ public class ElsaDbContextOptionsExtension : IDbContextOptionsExtension
|
|||
/// <inheritdoc />
|
||||
public void Validate(IDbContextOptions options)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Common.Entities;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace Elsa.EntityFrameworkCore.EntityHandlers;
|
||||
|
|
@ -12,7 +13,7 @@ public class ApplyTenantId : IEntitySavingHandler
|
|||
public ValueTask HandleAsync(ElsaDbContextBase dbContext, EntityEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (entry.Entity is Entity entity)
|
||||
entity.TenantId = dbContext.TenantId;
|
||||
entity.TenantId = dbContext.TenantId.NullIfEmpty();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Linq.Expressions;
|
||||
using Elsa.Common.Entities;
|
||||
using Elsa.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
|
|
@ -17,8 +18,9 @@ public class SetTenantIdFilter : IEntityModelCreatingHandler
|
|||
if (!entityType.ClrType.IsAssignableTo(typeof(Entity)))
|
||||
return;
|
||||
|
||||
var tenantId = dbContext.TenantId.NullIfEmpty();
|
||||
var parameter = Expression.Parameter(entityType.ClrType);
|
||||
Expression<Func<Entity, bool>> filterExpr = entity => dbContext.TenantId == entity.TenantId;
|
||||
Expression<Func<Entity, bool>> filterExpr = entity => entity.TenantId == tenantId;
|
||||
var body = ReplacingExpressionVisitor.Replace(filterExpr.Parameters[0], parameter, filterExpr.Body);
|
||||
var lambdaExpression = Expression.Lambda(body, parameter);
|
||||
|
||||
|
|
|
|||
|
|
@ -154,9 +154,7 @@ public static class BulkUpsertExtensions
|
|||
var entityType = dbContext.Model.FindEntityType(typeof(TEntity))!;
|
||||
var tableName = entityType.GetTableName();
|
||||
var storeObject = StoreObjectIdentifier.Table(tableName!, entityType.GetSchema());
|
||||
|
||||
var props = entityType.GetProperties().ToList();
|
||||
|
||||
var keyProp = entityType.FindProperty(keySelector.GetMemberAccess().Name)!;
|
||||
var keyColumnName = keyProp.GetColumnName(storeObject);
|
||||
var columnNames = props
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ public abstract class PersistenceFeatureBase<TFeature, TDbContext>(IModule modul
|
|||
Services.AddPooledDbContextFactory<TDbContext>(setup);
|
||||
else
|
||||
Services.AddDbContextFactory<TDbContext>(setup, DbContextFactoryLifetime);
|
||||
|
||||
Services.Decorate<IDbContextFactory<TDbContext>, TenantAwareDbContextFactory<TDbContext>>();
|
||||
}
|
||||
|
||||
protected virtual void ConfigureMigrations()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System.Linq.Expressions;
|
||||
using Elsa.Common.Entities;
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.EntityFrameworkCore.Extensions;
|
||||
using Elsa.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -76,7 +78,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public async Task AddManyAsync(
|
||||
IEnumerable<TEntity> entities,
|
||||
Func<TDbContext, TEntity, CancellationToken, ValueTask>? onSaving = default,
|
||||
Func<TDbContext, TEntity, CancellationToken, ValueTask>? onSaving = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entityList = entities.ToList();
|
||||
|
|
@ -151,7 +153,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <param name="entities">The entities to save.</param>
|
||||
/// <param name="keySelector">The key selector to get the primary key property.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public async Task SaveManyAsync(IEnumerable<TEntity> entities, Expression<Func<TEntity, string>> keySelector, CancellationToken cancellationToken = default) => await SaveManyAsync(entities, keySelector, default, cancellationToken);
|
||||
public async Task SaveManyAsync(IEnumerable<TEntity> entities, Expression<Func<TEntity, string>> keySelector, CancellationToken cancellationToken = default) => await SaveManyAsync(entities, keySelector, null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the specified entities.
|
||||
|
|
@ -163,7 +165,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
public async Task SaveManyAsync(
|
||||
IEnumerable<TEntity> entities,
|
||||
Expression<Func<TEntity, string>> keySelector,
|
||||
Func<TDbContext, TEntity, CancellationToken, ValueTask>? onSaving = default,
|
||||
Func<TDbContext, TEntity, CancellationToken, ValueTask>? onSaving = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entityList = entities.ToList();
|
||||
|
|
@ -179,6 +181,14 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
await Task.WhenAll(savingTasks);
|
||||
}
|
||||
|
||||
// When doing a custom SQL query (Bulk Upsert), none of the installed query filters will be applied. Hence, we are assigning the current tenant ID explicitly.
|
||||
var tenantId = serviceProvider.GetRequiredService<ITenantAccessor>().Tenant?.Id.NullIfEmpty();
|
||||
foreach (var entity in entityList)
|
||||
{
|
||||
if (entity is Entity entityWithTenant)
|
||||
entityWithTenant.TenantId = tenantId;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await dbContext.BulkUpsertAsync(entityList, keySelector, cancellationToken);
|
||||
|
|
@ -237,9 +247,9 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
dbContext.Attach(entity);
|
||||
|
||||
foreach (var property in properties)
|
||||
foreach (var property in properties)
|
||||
dbContext.Entry(entity).Property(property).IsModified = true;
|
||||
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +259,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <param name="predicate">The predicate to use.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The entity if found, otherwise <c>null</c>.</returns>
|
||||
public async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) => await FindAsync(predicate, default, cancellationToken);
|
||||
public async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) => await FindAsync(predicate, null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the entity matching the specified predicate.
|
||||
|
|
@ -258,7 +268,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <param name="onLoading">A callback to run after the entity is loaded</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns></returns>
|
||||
public async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, Func<TDbContext, TEntity?, TEntity?>? onLoading = default, CancellationToken cancellationToken = default)
|
||||
public async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, Func<TDbContext, TEntity?, TEntity?>? onLoading = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
var set = dbContext.Set<TEntity>().AsNoTracking();
|
||||
|
|
@ -267,7 +277,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
if (entity == null)
|
||||
return null;
|
||||
|
||||
if (onLoading != default)
|
||||
if (onLoading != null)
|
||||
entity = onLoading.Invoke(dbContext, entity);
|
||||
|
||||
return entity;
|
||||
|
|
@ -280,7 +290,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <param name="onLoading">A callback to run after the entity is loaded</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>The entity if found, otherwise <c>null</c></returns>
|
||||
public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = default, CancellationToken cancellationToken = default)
|
||||
public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await FindAsync(query, onLoading, false, cancellationToken);
|
||||
}
|
||||
|
|
@ -293,7 +303,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <param name="tenantAgnostic">Define is the request should be tenant agnostic or not</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>The entity if found, otherwise <c>null</c></returns>
|
||||
public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = default, bool tenantAgnostic = false, CancellationToken cancellationToken = default)
|
||||
public async Task<TEntity?> FindAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = null, bool tenantAgnostic = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await QueryAsync(query, onLoading, tenantAgnostic, cancellationToken).FirstOrDefault();
|
||||
}
|
||||
|
|
@ -324,12 +334,12 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <summary>
|
||||
/// Finds a list of entities using a query
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<TEntity>> FindManyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) => await FindManyAsync(predicate, default, cancellationToken);
|
||||
public async Task<IEnumerable<TEntity>> FindManyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) => await FindManyAsync(predicate, null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a list of entities using a query
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<TEntity>> FindManyAsync(Expression<Func<TEntity, bool>> predicate, Action<TDbContext, TEntity?>? onLoading = default, CancellationToken cancellationToken = default)
|
||||
public async Task<IEnumerable<TEntity>> FindManyAsync(Expression<Func<TEntity, bool>> predicate, Action<TDbContext, TEntity?>? onLoading = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
var set = dbContext.Set<TEntity>().AsNoTracking();
|
||||
|
|
@ -349,9 +359,9 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
Expression<Func<TEntity, bool>> predicate,
|
||||
Expression<Func<TEntity, TKey>> orderBy,
|
||||
OrderDirection orderDirection = OrderDirection.Ascending,
|
||||
PageArgs? pageArgs = default,
|
||||
PageArgs? pageArgs = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await FindManyAsync(predicate, orderBy, orderDirection, pageArgs, default, cancellationToken);
|
||||
await FindManyAsync(predicate, orderBy, orderDirection, pageArgs, null, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of entities using a query
|
||||
|
|
@ -360,8 +370,8 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
Expression<Func<TEntity, bool>>? predicate,
|
||||
Expression<Func<TEntity, TKey>>? orderBy,
|
||||
OrderDirection orderDirection = OrderDirection.Ascending,
|
||||
PageArgs? pageArgs = default,
|
||||
Func<TDbContext, TEntity?, TEntity?>? onLoading = default,
|
||||
PageArgs? pageArgs = null,
|
||||
Func<TDbContext, TEntity?, TEntity?>? onLoading = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
|
|
@ -394,7 +404,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
return ListAsync(null, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TEntity>> ListAsync(Action<TDbContext, TEntity?>? onLoading = default, CancellationToken cancellationToken = default)
|
||||
public async Task<IEnumerable<TEntity>> ListAsync(Action<TDbContext, TEntity?>? onLoading = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
var set = dbContext.Set<TEntity>().AsNoTracking();
|
||||
|
|
@ -447,7 +457,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// </summary>
|
||||
public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await QueryAsync(query, default, false, cancellationToken);
|
||||
return await QueryAsync(query, null, false, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -455,13 +465,13 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// </summary>
|
||||
public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, bool tenantAgnostic, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await QueryAsync(query, default, tenantAgnostic, cancellationToken);
|
||||
return await QueryAsync(query, null, tenantAgnostic, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the database using a query and a selector.
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = default, CancellationToken cancellationToken = default)
|
||||
public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await QueryAsync(query, onLoading, false, cancellationToken);
|
||||
}
|
||||
|
|
@ -469,7 +479,7 @@ public class Store<TDbContext, TEntity>(IDbContextFactory<TDbContext> dbContextF
|
|||
/// <summary>
|
||||
/// Queries the database using a query and a selector.
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = default, bool ignoreQueryFilters = false, CancellationToken cancellationToken = default)
|
||||
public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, Func<TDbContext, TEntity?, CancellationToken, ValueTask>? onLoading = null, bool ignoreQueryFilters = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
var asNoTracking = onLoading == null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Elsa.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// A factory class that wraps and extends the functionality of an existing <see cref="IDbContextFactory{TDbContext}"/>
|
||||
/// to provide multi-tenancy support. This ensures that the created DbContext instances are aware of the current tenant context.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class TenantAwareDbContextFactory<TDbContext>(
|
||||
IDbContextFactory<TDbContext> decoratedFactory,
|
||||
ITenantAccessor tenantAccessor) : IDbContextFactory<TDbContext>
|
||||
where TDbContext : DbContext
|
||||
{
|
||||
public TDbContext CreateDbContext()
|
||||
{
|
||||
var context = decoratedFactory.CreateDbContext();
|
||||
SetTenantId(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
public async Task<TDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var context = await decoratedFactory.CreateDbContextAsync(cancellationToken);
|
||||
SetTenantId(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
private void SetTenantId(TDbContext context)
|
||||
{
|
||||
if (context is ElsaDbContextBase elsaContext)
|
||||
elsaContext.TenantId = tenantAccessor.Tenant?.Id.NullIfEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Elsa.Common;
|
||||
using Elsa.Common.Codecs;
|
||||
using Elsa.Common.Entities;
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Compression;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using Elsa.Common;
|
||||
using Elsa.Common.Codecs;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Compression;
|
||||
using Elsa.Workflows.Management.Options;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public record ObjectConverterOptions(JsonSerializerOptions? SerializerOptions =
|
|||
/// </summary>
|
||||
public static class ObjectConverter
|
||||
{
|
||||
public static bool StrictMode = true; // Set to false to revert to original flexible behavior.
|
||||
public static bool StrictMode = false; // Set to true to opt into strict mode.
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert the source value into the destination type.
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions<JintOp
|
|||
if (context.IsContainedWithinCompositeActivity())
|
||||
return;
|
||||
|
||||
var inputs = context.GetWorkflowInputs().Where(x => x.Name.IsValidVariableName()).ToDictionary(x => x.Name);
|
||||
var inputs = context.GetWorkflowInputs().Where(x => x.Name.IsValidVariableName()).ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!context.TryGetWorkflowExecutionContext(out var workflowExecutionContext))
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
|
|||
public async Task<object?> EvaluateAsync(string expression,
|
||||
Type returnType,
|
||||
ExpressionExecutionContext context,
|
||||
ExpressionEvaluatorOptions? options = default,
|
||||
Action<Engine>? configureEngine = default,
|
||||
ExpressionEvaluatorOptions? options = null,
|
||||
Action<Engine>? configureEngine = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken);
|
||||
|
|
@ -46,7 +46,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
|
|||
|
||||
private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options, CancellationToken cancellationToken)
|
||||
{
|
||||
options ??= new ExpressionEvaluatorOptions();
|
||||
options ??= new();
|
||||
|
||||
var engineOptions = new Jint.Options
|
||||
{
|
||||
|
|
@ -130,7 +130,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
|
|||
{
|
||||
var prepareOptions = new ScriptPreparationOptions
|
||||
{
|
||||
ParsingOptions = new ScriptParsingOptions
|
||||
ParsingOptions = new()
|
||||
{
|
||||
AllowReturnOutsideFunction = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,48 +16,63 @@ public class MessageReceived : Trigger<object>
|
|||
internal const string InputKey = "Message";
|
||||
|
||||
/// <inheritdoc />
|
||||
public MessageReceived([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
|
||||
public MessageReceived([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The message type to receive.
|
||||
/// </summary>
|
||||
public Type MessageType { get; set; } = default!;
|
||||
public Type MessageType { get; set; } = null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object GetTriggerPayload(TriggerIndexingContext context) => GetBookmarkPayload(context.ExpressionExecutionContext);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
// If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark.
|
||||
if (!context.TryGetWorkflowInput<object>(InputKey, out var message) || message.GetType() != MessageType)
|
||||
// If we did not receive external input, it means we are just now encountering this activity, and we need to block execution by creating a bookmark.
|
||||
if (!TryGetMessage(context, out var message))
|
||||
{
|
||||
// Create bookmarks for when we receive the expected HTTP request.
|
||||
context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext), ResumeAsync, includeActivityInstanceId: false);
|
||||
return;
|
||||
return default;
|
||||
}
|
||||
|
||||
// Provide the received message as output.
|
||||
context.Set(Result, message);
|
||||
|
||||
// Remove the input to prevent it from being passed to the next activity.
|
||||
context.WorkflowInput.Remove(InputKey);
|
||||
|
||||
// Complete.
|
||||
await context.CompleteActivityAsync();
|
||||
return ExecuteInternalAsync(context, message);
|
||||
}
|
||||
|
||||
private ValueTask ResumeAsync(ActivityExecutionContext context)
|
||||
{
|
||||
if(!TryGetMessage(context, out var message))
|
||||
throw new InvalidOperationException("Message was not received.");
|
||||
return ExecuteInternalAsync(context, message);
|
||||
}
|
||||
|
||||
private ValueTask ExecuteInternalAsync(ActivityExecutionContext context, object message)
|
||||
{
|
||||
// Provide the received message as output.
|
||||
context.Set(Result, message);
|
||||
|
||||
// Remove the input to prevent it from being passed to the next activity.
|
||||
context.WorkflowInput.Remove(InputKey);
|
||||
|
||||
|
||||
// Complete.
|
||||
return context.CompleteActivityAsync();
|
||||
}
|
||||
|
||||
private bool TryGetMessage(ActivityExecutionContext context, out object message)
|
||||
{
|
||||
if (!context.TryGetWorkflowInput(InputKey, out message))
|
||||
return false;
|
||||
|
||||
if (message.GetType() == MessageType)
|
||||
return true;
|
||||
|
||||
message = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private object GetBookmarkPayload(ExpressionExecutionContext context)
|
||||
{
|
||||
// Generate bookmark data for message type.
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ public class MassTransitFeature : FeatureBase
|
|||
var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit";
|
||||
var descriptionAttr = messageType.GetCustomAttribute<DescriptionAttribute>();
|
||||
var description = descriptionAttr?.Description ?? activityAttr?.Description;
|
||||
options.VariableDescriptors.Add(new VariableDescriptor(messageType, category, description));
|
||||
options.VariableDescriptors.Add(new(messageType, category, description));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,27 +43,27 @@ public class WorkflowsFeature : FeatureBase
|
|||
public WorkflowsFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A factory that instantiates a concrete <see cref="IStandardInStreamProvider"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IStandardInStreamProvider> StandardInStreamProvider { get; set; } = _ => new StandardInStreamProvider(Console.In);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A factory that instantiates a concrete <see cref="IStandardOutStreamProvider"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IStandardOutStreamProvider> StandardOutStreamProvider { get; set; } = _ => new StandardOutStreamProvider(Console.Out);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A factory that instantiates a concrete <see cref="IIdentityGenerator"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IIdentityGenerator> IdentityGenerator { get; set; } = sp => new RandomLongIdentityGenerator();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A handler for committing workflow execution state.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, ICommitStateHandler> CommitStateHandler { get; set; } = sp => new NoopCommitStateHandler();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A factory that instantiates a concrete <see cref="ILoggerStateGenerator{WorkflowExecutionContext}"/>.
|
||||
/// </summary>
|
||||
|
|
@ -80,7 +80,7 @@ public class WorkflowsFeature : FeatureBase
|
|||
public Action<IWorkflowExecutionPipelineBuilder> WorkflowExecutionPipeline { get; set; } = builder => builder
|
||||
.UseExceptionHandling()
|
||||
.UseDefaultActivityScheduler();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A delegate to configure the <see cref="IActivityExecutionPipeline"/>.
|
||||
/// </summary>
|
||||
|
|
@ -94,7 +94,7 @@ public class WorkflowsFeature : FeatureBase
|
|||
StandardInStreamProvider = provider;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Fluent method to set <see cref="StandardOutStreamProvider"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -32,14 +32,34 @@ public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncid
|
|||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "An exception was caught from a downstream middleware component");
|
||||
context.Fault(e);
|
||||
LogExceptionAndTransition(context, e);
|
||||
FaultAncestors(context);
|
||||
await HandleIncidentAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogExceptionAndTransition(ActivityExecutionContext context, Exception e)
|
||||
{
|
||||
context.Exception = e;
|
||||
context.TransitionTo(ActivityStatus.Faulted);
|
||||
var activity = context.Activity;
|
||||
var exceptionState = ExceptionState.FromException(e);
|
||||
var now = systemClock.UtcNow;
|
||||
var incident = new ActivityIncident(activity.Id, activity.NodeId ,activity.Type, e.Message, exceptionState, now);
|
||||
context.WorkflowExecutionContext.Incidents.Add(incident);
|
||||
}
|
||||
|
||||
private async Task HandleIncidentAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var strategy = await incidentStrategyResolver.ResolveStrategyAsync(context);
|
||||
strategy.HandleIncident(context);
|
||||
}
|
||||
|
||||
private static void FaultAncestors(ActivityExecutionContext context)
|
||||
{
|
||||
var ancestors = context.GetAncestors();
|
||||
|
||||
foreach (var ancestor in ancestors)
|
||||
ancestor.TransitionTo(ActivityStatus.Faulted);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,27 +10,13 @@ using Elsa.Workflows.Memory;
|
|||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.UIHints;
|
||||
using Humanizer;
|
||||
using Container = Elsa.Workflows.Activities.Container;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ActivityDescriber : IActivityDescriber
|
||||
public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolver, IActivityFactory activityFactory, IPropertyUIHandlerResolver propertyUIHandlerResolver) : IActivityDescriber
|
||||
{
|
||||
//private readonly IPropertyOptionsResolver _optionsResolver;
|
||||
private readonly IPropertyDefaultValueResolver _defaultValueResolver;
|
||||
private readonly IActivityFactory _activityFactory;
|
||||
private readonly IPropertyUIHandlerResolver _propertyUIHandlerResolver;
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolver, IActivityFactory activityFactory, IPropertyUIHandlerResolver propertyUIHandlerResolver)
|
||||
{
|
||||
//_optionsResolver = optionsResolver;
|
||||
_defaultValueResolver = defaultValueResolver;
|
||||
_activityFactory = activityFactory;
|
||||
_propertyUIHandlerResolver = propertyUIHandlerResolver;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ActivityDescriptor> DescribeActivityAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -90,14 +76,14 @@ public class ActivityDescriber : IActivityDescriber
|
|||
Ports = allPorts.ToList(),
|
||||
Inputs = (await DescribeInputPropertiesAsync(inputProperties, cancellationToken)).ToList(),
|
||||
Outputs = (await DescribeOutputPropertiesAsync(outputProperties, cancellationToken)).ToList(),
|
||||
IsContainer = typeof(IContainer).IsAssignableFrom(activityType),
|
||||
IsContainer = typeof(Container).IsAssignableFrom(activityType),
|
||||
IsBrowsable = browsableAttr == null || browsableAttr.Browsable,
|
||||
IsStart = isStart,
|
||||
IsTerminal = isTerminal,
|
||||
Attributes = attributes,
|
||||
Constructor = context =>
|
||||
{
|
||||
var activity = _activityFactory.Create(activityType, context);
|
||||
var activity = activityFactory.Create(activityType, context);
|
||||
activity.Type = fullTypeName;
|
||||
return activity;
|
||||
}
|
||||
|
|
@ -158,7 +144,7 @@ public class ActivityDescriber : IActivityDescriber
|
|||
if (wrappedPropertyType.IsNullableType())
|
||||
wrappedPropertyType = wrappedPropertyType.GetTypeOfNullable();
|
||||
|
||||
var uiSpecification = await _propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken);
|
||||
var uiSpecification = await propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken);
|
||||
|
||||
return new InputDescriptor
|
||||
(
|
||||
|
|
@ -172,7 +158,7 @@ public class ActivityDescriber : IActivityDescriber
|
|||
descriptionAttribute?.Description ?? inputAttribute?.Description,
|
||||
inputAttribute?.Category,
|
||||
inputAttribute?.Order ?? 0,
|
||||
_defaultValueResolver.GetDefaultValue(propertyInfo),
|
||||
defaultValueResolver.GetDefaultValue(propertyInfo),
|
||||
inputAttribute?.DefaultSyntax,
|
||||
inputAttribute?.IsReadOnly ?? false,
|
||||
inputAttribute?.IsBrowsable ?? true,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Reflection;
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Caching.Features;
|
||||
using Elsa.Common.Codecs;
|
||||
using Elsa.Common.Features;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Extensions;
|
||||
|
|
@ -14,10 +15,8 @@ using Elsa.Features.Services;
|
|||
using Elsa.Workflows.Features;
|
||||
using Elsa.Workflows.LogPersistence;
|
||||
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
|
||||
using Elsa.Workflows.Management.Compression;
|
||||
using Elsa.Workflows.Management.Contracts;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Management.Handlers;
|
||||
using Elsa.Workflows.Management.Handlers.Notifications;
|
||||
using Elsa.Workflows.Management.Mappers;
|
||||
using Elsa.Workflows.Management.Materializers;
|
||||
|
|
@ -35,9 +34,10 @@ namespace Elsa.Workflows.Management.Features;
|
|||
/// <summary>
|
||||
/// Installs and configures the workflow management feature.
|
||||
/// </summary>
|
||||
[DependsOn(typeof(CompressionFeature))]
|
||||
[DependsOn(typeof(MediatorFeature))]
|
||||
[DependsOn(typeof(SystemClockFeature))]
|
||||
[DependsOn(typeof(MemoryCacheFeature))]
|
||||
[DependsOn(typeof(SystemClockFeature))]
|
||||
[DependsOn(typeof(WorkflowsFeature))]
|
||||
[DependsOn(typeof(WorkflowDefinitionsFeature))]
|
||||
[DependsOn(typeof(WorkflowInstancesFeature))]
|
||||
|
|
@ -234,10 +234,6 @@ public class WorkflowManagementFeature(IModule module) : FeatureBase(module)
|
|||
.AddScoped<WorkflowDefinitionMapper>()
|
||||
.AddSingleton<VariableDefinitionMapper>()
|
||||
.AddSingleton<WorkflowStateMapper>()
|
||||
.AddSingleton<ICompressionCodecResolver, CompressionCodecResolver>()
|
||||
.AddSingleton<ICompressionCodec, None>()
|
||||
.AddSingleton<ICompressionCodec, GZip>()
|
||||
.AddSingleton<ICompressionCodec, Zstd>()
|
||||
;
|
||||
|
||||
Services
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Elsa.Expressions.Helpers;
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Workflows.Management.Mappers;
|
||||
|
|
@ -11,7 +12,7 @@ namespace Elsa.Workflows.Management.Mappers;
|
|||
/// <summary>
|
||||
/// Maps <see cref="Variable"/>s to <see cref="VariableDefinition"/>s and vice versa.
|
||||
/// </summary>
|
||||
public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegistry, ILogger<VariableDefinitionMapper> logger)
|
||||
public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegistry, IServiceScopeFactory scopeFactory, ILogger<VariableDefinitionMapper> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a <see cref="VariableDefinition"/> to a <see cref="Variable"/>.
|
||||
|
|
@ -47,7 +48,7 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist
|
|||
});
|
||||
}
|
||||
|
||||
variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : null;
|
||||
variable.StorageDriverType = GetStorageDriverType(source.StorageDriverTypeName);
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
|
@ -90,4 +91,28 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist
|
|||
/// Maps a list of <see cref="Variable"/>s to a list of <see cref="VariableDefinition"/>s.
|
||||
/// </summary>
|
||||
public IEnumerable<VariableDefinition> Map(IEnumerable<Variable>? source) => source?.Select(Map) ?? [];
|
||||
|
||||
private Type? GetStorageDriverType(string? storageDriverTypeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(storageDriverTypeName))
|
||||
return null;
|
||||
|
||||
var type = Type.GetType(storageDriverTypeName);
|
||||
|
||||
if (type != null)
|
||||
return type;
|
||||
|
||||
// TODO: The following code handles backward compatibility with variable definitions referencing older .NET type namespaces.
|
||||
// We will refactor this by storing a driver identifier rather than its full type name - which is brittle in case we move namespaces.
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var storageDrivers = scope.ServiceProvider.GetServices<IStorageDriver>();
|
||||
foreach (var storageDriver in storageDrivers)
|
||||
{
|
||||
var typeName = storageDriver.GetType().Name;
|
||||
if (storageDriverTypeName.Contains(typeName, StringComparison.OrdinalIgnoreCase))
|
||||
return storageDriver.GetType();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,4 +3,21 @@ namespace Elsa.Workflows.Management.Models;
|
|||
/// <summary>
|
||||
/// Represents a description of a .NET type that can be used as a workflow variable.
|
||||
/// </summary>
|
||||
public record VariableDescriptor(Type Type, string Category, string? Description);
|
||||
public record VariableDescriptor(Type Type, string Category, string? Description)
|
||||
{
|
||||
public class VariableDescriptorComparer : IEqualityComparer<VariableDescriptor>
|
||||
{
|
||||
public bool Equals(VariableDescriptor? x, VariableDescriptor? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return true;
|
||||
if (x is null || y is null) return false;
|
||||
|
||||
return x.Type == y.Type;
|
||||
}
|
||||
|
||||
public int GetHashCode(VariableDescriptor obj)
|
||||
{
|
||||
return obj.Type.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ public class ManagementOptions
|
|||
/// <summary>
|
||||
/// A collection of types that are available to the system as variable types.
|
||||
/// </summary>
|
||||
public HashSet<VariableDescriptor> VariableDescriptors { get; set; } = new();
|
||||
public HashSet<VariableDescriptor> VariableDescriptors { get; set; } = new(new VariableDescriptor.VariableDescriptorComparer());
|
||||
|
||||
/// <summary>
|
||||
/// The format to use for compressing workflow state.
|
||||
|
|
|
|||
|
|
@ -31,8 +31,27 @@ public class DistributedWorkflowClient(
|
|||
|
||||
public async Task<RunWorkflowInstanceResponse> CreateAndRunInstanceAsync(CreateAndRunWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _localWorkflowClient.CreateAndRunInstanceAsync(request, cancellationToken);
|
||||
return result;
|
||||
var createRequest = new CreateWorkflowInstanceRequest
|
||||
{
|
||||
Properties = request.Properties,
|
||||
CorrelationId = request.CorrelationId,
|
||||
Name = request.Name,
|
||||
Input = request.Input,
|
||||
WorkflowDefinitionHandle = request.WorkflowDefinitionHandle,
|
||||
ParentId = request.ParentId
|
||||
};
|
||||
var workflowInstance = await _localWorkflowClient.CreateInstanceInternalAsync(createRequest, cancellationToken);
|
||||
|
||||
// We need to lock newly created workflow instances too, because it might dispatch child workflows that attempt to resume the parent workflow.
|
||||
// For example, when using a DispatchWorkflow activity configured to wait for the dispatched workflow to complete.
|
||||
return await WithLockAsync(async () => await _localWorkflowClient.RunInstanceAsync(workflowInstance, new()
|
||||
{
|
||||
Input = request.Input,
|
||||
Variables = request.Variables,
|
||||
Properties = request.Properties,
|
||||
TriggerActivityId = request.TriggerActivityId,
|
||||
ActivityHandle = request.ActivityHandle
|
||||
}, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task CancelAsync(CancellationToken cancellationToken = default)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Management.Materializers;
|
||||
using Elsa.Workflows.Runtime.Features;
|
||||
using Elsa.Workflows.Runtime.Options;
|
||||
|
|
@ -46,7 +47,7 @@ public class ClrWorkflowsProvider(
|
|||
{
|
||||
Id = id,
|
||||
DefinitionId = definitionId,
|
||||
TenantId = tenantId
|
||||
TenantId = tenantId?.NullIfEmpty()
|
||||
};
|
||||
|
||||
var materializerContext = new ClrWorkflowMaterializerContext(workflowBuilder.GetType());
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public class BookmarkQueueSignaler : IBookmarkQueueSignaler
|
|||
lock (_lock)
|
||||
{
|
||||
// Reset the TCS for the next wait
|
||||
_tcs = new TaskCompletionSource<object?>();
|
||||
_tcs = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ public class LocalWorkflowClient(
|
|||
return workflowInstanceManager.ExistsAsync(workflowInstanceId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<RunWorkflowInstanceResponse> RunInstanceAsync(WorkflowInstance workflowInstance, RunWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
|
||||
public async Task<RunWorkflowInstanceResponse> RunInstanceAsync(WorkflowInstance workflowInstance, RunWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowState = workflowInstance.WorkflowState;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ namespace Elsa.Workflows.Core.UnitTests.ObjectConversion;
|
|||
|
||||
public class Tests
|
||||
{
|
||||
public Tests()
|
||||
{
|
||||
ObjectConverter.StrictMode = true;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryConvertTo_SameType_ReturnsSuccess()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue