Various API enhancements

This commit is contained in:
Sipke Schoorstra 2022-03-08 22:41:12 +01:00
parent 80e72846e3
commit 4f3a3de208
18 changed files with 167 additions and 43 deletions

View file

@ -18,12 +18,6 @@ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "architecture", "architecture", "{A34FD350-79C5-492A-8811-6DBD80830F8B}"
ProjectSection(SolutionItems) = preProject
docs\architecture\Layer1.md = docs\architecture\Layer1.md
docs\architecture\Layer2.md = docs\architecture\Layer2.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "console", "console", "{873BFC3E-63C2-4495-A503-5EC05DCD84E4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aspnet", "aspnet", "{56C2FFB8-EA54-45B5-A095-4A78142EB4B5}"
@ -211,7 +205,6 @@ Global
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
{C6658DE0-2B2F-47F0-BB61-2CA66D435C09} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
{6FA9ED7D-2D32-46BC-B47F-E503B2A4C0A9} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
{A34FD350-79C5-492A-8811-6DBD80830F8B} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6}
{873BFC3E-63C2-4495-A503-5EC05DCD84E4} = {155227F0-A33B-40AA-A4B4-06F813EB921B}
{B44A9DDA-8091-490A-BC0B-2EAE4A601B09} = {873BFC3E-63C2-4495-A503-5EC05DCD84E4}
{56C2FFB8-EA54-45B5-A095-4A78142EB4B5} = {155227F0-A33B-40AA-A4B4-06F813EB921B}

View file

@ -1,4 +1,5 @@
using System.ComponentModel;
using Elsa.Attributes;
using Elsa.Models;
namespace Elsa.Activities.Console;

View file

@ -3,13 +3,29 @@ namespace Elsa.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class ActivityAttribute : Attribute
{
public ActivityAttribute(string? typeName = default, string? description = default, string? category = default)
public ActivityAttribute(string @namespace, string? category, string? description = default)
{
Namespace = @namespace;
Description = description;
Category = category;
}
public ActivityAttribute(string @namespace, string? description = default)
{
Namespace = @namespace;
Description = description;
Category = @namespace;
}
public ActivityAttribute(string @namespace, string? typeName, string? description = default, string? category = default)
{
Namespace = @namespace;
TypeName = typeName;
Description = description;
Category = category;
}
public string? Namespace { get; }
public string? TypeName { get; }
public string? Description { get; }
public string? Category { get; }

View file

@ -25,7 +25,7 @@ public class InputAttribute : Attribute
// /// <summary>
// /// A brief description about this property for workflow tooling to use when displaying activity editors.
// /// </summary>
// public string? Description { get; set; }
public string? Description { get; set; }
/// <summary>
/// A category to group this property with.
@ -68,7 +68,7 @@ public class InputAttribute : Attribute
public string[]? SupportedSyntaxes { get; set; }
/// <summary>
/// A value indicating whether this property should be displayed but as read-only.
/// A value indicating whether this property should be displayed as read-only.
/// </summary>
public bool IsReadOnly { get; set; }

View file

@ -1,9 +1,24 @@
using System.Text.Json;
using Elsa.Contracts;
using Elsa.Models;
namespace Elsa.Extensions;
public static class ActivityExecutionContextExtensions
{
public static bool TryGetInput<T>(this ActivityExecutionContext context, string key, out T value) => context.Input.TryGetValue(key, out value);
public static bool TryGetInput<T>(this ActivityExecutionContext context, string key, out T value) => context.Input.TryGetValue(key, out value!);
public static T GetInput<T>(this ActivityExecutionContext context) => context.GetInput<T>(typeof(T).Name);
public static T GetInput<T>(this ActivityExecutionContext context, string key) => (T)context.Input[key];
public static WorkflowExecutionLogEntry AddExecutionLogEntry(this ActivityExecutionContext context, string eventName, string? message = default, object? payload = default) =>
context.AddExecutionLogEntry(eventName, message, default, payload);
public static WorkflowExecutionLogEntry AddExecutionLogEntry(this ActivityExecutionContext context, string eventName, string? message = default, string? source = default, object? payload = default)
{
var activity = context.Activity;
var workflowExecutionContext = context.WorkflowExecutionContext;
var now = context.GetRequiredService<ISystemClock>().UtcNow;
var logEntry = new WorkflowExecutionLogEntry(activity.Id, activity.TypeName, now, eventName, message, source, payload);
workflowExecutionContext.ExecutionLog.Add(logEntry);
return logEntry;
}
}

View file

@ -62,7 +62,7 @@ public class ActivityExecutionContext
/// A dictionary of received inputs.
/// </summary>
public IReadOnlyDictionary<string, object> Input => WorkflowExecutionContext.Input;
/// <summary>
/// Journal data will be added to the workflow execution log for the "Executed" event.
/// </summary>
@ -98,10 +98,12 @@ public class ActivityExecutionContext
CreateBookmark(bookmarkDatum, callback);
}
public void CreateBookmarks(IEnumerable<Bookmark> bookmarks) => _bookmarks.AddRange(bookmarks);
public void CreateBookmark(Bookmark bookmark) => _bookmarks.Add(bookmark);
public void AddBookmarks(IEnumerable<Bookmark> bookmarks) => _bookmarks.AddRange(bookmarks);
public void AddBookmark(Bookmark bookmark) => _bookmarks.Add(bookmark);
public void CreateBookmark(object? bookmarkDatum, ExecuteActivityDelegate? callback = default)
public Bookmark CreateBookmark(ExecuteActivityDelegate callback) => CreateBookmark(default, callback);
public Bookmark CreateBookmark(object? bookmarkDatum = default, ExecuteActivityDelegate? callback = default)
{
var hasher = GetRequiredService<IHasher>();
var identityGenerator = GetRequiredService<IIdentityGenerator>();
@ -109,17 +111,20 @@ public class ActivityExecutionContext
var bookmarkDatumJson = bookmarkDatum != null ? bookmarkDataSerializer.Serialize(bookmarkDatum) : default;
var hash = bookmarkDatumJson != null ? hasher.Hash(bookmarkDatumJson) : default;
CreateBookmark(new Bookmark(
var bookmark = new Bookmark(
identityGenerator.GenerateId(),
Activity.TypeName,
hash,
bookmarkDatumJson,
Activity.Id,
Id,
callback?.Method.Name));
callback?.Method.Name);
AddBookmark(bookmark);
return bookmark;
}
public T? GetProperty<T>(string key) => Properties.TryGetValue<T?>(key, out var value) ? value : default;
public T? GetProperty<T>(string key) => Properties!.TryGetValue<T?>(key, out var value) ? value : default;
public void SetProperty<T>(string key, T value) where T : notnull => Properties[key] = value;
public T UpdateProperty<T>(string key, Func<T?, T> updater) where T : notnull

View file

@ -26,7 +26,24 @@ public class ExpressionExecutionContext
public object Get(RegisterLocationReference locationReference) => GetLocation(locationReference).Value!;
public T Get<T>(RegisterLocationReference locationReference) => (T)Get(locationReference);
public T? Get<T>(Input<T>? input) => input != null ? (T?)GetLocation(input.LocationReference).Value : default;
public T? GetVariable<T>(string name, object? defaultValue = default) => Get<T>(new Variable(name, defaultValue));
public T? GetVariable<T>(string name) => (T?)GetVariable(name);
public T? GetVariable<T>() => (T?)GetVariable(typeof(T).Name);
public object? GetVariable(string name)
{
var variable = Workflow.Variables.FirstOrDefault(x => x.Name == name);
return variable?.Get(Register);
}
public Variable SetVariable<T>(T? value) => SetVariable(typeof(T).Name, value);
public Variable SetVariable<T>(string name, T? value) => SetVariable(name, (object?)value);
public Variable SetVariable(string name, object? value)
{
var variable = Workflow.Variables.FirstOrDefault(x => x.Name == name) ?? new Variable(name, value);
variable.Set(Register, value);
return variable;
}
public void Set(RegisterLocationReference locationReference, object? value)
{

View file

@ -10,10 +10,13 @@ public abstract class RegisterLocationReference
public string Id { get; set; } = default!;
public abstract RegisterLocation Declare();
public object? Get(Register register) => GetLocation(register).Value;
public T? Get<T>(Register register) => (T?)Get(register);
public object? Get(ExpressionExecutionContext context) => context.Get(this);
public object? Get(ActivityExecutionContext context) => Get(context.ExpressionExecutionContext);
public T? Get<T>(ExpressionExecutionContext context) => (T?)Get(context);
public T? Get<T>(ActivityExecutionContext context) => (T?)Get(context.ExpressionExecutionContext);
public void Set(Register register, object? value) => GetLocation(register).Value = value;
public void Set(ExpressionExecutionContext context, object? value) => context.Set(this, value);
public void Set(ActivityExecutionContext context, object? value) => Set(context.ExpressionExecutionContext, value);

View file

@ -37,10 +37,12 @@ public class WorkflowExecutionContext
CancellationToken = cancellationToken;
NodeIdLookup = _nodes.ToDictionary(x => x.NodeId);
NodeActivityLookup = _nodes.ToDictionary(x => x.Activity);
Register = workflow.CreateRegister();
}
public Workflow Workflow { get; }
public ActivityNode Graph { get; set; }
public ActivityNode Graph { get; }
public Register Register { get; }
public string Id { get; set; }
public IReadOnlyCollection<ActivityNode> Nodes => new ReadOnlyCollection<ActivityNode>(_nodes);
public IDictionary<string, ActivityNode> NodeIdLookup { get; }
@ -48,18 +50,18 @@ public class WorkflowExecutionContext
public IActivityScheduler Scheduler { get; }
public Bookmark? Bookmark { get; }
public IReadOnlyDictionary<string, object> Input { get; }
/// <summary>
/// A dictionary that can be used by application code and activities to store information. Values need to be serializable, since this dictionary will be persisted alongside the workflow instance.
/// </summary>
public IDictionary<string, object?> Properties { get; set; } = new Dictionary<string, object?>();
/// <summary>
/// A dictionary that can be used by application code and middleware to store information and even services. Values do not need to be serializable, since this dictionary will not be persisted.
/// All data will be gone once workflow execution completes.
/// </summary>
public IDictionary<string, object?> TransientProperties { get; set; } = new Dictionary<string, object?>();
public ExecuteActivityDelegate? ExecuteDelegate { get; set; }
public CancellationToken CancellationToken { get; }
public IReadOnlyCollection<Bookmark> Bookmarks => new ReadOnlyCollection<Bookmark>(_bookmarks);
@ -99,7 +101,7 @@ public class WorkflowExecutionContext
RemoveCompletionCallback(entry);
return entry.CompletionCallback;
}
public void RemoveCompletionCallback(ActivityCompletionCallbackEntry entry) => _completionCallbackEntries.Remove(entry);
public ActivityNode FindNodeById(string nodeId) => NodeIdLookup[nodeId];
@ -141,8 +143,27 @@ public class WorkflowExecutionContext
var activityInvoker = GetRequiredService<IActivityInvoker>();
var workItem = new ActivityWorkItem(bookmarkedActivity.Id, async () => await activityInvoker.InvokeAsync(bookmarkedActivityContext));
Scheduler.Push(workItem);
// If no resumption point was specified, use Noop to prevent the regular "ExecuteAsync" method to be invoked.
ExecuteDelegate = bookmark.CallbackMethodName != null ? bookmarkedActivity.GetResumeActivityDelegate(bookmark.CallbackMethodName) : Noop;
}
public T? GetVariable<T>(string name) => (T?)GetVariable(name);
public T? GetVariable<T>() => (T?)GetVariable(typeof(T).Name);
public object? GetVariable(string name)
{
var variable = Workflow.Variables.FirstOrDefault(x => x.Name == name);
return variable?.Get(Register);
}
public Variable SetVariable<T>(T? value) => SetVariable(typeof(T).Name, value);
public Variable SetVariable<T>(string name, T? value) => SetVariable(name, (object?)value);
public Variable SetVariable(string name, object? value)
{
var variable = Workflow.Variables.FirstOrDefault(x => x.Name == name) ?? new Variable(name, value);
variable.Set(Register, value);
return variable;
}
}

View file

@ -64,13 +64,7 @@ public class ActivityInvokerMiddleware : IActivityExecutionMiddleware
await CompleteParentsAsync(context);
}
private void LogExecutionRecord(ActivityExecutionContext context, string eventName, string? message = default, string? source = default, object? payload = default)
{
var activity = context.Activity;
var workflowExecutionContext = context.WorkflowExecutionContext;
var logEntry = new WorkflowExecutionLogEntry(activity.Id, activity.TypeName, _clock.UtcNow, eventName, message, source, payload);
workflowExecutionContext.ExecutionLog.Add(logEntry);
}
private void LogExecutionRecord(ActivityExecutionContext context, string eventName, string? message = default, string? source = default, object? payload = default) => context.AddExecutionLogEntry(eventName, message, source, payload);
private async Task EvaluateInputPropertiesAsync(ActivityExecutionContext context)
{

View file

@ -26,8 +26,7 @@ public class ActivityInvoker : IActivityInvoker
var parentActivityExecutionContext = owner;
// Setup an activity execution context.
var register = workflowExecutionContext.Workflow.CreateRegister();
var register = workflowExecutionContext.Register;
var workflow = workflowExecutionContext.Workflow;
var transientProperties = workflowExecutionContext.TransientProperties;
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, workflow, transientProperties, parentActivityExecutionContext?.ExpressionExecutionContext, cancellationToken);

View file

@ -88,13 +88,13 @@ public class ActivityDescriber : IActivityDescriber
wrappedPropertyType,
GetUIHint(wrappedPropertyType, inputAttribute),
inputAttribute?.DisplayName ?? propertyInfo.Name.Humanize(LetterCasing.Title),
descriptionAttribute?.Description,
descriptionAttribute?.Description ?? inputAttribute?.Description,
_optionsResolver.GetOptions(propertyInfo),
inputAttribute?.Category,
inputAttribute?.Order ?? 0,
_defaultValueResolver.GetDefaultValue(propertyInfo),
inputAttribute?.DefaultSyntax,
//inputAttribute?.SupportedSyntaxes, TODO: Come up with a different way to specify support languages for activity inputs. By default, maybe all pros should support all registered scripting languages?
//inputAttribute?.SupportedSyntaxes, TODO: Come up with a different way to specify support languages for activity inputs. By default, maybe all props should support all registered scripting languages?
inputAttribute?.IsReadOnly ?? false,
inputAttribute?.IsBrowsable ?? true
);

View file

@ -0,0 +1,27 @@
using Elsa.Extensions;
using Elsa.Models;
using Elsa.Modules.WorkflowContexts.Models;
namespace Elsa.Modules.WorkflowContexts.Extensions;
public static class WorkflowExecutionContextExtensions
{
public static void SetWorkflowContext(this WorkflowExecutionContext workflowExecutionContext, WorkflowContext workflowContext, object? value) => workflowExecutionContext.TransientProperties.SetWorkflowContext(workflowContext, value);
public static void SetWorkflowContext(this ExpressionExecutionContext expressionExecutionContext, WorkflowContext workflowContext, object? value) => expressionExecutionContext.TransientProperties.SetWorkflowContext(workflowContext, value);
public static T? GetWorkflowContext<T, TProvider>(this WorkflowExecutionContext workflowExecutionContext) => (T?)workflowExecutionContext.GetWorkflowContext(new WorkflowContext(typeof(TProvider)));
public static T? GetWorkflowContext<T, TProvider>(this ExpressionExecutionContext expressionExecutionContext) => (T?)expressionExecutionContext.TransientProperties.GetWorkflowContext(new WorkflowContext(typeof(TProvider)));
public static object? GetWorkflowContext(this WorkflowExecutionContext workflowExecutionContext, WorkflowContext workflowContext) => workflowExecutionContext.TransientProperties.GetWorkflowContext(workflowContext);
public static object? GetWorkflowContext(this ExpressionExecutionContext expressionExecutionContext, WorkflowContext workflowContext) => expressionExecutionContext.TransientProperties.GetWorkflowContext(workflowContext);
private static void SetWorkflowContext(this IDictionary<string, object?> transientProperties, WorkflowContext workflowContext, object? value)
{
var contextDictionary = transientProperties.GetOrAdd("WorkflowContexts", () => new Dictionary<Type, object?>());
contextDictionary[workflowContext.ProviderType] = value;
}
private static object? GetWorkflowContext(this IDictionary<string, object?> transientProperties, WorkflowContext workflowContext)
{
var contextDictionary = transientProperties.GetOrAdd("WorkflowContexts", () => new Dictionary<Type, object?>());
return contextDictionary[workflowContext.ProviderType];
}
}

View file

@ -1,6 +1,7 @@
using Elsa.Extensions;
using Elsa.Models;
using Elsa.Modules.WorkflowContexts.Contracts;
using Elsa.Modules.WorkflowContexts.Extensions;
using Elsa.Modules.WorkflowContexts.Models;
using Elsa.Pipelines.WorkflowExecution;
using Elsa.Pipelines.WorkflowExecution.Components;
@ -36,8 +37,7 @@ public class WorkflowContextMiddleware : WorkflowExecutionMiddleware
var value = await provider.LoadAsync(context);
// Store the loaded value into the workflow execution context.
var contextDictionary = context.TransientProperties.GetOrAdd("WorkflowContexts", () => new Dictionary<WorkflowContext, object?>());
contextDictionary.Add(workflowContext, value);
context.SetWorkflowContext(workflowContext, value);
}
// Invoke the next middleware.
@ -47,8 +47,7 @@ public class WorkflowContextMiddleware : WorkflowExecutionMiddleware
foreach (var workflowContext in workflowContexts!)
{
// Get the loaded value from the workflow execution context.
var contextDictionary = context.TransientProperties.GetOrAdd("WorkflowContexts", () => new Dictionary<WorkflowContext, object?>());
var value = contextDictionary[workflowContext];
var value = context.GetWorkflowContext(workflowContext);
var provider = (IWorkflowContextProvider)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, workflowContext.ProviderType);
await provider.SaveAsync(context, value);

View file

@ -0,0 +1,6 @@
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;
namespace Elsa.Persistence.Requests;
public record FindWorkflowBookmark(string BookmarkId) : IRequest<WorkflowBookmark?>;

View file

@ -0,0 +1,13 @@
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;
using Elsa.Persistence.EntityFrameworkCore.Contracts;
using Elsa.Persistence.Requests;
namespace Elsa.Persistence.EntityFrameworkCore.Handlers.Requests;
public class FindWorkflowBookmarkHandler : IRequestHandler<FindWorkflowBookmark, WorkflowBookmark?>
{
private readonly IStore<WorkflowBookmark> _store;
public FindWorkflowBookmarkHandler(IStore<WorkflowBookmark> store) => _store = store;
public async Task<WorkflowBookmark?> HandleAsync(FindWorkflowBookmark request, CancellationToken cancellationToken) => await _store.FindAsync(x => x.Id == request.BookmarkId, cancellationToken);
}

View file

@ -0,0 +1,16 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;
using Elsa.Persistence.InMemory.Services;
using Elsa.Persistence.Requests;
namespace Elsa.Persistence.InMemory.Handlers.Requests;
public class FindWorkflowBookmarkHandler : IRequestHandler<FindWorkflowBookmark, WorkflowBookmark?>
{
private readonly InMemoryStore<WorkflowBookmark> _store;
public FindWorkflowBookmarkHandler(InMemoryStore<WorkflowBookmark> store) => _store = store;
public Task<WorkflowBookmark?> HandleAsync(FindWorkflowBookmark request, CancellationToken cancellationToken) => Task.FromResult(FindById(request.BookmarkId));
private WorkflowBookmark? FindById(string id) => _store.Find(x => x.Id == id);
}

View file

@ -4,7 +4,6 @@ using Elsa.Contracts;
using Elsa.Models;
using Elsa.Modules.WorkflowContexts.Abstractions;
using Elsa.Modules.WorkflowContexts.Extensions;
using Elsa.Modules.WorkflowContexts.Models;
using Elsa.Runtime.Contracts;
namespace Elsa.Samples.Web1.Workflows;