Add support for lazy initialization of output

This commit is contained in:
Sipke Schoorstra 2022-11-20 20:37:56 +01:00
parent 660cfbfd05
commit 9dcae97a0e
24 changed files with 216 additions and 60 deletions

View file

@ -36,9 +36,13 @@ public class ExpressionExecutionContext
public ExpressionExecutionContext? ParentContext { get; set; }
public CancellationToken CancellationToken { get; }
public MemoryBlock GetBlock(Func<MemoryBlockReference> blockReference) => GetBlock(blockReference());
public MemoryBlock GetBlock(MemoryBlockReference blockReference) => GetMemoryDatumInternal(blockReference) ?? throw new Exception($"Failed to retrieve memory block with reference {blockReference.Id}");
public object Get(Func<MemoryBlockReference> blockReference) => Get(blockReference());
public object Get(MemoryBlockReference blockReference) => GetBlock(blockReference).Value!;
public T Get<T>(Func<MemoryBlockReference> blockReference) => Get<T>(blockReference());
public T Get<T>(MemoryBlockReference blockReference) => (T)Get(blockReference);
public void Set(Func<MemoryBlockReference> blockReference, object? value) => Set(blockReference(), value);
public void Set(MemoryBlockReference blockReference, object? value)
{

View file

@ -33,7 +33,7 @@ public class ConfigureJavaScriptEngineWithActivityOutput : INotificationHandler<
foreach (var property in properties)
{
if (register.TryGetBlock(property.Value.MemoryBlockReference.Id, out var location))
if (register.TryGetBlock(property.Value.MemoryBlockReference().Id, out var location))
jsActivity[property.Name] = location.Value;
}

View file

@ -1,12 +1,18 @@
using Elsa.Persistence.EntityFrameworkCore.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Elsa.Persistence.EntityFrameworkCore.SqlServer.Extensions;
public static class DbContextOptionsBuilderExtensions
{
public static DbContextOptionsBuilder UseElsaSqlServer(this DbContextOptionsBuilder builder, string connectionString) =>
builder.UseSqlServer(connectionString, db => db
.MigrationsAssembly(typeof(DbContextOptionsBuilderExtensions).Assembly.GetName().Name)
.MigrationsHistoryTable(ElsaDbContextBase.MigrationsHistoryTable, ElsaDbContextBase.ElsaSchema));
public static DbContextOptionsBuilder UseElsaSqlServer(this DbContextOptionsBuilder builder, string connectionString, Action<SqlServerDbContextOptionsBuilder>? configure = default) =>
builder.UseSqlServer(connectionString, db =>
{
db
.MigrationsAssembly(typeof(DbContextOptionsBuilderExtensions).Assembly.GetName().Name)
.MigrationsHistoryTable(ElsaDbContextBase.MigrationsHistoryTable, ElsaDbContextBase.ElsaSchema);
configure?.Invoke(db);
});
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props"/>
<Import Project="..\..\..\configureawait.props"/>
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>

View file

@ -0,0 +1,14 @@
using Elsa.Features.Extensions;
using Elsa.Features.Services;
using Elsa.Quartz.Features;
namespace Elsa.Quartz.Extensions;
public static class ModuleExtensions
{
public static IModule UseQuartz(this IModule module, Action<QuartzFeature>? configure = default)
{
module.Use(configure);
return module;
}
}

View file

@ -0,0 +1,15 @@
using Elsa.Features.Extensions;
using Elsa.Features.Services;
using Elsa.Quartz.Features;
using Elsa.Scheduling.Features;
namespace Elsa.Quartz.Extensions;
public static class SchedulingFeatureExtensions
{
public static SchedulingFeature UseQuartzScheduling(this SchedulingFeature scheduling)
{
scheduling.Module.Use<QuartzSchedulerFeature>();
return scheduling;
}
}

View file

@ -1,4 +1,5 @@
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Bookmarks;
using Elsa.Telnyx.Client.Models;
using Elsa.Telnyx.Client.Services;
using Elsa.Telnyx.Exceptions;
using Elsa.Telnyx.Extensions;
@ -8,6 +9,7 @@ using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Models;
using Microsoft.Extensions.Options;
@ -17,8 +19,7 @@ namespace Elsa.Telnyx.Activities;
/// Dial a number or SIP URI.
/// </summary>
[Activity(Constants.Namespace, "Dial a number or SIP URI.", Kind = ActivityKind.Task)]
[FlowNode("Answered", "Hangup")]
public class Dial : ActivityBase
public abstract class DialBase : ActivityBase
{
[Input(Description = "The DID or SIP URI to dial out and bridge to the given call.")]
public Input<string?>? To { get; set; } = default!;
@ -33,7 +34,7 @@ public class Dial : ActivityBase
[Input(
Description = "Enables Answering Machine Detection.",
UIHint = InputUIHints.Dropdown,
Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end" },
Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end", "premium" },
DefaultValue = "disabled")]
public Input<string?>? AnsweringMachineDetection { get; set; } = new("disabled");
@ -71,16 +72,16 @@ public class Dial : ActivityBase
}
}
private async ValueTask HandleAnsweredAsync(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Answered");
private async ValueTask HandleHangupAsync(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Hangup");
private void HandleMachineGreetingEnded(ActivityExecutionContext context, CallMachineGreetingEndedBase payload)
{
context.Set(MachineDetectionResult, payload.Result);
context.CreateBookmark();
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallAnswered), ResumeAsync);
context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallHangup), ResumeAsync);
}
protected abstract ValueTask HandleAnsweredAsync(ActivityExecutionContext context);
protected abstract ValueTask HandleHangupAsync(ActivityExecutionContext context);
private async Task DialAsync(ActivityExecutionContext context)
{
var telnyxOptions = context.GetRequiredService<IOptions<TelnyxOptions>>().Value;
@ -100,8 +101,33 @@ public class Dial : ActivityBase
AnsweringMachineDetection.Get(context),
ClientState: clientState
);
var telnyxClient = context.GetRequiredService<ITelnyxClient>();
await telnyxClient.Calls.DialAsync(request, context.CancellationToken);
}
}
[FlowNode("Answered", "Hangup")]
public class FlowDial : DialBase
{
protected override async ValueTask HandleAnsweredAsync(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Answered");
protected override async ValueTask HandleHangupAsync(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Hangup");
}
public class Dial : DialBase
{
[Port] public IActivity? Answered { get; set; }
[Port] public IActivity? Hangup { get; set; }
protected override ValueTask HandleAnsweredAsync(ActivityExecutionContext context)
{
context.ScheduleActivity(Answered);
return ValueTask.CompletedTask;
}
protected override ValueTask HandleHangupAsync(ActivityExecutionContext context)
{
context.ScheduleActivity(Hangup);
return ValueTask.CompletedTask;
}
}

View file

@ -10,6 +10,7 @@ internal static class WebhookEventTypes
public const string CallInitiated = "call.initiated";
public const string CallMachineGreetingEnded = "call.machine.greeting.ended";
public const string CallMachinePremiumGreetingEnded = "call.machine.premium.greeting.ended";
public const string CallMachineDetectionEnded = "call.machine.detection.ended";
public const string CallMachinePremiumDetectionEnded = "call.machine.premium.detection.ended";
public const string CallPlaybackStarted = "call.playback.started";
public const string CallPlaybackEnded = "call.playback.ended";

View file

@ -95,8 +95,8 @@ public class ForEach<T> : ForEach
[Input]
public new Input<ICollection<T>> Items
{
get => new(base.Items.Expression, base.Items.MemoryBlockReference);
set => base.Items = new Input<ICollection<object>>(value.Expression, value.MemoryBlockReference);
get => new(base.Items.Expression, base.Items.MemoryBlockReference());
set => base.Items = new Input<ICollection<object>>(value.Expression, value.MemoryBlockReference());
}
public new Output<T?> CurrentValue

View file

@ -64,7 +64,7 @@ public static class ActivityExecutionContextExtensions
foreach (var input in assignedInputs)
{
var memoryReference = input.MemoryBlockReference;
var memoryReference = input.MemoryBlockReference();
var value = await evaluator.EvaluateAsync(input, expressionExecutionContext);
memoryReference.Set(context, value);
}
@ -96,9 +96,9 @@ public static class ActivityExecutionContextExtensions
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
var expressionExecutionContext = context.ExpressionExecutionContext;
var locationReference = input.MemoryBlockReference;
var memoryBlockReference = input.MemoryBlockReference();
var value = await evaluator.EvaluateAsync(input, expressionExecutionContext);
locationReference.Set(context, value);
memoryBlockReference.Set(context, value);
return input;
}
@ -151,9 +151,9 @@ public static class ActivityExecutionContextExtensions
public static async Task<T?> EvaluateAsync<T>(this ActivityExecutionContext context, Input<T> input)
{
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
var locationReference = input.MemoryBlockReference;
var memoryBlockReference = input.MemoryBlockReference();
var value = await evaluator.EvaluateAsync(input, context.ExpressionExecutionContext);
locationReference.Set(context, value);
memoryBlockReference.Set(context, value);
return value;
}

View file

@ -45,6 +45,6 @@ public static class ExpressionExecutionContextExtensions
public static void Set(this ExpressionExecutionContext context, Output? output, object? value)
{
if(output != null) context.Set(output.MemoryBlockReference, value);
if(output != null) context.Set(output.MemoryBlockReference(), value);
}
}

View file

@ -36,24 +36,33 @@ public class IdentityGraphService : IIdentityGraphService
private void AssignInputOutputs(IActivity activity)
{
var inputs = activity.GetInputs();
var assignedInputs = inputs.Where(x => x.MemoryBlockReference != null! && x.MemoryBlockReference.Id == null!).ToList();
var assignedInputs = inputs.Where(x =>
{
var memoryBlockReference = x.MemoryBlockReference();
return memoryBlockReference.Id == null!;
}).ToList();
var seed = 0;
foreach (var input in assignedInputs)
{
var locationReference = input.MemoryBlockReference;
var locationReference = input.MemoryBlockReference();
locationReference.Id = $"{activity.Id}:input-{++seed}";
}
seed = 0;
var outputs = activity.GetOutputs();
var assignedOutputs = outputs.Where(x => x.Value.MemoryBlockReference != null! && x.Value.MemoryBlockReference.Id == null!).ToList();
var assignedOutputs = outputs.Where(x =>
{
var memoryBlockReference = x.Value.MemoryBlockReference();
return memoryBlockReference != null! && memoryBlockReference.Id == null!;
}).ToList();
foreach (var output in assignedOutputs)
{
var memoryReference = output.Value.MemoryBlockReference;
var memoryReference = output.Value.MemoryBlockReference();
memoryReference.Id = $"{activity.Id}:output-{++seed}";
}
}

View file

@ -190,7 +190,7 @@ public class ActivityExecutionContext
public T? GetService<T>() where T : notnull => WorkflowExecutionContext.GetService<T>();
public IEnumerable<T> GetServices<T>() where T : notnull => WorkflowExecutionContext.GetServices<T>();
public object? GetService(Type serviceType) => WorkflowExecutionContext.GetService(serviceType);
public T? Get<T>(Input<T>? input) => input == null ? default : Get<T>(input.MemoryBlockReference);
public T? Get<T>(Input<T>? input) => input == null ? default : Get<T>(input.MemoryBlockReference());
public object? Get(MemoryBlockReference blockReference)
{

View file

@ -4,14 +4,18 @@ namespace Elsa.Workflows.Core.Models;
public abstract class Argument
{
protected Argument(){}
protected Argument(MemoryBlockReference memoryBlockReference /*, Func<object?, object?>? valueConverter = default*/)
protected Argument()
{
MemoryBlockReference = memoryBlockReference;
//ValueConverter = valueConverter;
}
public MemoryBlockReference MemoryBlockReference { get; set; } = default!;
//public Func<object?, object?>? ValueConverter { get; set; }
protected Argument(MemoryBlockReference memoryBlockReference) : this(() => memoryBlockReference)
{
}
protected Argument(Func<MemoryBlockReference> memoryBlockReference)
{
MemoryBlockReference = memoryBlockReference;
}
public Func<MemoryBlockReference> MemoryBlockReference { get; set; } = default!;
}

View file

@ -43,7 +43,7 @@ public class Input<T> : Input
{
}
public Input(Output output) : base(new OutputExpression(output), output.MemoryBlockReference, typeof(T))
public Input(Output output) : base(new OutputExpression(output), output.MemoryBlockReference(), typeof(T))
{
}

View file

@ -8,11 +8,13 @@ public class Output : Argument
{
}
public Output(MemoryBlockReference locationBlockReference) : base(locationBlockReference)
public Output(MemoryBlockReference memoryBlockReference) : base(memoryBlockReference)
{
}
public Output(Func<MemoryBlockReference> memoryBlockReference) : base(memoryBlockReference)
{
}
//public ICollection<MemoryReference> Targets { get; } = new List<MemoryReference>();
}
public class Output<T> : Output
@ -21,7 +23,11 @@ public class Output<T> : Output
{
}
public Output(MemoryBlockReference locationBlockReference) : base(locationBlockReference)
public Output(MemoryBlockReference memoryBlockReference) : base(memoryBlockReference)
{
}
public Output(Func<MemoryBlockReference> memoryBlockReference) : base(memoryBlockReference)
{
}
}

View file

@ -46,7 +46,7 @@ public class OutputJsonConverter<T> : JsonConverter<Output<T>?>
Type = valueTypeAlias,
MemoryReference = value == null ? null : new
{
Id = value.MemoryBlockReference.Id
Id = value.MemoryBlockReference().Id
}
};

View file

@ -165,7 +165,7 @@ public class TriggerIndexer : ITriggerIndexer
// Evaluate activity inputs before requesting trigger data.
foreach (var input in assignedInputs)
{
var locationReference = input.MemoryBlockReference;
var locationReference = input.MemoryBlockReference();
try
{

View file

@ -7,7 +7,7 @@ using Elsa.Workflows.Runtime.Services;
namespace Elsa.Workflows.Runtime.Middleware;
/// <summary>
/// Takes care of persisting a workflow instance after workflow execution.
/// Takes care of persisting workflow execution log entries.
/// </summary>
public class PersistWorkflowExecutionLogMiddleware : WorkflowExecutionMiddleware
{

View file

@ -4,7 +4,6 @@ using Elsa.Workflows.Core.Models;
public class AskAge : Composite<int>
{
private readonly WriteLine _prompt = new();
private readonly Variable<string> _age = new();
public Input<string> Prompt { get; set; } = new("Please tell me your age:");
@ -16,17 +15,12 @@ public class AskAge : Composite<int>
Variables = { _age },
Activities =
{
_prompt,
new WriteLine(context => Prompt.Get(context)),
new ReadLine(_age)
}
};
}
protected override void ConfigureActivities(ActivityExecutionContext context)
{
_prompt.Text = Prompt;
}
protected override void OnCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext)
{
var age = _age.Get<int>(context);

View file

@ -0,0 +1,42 @@
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Models;
public class AskDetails : Composite<Person>
{
private readonly Variable<string> _name = new();
private readonly Variable<int> _age = new();
public Input<string> NamePrompt { get; set; } = new("Please tell me your name:");
public Input<string> AgePrompt { get; set; } = new("Please tell me your age:");
public AskDetails()
{
Root = new Sequence
{
Variables = { _name, _age },
Activities =
{
new AskName
{
Prompt = new Input<string>(context => NamePrompt.Get(context)),
Result = new Output(() => _name)
},
new AskAge
{
Prompt = new Input<string>(context => AgePrompt.Get(context)),
Result = new Output(() => _age)
}
}
};
}
protected override void OnCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext)
{
var name = _name.Get<string>(context)!;
var age = _age.Get<int>(context);
var person = new Person(name, age);
context.Set(Result, person);
}
}

View file

@ -0,0 +1,29 @@
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Models;
public class AskName : Composite<string>
{
private readonly Variable<string> _name = new();
public Input<string> Prompt { get; set; } = new("Please tell me your name:");
public AskName()
{
Root = new Sequence
{
Variables = { _name },
Activities =
{
new WriteLine(context => Prompt.Get(context)),
new ReadLine(_name)
}
};
}
protected override void OnCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext)
{
var name = _name.Get<string>(context);
context.Set(Result, name);
}
}

View file

@ -0,0 +1 @@
public record Person(string Name, int Age);

View file

@ -14,20 +14,25 @@ services.AddElsa();
var serviceProvider = services.BuildServiceProvider();
// Declare a workflow variable for use in the workflow.
var ageVariable = new Variable<int>();
var personVariable = new Variable<Person>();
// Declare a workflow.
var workflow = new Sequence
{
Variables = { ageVariable },
Variables = { personVariable },
Activities =
{
new AskAge
new AskDetails()
{
Prompt = new Input<string>("What's your age?"),
Result = new Output(ageVariable)
NamePrompt = new Input<string>("What's your name?"),
AgePrompt = new Input<string>("What's your age?"),
Result = new Output(personVariable)
},
new WriteLine(context => $"The age you told me is: {ageVariable.Get(context)}")
new WriteLine(context =>
{
var person = personVariable.Get(context)!;
return $"Your name is {person.Name} and you are {person.Age} years old.";
})
}
};