Improve variable vs non-variable persistence (#3568)

* Incremental fixes

* Fix base assignment of async delegate block reference

* Reuse existing memory blocks

* Remove PlaybackStarted child activity

The PlaybackStarted property is unnecessary, since the PlayAudio activity already completes right after playback has started.

* Add GetCallStatus API method

* Prevent variables on composite activities to be overwritten during deserialization

* Remove unnecessary variable declarations

Activity variables will be declared automatically during activity invocation.

* Add XML comments

* Remove unused references arg

* Streamline variable identity assignment

* Fix composite early exit completion

* Move identity assignment to workflow definition service
This commit is contained in:
Sipke Schoorstra 2022-12-25 22:02:28 +01:00 committed by GitHub
parent 24692586a5
commit 8ac225520b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 208 additions and 104 deletions

View file

@ -55,11 +55,11 @@ export class VariableEditorDialogContent {
</FormEntry>
<FormEntry fieldId="variableStorageDriverId" label="Storage" hint="The storage to use when persisting the variable.">
<select id="variableStorageDriverId" name="variableStorageDriverId">
<select id="variableStorageDriverTypeName" name="variableStorageDriverTypeName">
{storageDrivers.map(driver => {
const value = driver.id;
const value = driver.typeName;
const text = driver.displayName;
const selected = value == variable.storageDriverId;
const selected = value == variable.storageDriverTypeName;
return <option value={value} selected={selected}>{text}</option>;
})}
</select>
@ -84,13 +84,13 @@ export class VariableEditorDialogContent {
const name = formData.get('variableName') as string;
const value = formData.get('variableValue') as string;
const type = formData.get('variableTypeName') as string;
const driverId = formData.get('variableStorageDriverId') as string;
const driverTypeName = formData.get('variableStorageDriverTypeName') as string;
const variable = this.variable;
variable.name = name;
variable.typeName = type;
variable.value = value;
variable.storageDriverId = isNullOrWhitespace(driverId) ? null : driverId;
variable.storageDriverTypeName = isNullOrWhitespace(driverTypeName) ? null : driverTypeName;
return variable;
};

View file

@ -60,7 +60,7 @@ export class VariablesEditor {
</thead>
<tbody>
{variables.map(variable => {
const storage = storageDrivers.find(x => x.id == variable.storageDriverId);
const storage = storageDrivers.find(x => x.typeName == variable.storageDriverTypeName);
const storageName = storage?.displayName ?? '-';
const descriptor = descriptorsStore.variableDescriptors.find(x => x.typeName == variable.typeName);
const typeDisplayName = descriptor?.displayName ?? variable.typeName;

View file

@ -72,7 +72,7 @@ export interface IntellisenseContext {
}
export interface StorageDriverDescriptor {
id: string;
typeName: string;
displayName: string;
}

View file

@ -34,7 +34,7 @@ export interface Variable {
name: string;
typeName: string;
value?: any;
storageDriverId?: string;
storageDriverTypeName?: string;
}
export interface ActivityInput {

View file

@ -2,6 +2,7 @@
using Elsa.Dsl.Models;
using Elsa.Dsl.Services;
using Elsa.Expressions.Services;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Services;
namespace Elsa.Dsl.Interpreters;
@ -17,8 +18,9 @@ public partial class WorkflowDefinitionBuilderInterpreter : ElsaParserBaseVisito
private readonly ParseTreeProperty<IList<object?>> _argValues = new();
private readonly ParseTreeProperty<Type> _expressionType = new();
private readonly IDictionary<string, DefinedVariable> _definedVariables = new Dictionary<string, DefinedVariable>();
private readonly Stack<IContainer> _containerStack = new();
private readonly Stack<Container> _containerStack = new();
/// <inheritdoc />
public WorkflowDefinitionBuilderInterpreter(
ITypeSystem typeSystem,
IFunctionActivityRegistry functionActivityRegistry,

View file

@ -13,7 +13,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dahomey.Json" Version="1.12.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
</ItemGroup>

View file

@ -0,0 +1,12 @@
namespace Elsa.Expressions.Extensions;
/// <summary>
/// Adds extension methods to <see cref="Type"/>.
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// Returns the default value for the specified type.
/// </summary>
public static object? GetDefaultValue(this Type type) => type.IsClass ? null : Activator.CreateInstance(type);
}

View file

@ -1,17 +1,32 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dahomey.Json;
using Dahomey.Json.Serialization.Conventions;
using Dahomey.Json.Util;
using Elsa.Expressions.Exceptions;
using DahomeyJsonNode = System.Text.Json.JsonNode;
using Elsa.Expressions.Extensions;
using Elsa.Expressions.Models;
namespace Elsa.Expressions.Helpers;
/// <summary>
/// A helper that attempts many strategies to try and convert the source value into the destination type.
/// </summary>
public static class ObjectConverter
{
public static Result TryConvertTo<T>(this object? value, JsonSerializerOptions? serializerOptions = null) => value.TryConvertTo(typeof(T), serializerOptions);
public static Result TryConvertTo(this object? value, Type targetType, JsonSerializerOptions? serializerOptions = null)
{
try
{
var convertedValue = value.ConvertTo(targetType, serializerOptions);
return new Result(true, convertedValue, null);
}
catch (Exception e)
{
return new Result(false, null, e);
}
}
public static T? ConvertTo<T>(this object? value, JsonSerializerOptions? serializerOptions = null) => value != null ? (T?)value.ConvertTo(typeof(T), serializerOptions) : default;
public static object? ConvertTo(this object? value, Type targetType, JsonSerializerOptions? serializerOptions = null)
@ -32,9 +47,6 @@ public static class ObjectConverter
var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (value is DahomeyJsonNode { ValueKind: JsonValueKind.Object } dahomyJsonObject)
return ToObject(dahomyJsonObject, targetType, options);
if (value is JsonElement jsonNumber && jsonNumber.ValueKind == JsonValueKind.Number && underlyingTargetType == typeof(string))
return jsonNumber.ToString().ConvertTo(underlyingTargetType);
@ -42,7 +54,7 @@ public static class ObjectConverter
{
if (jsonObject.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
return jsonObject.GetString().ConvertTo(underlyingTargetType);
return jsonObject.Deserialize(targetType, options);
}
@ -108,10 +120,4 @@ public static class ObjectConverter
throw new TypeConversionException($"Failed to convert an object of type {sourceType} to {underlyingTargetType}", value, underlyingTargetType, e);
}
}
private static object? ToObject(this DahomeyJsonNode node, Type type, JsonSerializerOptions? options = null)
{
using var arrayBufferWriter = new ArrayBufferWriter<byte>();
return JsonSerializer.Deserialize(node.ToString(), type, options);
}
}

View file

@ -32,7 +32,7 @@ public class DelegateBlockReference<T> : DelegateBlockReference
{
}
public DelegateBlockReference(Func<ExpressionExecutionContext, ValueTask<T?>> @delegate) : base(x => @delegate(x))
public DelegateBlockReference(Func<ExpressionExecutionContext, ValueTask<T?>> @delegate) : base(async x => await @delegate(x))
{
}
}

View file

@ -38,7 +38,10 @@ public class MemoryRegister
public MemoryBlock Declare(MemoryBlockReference blockReference)
{
var block = blockReference.Declare();
if (TryGetBlock(blockReference.Id, out var block))
return block;
block = blockReference.Declare();
Blocks[blockReference.Id] = block;
return block;
}

View file

@ -0,0 +1,51 @@
namespace Elsa.Expressions.Models;
/// <summary>
/// A simple monad that runs either the <see cref="OnSuccess"/> or <see cref="OnFailure"/> lambda, depending on whether or not the operation succeeded.
/// </summary>
public class Result
{
internal Result(bool success, object? value, Exception? exception)
{
Success = success;
Value = value;
Exception = exception;
}
/// <summary>
/// True if the conversaion succeeded, false otherwise.
/// </summary>
public bool Success { get; }
/// <summary>
/// The result value.
/// </summary>
public object? Value { get; }
/// <summary>
/// Any exception that may have occurred during the operation.
/// </summary>
public Exception? Exception { get; }
/// <summary>
/// Runs the provided delegate if the result is successful.
/// </summary>
public Result OnSuccess(Action<object?> successHandler)
{
if (Success)
successHandler(Value);
return this;
}
/// <summary>
/// Runs the provided delegate if the result is unsuccessful.
/// </summary>
public Result OnFailure(Action<Exception> failureHandler)
{
if (Exception != null)
failureHandler(Exception);
return this;
}
}

View file

@ -42,12 +42,6 @@ public class PlayAudio : PlayAudioBase
{
}
/// <summary>
/// The <see cref="IActivity"/> to execute when audio playback has started.
/// </summary>
[Port]
public IActivity? PlaybackStarted { get; set; }
/// <summary>
/// The <see cref="IActivity"/> to execute when the call was no longer active.
/// </summary>
@ -55,7 +49,7 @@ public class PlayAudio : PlayAudioBase
public IActivity? Disconnected { get; set; }
/// <inheritdoc />
protected override async ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(PlaybackStarted, OnCompletedAsync);
protected override async ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync();
/// <inheritdoc />
protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync);

View file

@ -5,6 +5,15 @@ namespace Elsa.Telnyx.Client.Models;
public record TelnyxResponse<T>(T Data);
public record CallStatusResponse(
string CallControlId,
string CallLegId,
string CallSessionId,
string ClientState,
bool IsAlive,
string RecordType
);
public record DialResponse(
string CallControlId,
string CallLegId,

View file

@ -5,6 +5,9 @@ namespace Elsa.Telnyx.Client.Services;
public interface ICallsApi
{
[Get("/v2/calls/{callControlId}")]
Task<TelnyxResponse<CallStatusResponse>> GetStatusAsync(string callControlId, CancellationToken cancellationToken = default);
[Post("/v2/calls")]
Task<TelnyxResponse<DialResponse>> DialAsync([Body] DialRequest request, CancellationToken cancellationToken = default);

View file

@ -10,4 +10,4 @@ public class Response
public ICollection<StorageDriverDescriptor> Items { get; set; }
}
public record StorageDriverDescriptor(string Id, string DisplayName);
public record StorageDriverDescriptor(string TypeName, string DisplayName);

View file

@ -22,6 +22,7 @@ public abstract class Composite : ActivityBase, IVariableContainer
}
/// <inheritdoc />
[JsonIgnore] // Composite activities' Variables is intended to be constructed from code only.
public ICollection<Variable> Variables { get; init; } = new List<Variable>();
/// <summary>
@ -29,7 +30,7 @@ public abstract class Composite : ActivityBase, IVariableContainer
/// </summary>
[Port]
[Browsable(false)]
[JsonIgnore] // Composite activities' Root is intended to be constructed from code only, so we don't want to get it serialized.
[JsonIgnore] // Composite activities' Root is intended to be constructed from code only.
public IActivity Root { get; set; } = new Sequence();
/// <inheritdoc />
@ -74,12 +75,15 @@ public abstract class Composite : ActivityBase, IVariableContainer
private async ValueTask OnCompleteCompositeSignal(CompleteCompositeSignal signal, SignalContext context)
{
await OnCompletedAsync(context.ReceiverActivityExecutionContext, context.SenderActivityExecutionContext);
// Complete the sender first so that it notifies its parents to complete.
await context.SenderActivityExecutionContext.CompleteActivityAsync();
// Then complete this activity.
await context.ReceiverActivityExecutionContext.CompleteActivityAsync(signal.Result);
context.StopPropagation();
}
/// <summary>

View file

@ -28,9 +28,6 @@ public abstract class Container : ActivityBase, IVariableContainer
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Register variables.
context.ExpressionExecutionContext.Memory.Declare(Variables);
// Schedule children.
await ScheduleChildrenAsync(context);
}

View file

@ -103,6 +103,9 @@ public static class ActivityExecutionContextExtensions
context.SetHasEvaluatedProperties();
}
/// <summary>
/// Evaluates the specified input property of the activity.
/// </summary>
public static async Task<T?> EvaluateInputPropertyAsync<TActivity, T>(this ActivityExecutionContext context, Expression<Func<TActivity, Input<T>>> propertyExpression)
{
var inputName = propertyExpression.GetProperty()!.Name;

View file

@ -43,15 +43,16 @@ public static class ActivityExtensions
return query.Select(x => x!).ToList();
}
public static IEnumerable<Variable> GetVariables(this IActivity activity)
public static IEnumerable<Variable> GetVariables(this IVariableContainer activity)
{
var properties = activity.GetType().GetProperties();
var variableProps = properties.Where(x => typeof(Variable).IsAssignableFrom(x.PropertyType)).ToList();
var variablesProps = properties.Where(x => typeof(IEnumerable<Variable>).IsAssignableFrom(x.PropertyType)).ToList();
var variables = variableProps.Select(x => (Variable?)x.GetValue(activity)).Where(x => x != null).Select(x => x!).ToList();
var manyVariables = variablesProps.Select(x => (IEnumerable<Variable>?)x.GetValue(activity)).Where(x => x != null).SelectMany(x => x!).ToList();
//var properties = activity.GetType().GetProperties();
//var variableProps = properties.Where(x => typeof(Variable).IsAssignableFrom(x.PropertyType)).ToList();
//var variablesProps = properties.Where(x => typeof(IEnumerable<Variable>).IsAssignableFrom(x.PropertyType)).ToList();
//var variables = variableProps.Select(x => (Variable?)x.GetValue(activity)).Where(x => x != null).Select(x => x!).ToList();
//var manyVariables = variablesProps.Select(x => (IEnumerable<Variable>?)x.GetValue(activity)).Where(x => x != null).SelectMany(x => x!).ToList();
return variables.Concat(manyVariables).ToList();
//return variables.Concat(manyVariables).ToList();
return activity.Variables;
}
public static TDelegate GetDelegate<TDelegate>(this IActivity activity, string methodName) where TDelegate : Delegate

View file

@ -15,10 +15,20 @@ public static class VariableExtensions
/// </summary>
public static Variable WithWorkflowStorage(this Variable variable) => variable.WithStorage<WorkflowStorageDriver>();
/// <summary>
/// Configures the variable to use the <see cref="WorkflowStorageDriver"/>.
/// </summary>
public static Variable<T> WithWorkflowStorage<T>(this Variable<T> variable) => (Variable<T>)variable.WithStorage<WorkflowStorageDriver>();
/// <summary>
/// Configures the variable to use the <see cref="MemoryStorageDriver"/>.
/// </summary>
public static Variable WithMemoryStorage(this Variable variable) => variable.WithStorage<MemoryStorageDriver>();
/// <summary>
/// Configures the variable to use the <see cref="MemoryStorageDriver"/>.
/// </summary>
public static Variable<T> WithMemoryStorage<T>(this Variable<T> variable) => (Variable<T>)variable.WithStorage<MemoryStorageDriver>();
/// <summary>
/// Configures the variable to use the specified <see cref="IStorageDriver"/> type.

View file

@ -76,10 +76,10 @@ public static class WorkflowExecutionContextExtensions
IActivity activity,
ActivityExecutionContext owner,
ActivityCompletionCallback? completionCallback = default,
IEnumerable<MemoryBlockReference>? references = default, object? tag = default)
object? tag = default)
{
var activityInvoker = workflowExecutionContext.GetRequiredService<IActivityInvoker>();
var workItem = new ActivityWorkItem(activity.Id, async () => await activityInvoker.InvokeAsync(workflowExecutionContext, activity, owner, references), tag);
var workItem = new ActivityWorkItem(activity.Id, async () => await activityInvoker.InvokeAsync(workflowExecutionContext, activity, owner), tag);
workflowExecutionContext.Scheduler.Schedule(workItem);
workflowExecutionContext.AddCompletionCallback(owner, activity, completionCallback);
}

View file

@ -21,20 +21,11 @@ public class ActivityInvoker : IActivityInvoker
public async Task InvokeAsync(
WorkflowExecutionContext workflowExecutionContext,
IActivity activity,
ActivityExecutionContext? owner,
IEnumerable<MemoryBlockReference>? memoryReferences = default)
ActivityExecutionContext? owner)
{
// Setup an activity execution context.
var activityExecutionContext = workflowExecutionContext.CreateActivityExecutionContext(activity, owner);
// Declare memory.
if (memoryReferences != null)
{
var workflowMemory = workflowExecutionContext.MemoryRegister;
var activityMemory = new MemoryRegister(workflowMemory);
activityMemory.Declare(memoryReferences);
}
// Add the activity context to the workflow context.
workflowExecutionContext.AddActivityExecutionContext(activityExecutionContext);

View file

@ -41,9 +41,6 @@ public class DefaultWorkflowExecutionContextFactory : IWorkflowExecutionContextF
// Build graph.
var graph = await _activityWalker.WalkAsync(root, cancellationToken);
// Assign identities.
_identityGraphService.AssignIdentities(graph);
// Create scheduler.
var scheduler = _schedulerFactory.CreateScheduler();

View file

@ -12,7 +12,7 @@ public class IdentityGraphService : IIdentityGraphService
_activityWalker = activityWalker;
}
public async Task AssignIdentitiesAsync(Workflow workflow, CancellationToken cancellationToken = default) => await AssignIdentitiesAsync(workflow.Root, cancellationToken);
public async Task AssignIdentitiesAsync(Workflow workflow, CancellationToken cancellationToken = default) => await AssignIdentitiesAsync((IActivity)workflow, cancellationToken);
public async Task AssignIdentitiesAsync(IActivity root, CancellationToken cancellationToken = default)
{
@ -29,7 +29,9 @@ public class IdentityGraphService : IIdentityGraphService
{
node.Activity.Id = CreateId(node, identityCounters, list);
AssignInputOutputs(node.Activity);
AssignVariables(node.Activity);
if(node.Activity is IVariableContainer variableContainer)
AssignVariables(variableContainer);
}
}
@ -64,7 +66,7 @@ public class IdentityGraphService : IIdentityGraphService
}
}
public void AssignVariables(IActivity activity)
public void AssignVariables(IVariableContainer activity)
{
var variables = activity.GetVariables();
var seed = 0;

View file

@ -33,18 +33,18 @@ public class VariablePersistenceManager : IVariablePersistenceManager
/// <inheritdoc />
public IEnumerable<Variable> GetVariablesInScope(ActivityExecutionContext context)
{
// Get variables for the current activity's immediate composite container.
var immediateCompositeVariables = ((Composite?)context.ActivityNode.Ancestors()
.FirstOrDefault(x => x.Activity is Composite)?.Activity)?.Variables
.Where(x => x.StorageDriverType != null) ?? Enumerable.Empty<Variable>();
// Get variables between the current activity and immediate composite container.
var ancestors = context.ActivityNode.Ancestors();
foreach (var node in ancestors)
{
if (node.Activity is IVariableContainer variableContainer)
foreach (var variable in variableContainer.Variables)
yield return variable;
// Get variables for the current activity itself, if it's a container.
var directVariables = (context.Activity is Composite composite
? composite.Variables.Where(x => x.StorageDriverType != null)
: Enumerable.Empty<Variable>());
// Return a concatenated list of variables.
return immediateCompositeVariables.Concat(directVariables);
if (node.Activity is Composite)
break;
}
}
/// <inheritdoc />
@ -112,12 +112,7 @@ public class VariablePersistenceManager : IVariablePersistenceManager
/// <inheritdoc />
public void EnsureVariables(WorkflowExecutionContext context, IEnumerable<Variable> variables)
{
var register = context.MemoryRegister;
foreach (var variable in variables)
{
if (!register.IsDeclared(variable))
register.Declare(variable);
}
foreach (var variable in variables) context.MemoryRegister.Declare(variable);
}
/// <inheritdoc />

View file

@ -84,12 +84,12 @@ public class ActivityExecutionContext
public ResumedBookmarkContext? ResumedBookmarkContext => WorkflowExecutionContext.ResumedBookmarkContext;
public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityCompletionCallback? completionCallback = default, IEnumerable<MemoryBlockReference>? references = default, object? tag = default)
public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityCompletionCallback? completionCallback = default, object? tag = default)
{
await ScheduleActivityAsync(activity, this, completionCallback, references, tag);
await ScheduleActivityAsync(activity, this, completionCallback, tag);
}
public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityExecutionContext owner, ActivityCompletionCallback? completionCallback = default, IEnumerable<MemoryBlockReference>? references = default, object? tag = default)
public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityExecutionContext owner, ActivityCompletionCallback? completionCallback = default, object? tag = default)
{
if (activity == null)
{
@ -100,7 +100,7 @@ public class ActivityExecutionContext
return;
}
WorkflowExecutionContext.Schedule(activity, owner, completionCallback, references, tag);
WorkflowExecutionContext.Schedule(activity, owner, completionCallback, tag);
}
public async ValueTask ScheduleActivitiesAsync(params IActivity?[] activities) => await ScheduleActivities(activities);

View file

@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
using Elsa.Expressions.Helpers;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
using Microsoft.Extensions.Logging;
namespace Elsa.Workflows.Core.Serialization.Converters;
@ -12,11 +13,13 @@ namespace Elsa.Workflows.Core.Serialization.Converters;
public class VariableConverter : JsonConverter<Variable>
{
private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry;
private readonly ILogger _logger;
/// <inheritdoc />
public VariableConverter(IWellKnownTypeRegistry wellKnownTypeRegistry)
public VariableConverter(IWellKnownTypeRegistry wellKnownTypeRegistry, ILogger<VariableConverter> logger)
{
_wellKnownTypeRegistry = wellKnownTypeRegistry;
_logger = logger;
}
/// <inheritdoc />
@ -39,7 +42,7 @@ public class VariableConverter : JsonConverter<Variable>
{
if (string.IsNullOrWhiteSpace(source.TypeName))
return null;
if (!_wellKnownTypeRegistry.TryGetTypeOrDefault(source.TypeName, out var type))
return null;
@ -47,7 +50,11 @@ public class VariableConverter : JsonConverter<Variable>
var variable = (Variable)Activator.CreateInstance(variableGenericType)!;
variable.Name = source.Name;
variable.Value = source.Value.ConvertTo(type);
source.Value.TryConvertTo(type)
.OnSuccess(value => variable.Value = value)
.OnFailure(e => _logger.LogWarning("Failed to convert {SourceValue} to {TargetType}", source.Value, type.Name));
variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : default;
return variable;

View file

@ -8,8 +8,7 @@ public interface IActivityInvoker
Task InvokeAsync(
WorkflowExecutionContext workflowExecutionContext,
IActivity activity,
ActivityExecutionContext? owner = default,
IEnumerable<MemoryBlockReference>? memoryReferences = default);
ActivityExecutionContext? owner = default);
Task InvokeAsync(ActivityExecutionContext activityExecutionContext);
}

View file

@ -8,5 +8,5 @@ public interface IIdentityGraphService
Task AssignIdentitiesAsync(IActivity root, CancellationToken cancellationToken = default);
void AssignIdentities(ActivityNode root);
void AssignInputOutputs(IActivity activity);
void AssignVariables(IActivity activity);
void AssignVariables(IVariableContainer activity);
}

View file

@ -5,7 +5,10 @@ namespace Elsa.Workflows.Core.Services;
/// <summary>
/// Represents a container for <see cref="Variable"/>s.
/// </summary>
public interface IVariableContainer
public interface IVariableContainer : IActivity
{
/// <summary>
/// A collection of variables within the scope of the variable container.
/// </summary>
ICollection<Variable> Variables { get; }
}

View file

@ -59,9 +59,6 @@ public class ActivityJsonConverter : JsonConverter<IActivity>
var context = new ActivityConstructorContext(doc.RootElement, newOptions);
var activity = activityDescriptor.Constructor(context);
_identityGraphService.AssignInputOutputs(activity);
_identityGraphService.AssignVariables(activity);
return activity;
}

View file

@ -59,8 +59,6 @@ public class ClrWorkflowDefinitionProvider : IWorkflowDefinitionProvider
await workflowBuilder.BuildAsync(builder, cancellationToken);
var workflow = builder.BuildWorkflow();
await _identityGraphService.AssignIdentitiesAsync(workflow, cancellationToken);
var workflowJson = JsonSerializer.Serialize(workflow.Root, _serializerOptionsProvider.CreatePersistenceOptions());
var materializerContext = new ClrWorkflowMaterializerContext(workflowBuilder.GetType());
var materializerContextJson = JsonSerializer.Serialize(materializerContext, _serializerOptionsProvider.CreatePersistenceOptions());
@ -75,7 +73,6 @@ public class ClrWorkflowDefinitionProvider : IWorkflowDefinitionProvider
Description = workflow.WorkflowMetadata.Description,
CustomProperties = workflow.Metadata,
Variables = workflow.Variables,
//ApplicationProperties = workflow.ApplicationProperties,
IsLatest = workflow.Publication.IsLatest,
IsPublished = workflow.Publication.IsPublished,
CreatedAt = workflow.WorkflowMetadata.CreatedAt == default ? _systemClock.UtcNow : workflow.WorkflowMetadata.CreatedAt,

View file

@ -1,22 +1,30 @@
using Elsa.Common.Models;
using Elsa.Workflows.Management.Services;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Services;
namespace Elsa.Workflows.Runtime.Implementations;
/// <inheritdoc />
public class WorkflowDefinitionService : IWorkflowDefinitionService
{
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
private readonly IIdentityGraphService _identityGraphService;
private readonly IEnumerable<IWorkflowMaterializer> _materializers;
public WorkflowDefinitionService(IWorkflowDefinitionStore workflowDefinitionStore, IEnumerable<IWorkflowMaterializer> materializers)
/// <summary>
/// Constructor.
/// </summary>
public WorkflowDefinitionService(IWorkflowDefinitionStore workflowDefinitionStore, IIdentityGraphService identityGraphService, IEnumerable<IWorkflowMaterializer> materializers)
{
_workflowDefinitionStore = workflowDefinitionStore;
_identityGraphService = identityGraphService;
_materializers = materializers;
}
/// <inheritdoc />
public async Task<Workflow> MaterializeWorkflowAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
var provider = _materializers.FirstOrDefault(x => x.Name == definition.MaterializerName);
@ -24,9 +32,15 @@ public class WorkflowDefinitionService : IWorkflowDefinitionService
if (provider == null)
throw new Exception("Provider not found");
return await provider.MaterializeAsync(definition, cancellationToken);
}
var workflow = await provider.MaterializeAsync(definition, cancellationToken);
// Assign identities.
await _identityGraphService.AssignIdentitiesAsync(workflow, cancellationToken);
return workflow;
}
/// <inheritdoc />
public async Task<WorkflowDefinition?> FindAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default) =>
await _workflowDefinitionStore.FindByDefinitionIdAsync(definitionId, versionOptions, cancellationToken);
}

View file

@ -9,6 +9,13 @@ namespace Elsa.Workflows.Runtime.Services;
/// </summary>
public interface IWorkflowDefinitionService
{
/// <summary>
/// Constructs an executable <see cref="Workflow"/> from the specified <see cref="WorkflowDefinition"/>.
/// </summary>
Task<Workflow> MaterializeWorkflowAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
/// Looks for a <see cref="WorkflowDefinition"/> by the specified definition ID and <see cref="VersionOptions"/>.
/// </summary>
Task<WorkflowDefinition?> FindAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default);
}

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Activities.Flowchart.Models;
using Elsa.Workflows.Core.Implementations;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
@ -18,7 +19,7 @@ class Workflow1 : WorkflowBase
protected override void Build(IWorkflowBuilder workflow)
{
var currentItem = workflow.WithVariable<string>("CurrentValue", "", StorageDriverNames.Memory);
var currentItem = workflow.WithVariable<string>("CurrentValue", "").WithMemoryStorage();
var writeLine1 = new WriteLine { Id = "WriteLine1", Text = new Input<string>("Start!") };
var forEach1 = new ForEach<string> { Id = "ForEach1", Items = new Input<ICollection<string>>(_items), CurrentValue = new Output<string?>(currentItem) };
var writeLine2 = new WriteLine { Id = "WriteLine2", Text = new Input<string>("Current Item") };
@ -51,7 +52,7 @@ class Workflow2 : WorkflowBase
protected override void Build(IWorkflowBuilder workflow)
{
var currentItem = workflow.WithVariable<string>("CurrentValue", "", StorageDriverNames.Memory);
var currentItem = workflow.WithVariable<string>("CurrentValue", "").WithMemoryStorage();
var writeLine1 = new WriteLine { Id = "WriteLine1", Text = new Input<string>("Start!") };
var forEach1 = new ForEach<string> { Id = "ForEach1", Items = new Input<ICollection<string>>(_items), CurrentValue = new Output<string?>(currentItem) };
var writeLine2 = new WriteLine { Id = "WriteLine2", Text = new Input<string>(currentItem) };
@ -82,7 +83,7 @@ class Workflow3 : WorkflowBase
protected override void Build(IWorkflowBuilder workflow)
{
var currentItem = workflow.WithVariable<string>("CurrentValue", "", StorageDriverNames.Memory);
var currentItem = workflow.WithVariable<string>("CurrentValue", "").WithMemoryStorage();
var writeLine1 = new WriteLine { Id = "WriteLine1", Text = new Input<string>("Start!") };
var forEach1 = new ForEach<string>