Update DSL implementation and integration (#4073)

This commit is contained in:
Sipke Schoorstra 2023-05-28 11:54:12 +02:00 committed by GitHub
parent 7d56d00739
commit 6f455f7e70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 959 additions and 387 deletions

View file

@ -202,6 +202,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Environments", "src\mo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.MySql", "src\modules\Elsa.EntityFrameworkCore.MySql\Elsa.EntityFrameworkCore.MySql.csproj", "{8DC74562-1F06-4AFA-8308-8A779E8812A3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.DslWorkflowProvider", "src\samples\aspnet\Elsa.Samples.AspNet.DslWorkflowProvider\Elsa.Samples.AspNet.DslWorkflowProvider.csproj", "{4BC5D3D2-9425-41D7-9804-D145B92520EE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -516,6 +518,10 @@ Global
{8DC74562-1F06-4AFA-8308-8A779E8812A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DC74562-1F06-4AFA-8308-8A779E8812A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DC74562-1F06-4AFA-8308-8A779E8812A3}.Release|Any CPU.Build.0 = Release|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@ -606,5 +612,6 @@ Global
{165ACC9D-B67C-4B49-A818-ECFD247C899C} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{B5CDF747-8066-40D5-9BAE-CBE397BC9B51} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{8DC74562-1F06-4AFA-8308-8A779E8812A3} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{4BC5D3D2-9425-41D7-9804-D145B92520EE} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
EndGlobalSection
EndGlobal

View file

@ -14,7 +14,7 @@ namespace Elsa.Features.Implementations;
/// <inheritdoc />
public class Module : IModule
{
private record HostedServiceDescriptor(int Order, Type HostedServiceType);
private record HostedServiceDescriptor(int Order, Type Type);
private IDictionary<Type, IFeature> _features = new Dictionary<Type, IFeature>();
private readonly ISet<IFeature> _configuredFeatures = new HashSet<IFeature>();
@ -84,12 +84,12 @@ public class Module : IModule
}
foreach (var hostedServiceDescriptor in _hostedServiceDescriptors.OrderBy(x => x.Order))
Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.HostedServiceType));
Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.Type));
// Make sure to use the complete list of features when applying them.
foreach (var feature in _features.Values)
feature.Apply();
// Add a registry of enabled features to the service collection for client applications to reflect on what features are installed.
var registry = new InstalledFeatureRegistry();
foreach (var feature in _features.Values)

View file

@ -17,22 +17,22 @@ public interface IModule
/// A dictionary into which features can stash away values for later use.
/// </summary>
IDictionary<object, object> Properties { get; }
/// <summary>
/// Creates and configures a feature of the specified type.
/// </summary>
T Configure<T>(Action<T>? configure = default) where T : class, IFeature;
/// <summary>
/// Creates and configures a feature of the specified type.
/// </summary>
T Configure<T>(Func<IModule, T> factory, Action<T>? configure = default) where T : class, IFeature;
/// <summary>
/// Configures a <see cref="IHostedService"/> using an optional priority to control in which order it will be registered with the service container.
/// </summary>
IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService;
/// <summary>
/// Will apply all configured features, causing the <see cref="Services"/> collection to be populated.
/// </summary>

View file

@ -37,6 +37,7 @@ public class TestApplicationBuilder
.UseScheduling()
.UseJavaScript()
.UseLiquid()
.UseDsl()
.UseWorkflows(workflows => workflows
.WithStandardOutStreamProvider(_ => new StandardOutStreamProvider(new XunitConsoleTextWriter(_testOutputHelper)))
);

View file

@ -1,9 +1,33 @@
using Elsa.Dsl.Models;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Dsl.Contracts;
/// <summary>
/// Provides a registry for mapping functions to activities that can be invoked from a DSL script.
/// </summary>
public interface IFunctionActivityRegistry
{
void RegisterFunction(string functionName, string activityTypeName, IEnumerable<string>? propertyNames = default);
/// <summary>
/// Registers a function that is mapped to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="functionName">The name of the function.</param>
/// <param name="activityTypeName">The name of the activity type.</param>
/// <param name="propertyNames">The names of the properties that are mapped to the function arguments.</param>
/// <param name="configure">An optional action that can be used to configure the activity.</param>
void RegisterFunction(string functionName, string activityTypeName, IEnumerable<string>? propertyNames = default, Action<IActivity>? configure = default);
/// <summary>
/// Registers a function that is mapped to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="descriptor">The descriptor that describes the function.</param>
void RegisterFunction(FunctionActivityDescriptor descriptor);
/// <summary>
/// Resolves a function to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="functionName">The name of the function.</param>
/// <param name="arguments">The arguments that are passed to the function.</param>
/// <returns>An activity that can be invoked from a DSL script.</returns>
IActivity ResolveFunction(string functionName, IEnumerable<object?>? arguments = default);
}

View file

@ -5,20 +5,21 @@ namespace Elsa.Dsl.Interpreters;
public partial class WorkflowDefinitionBuilderInterpreter
{
/// <inheritdoc />
public override IWorkflowBuilder VisitBracketsExpr(ElsaParser.BracketsExprContext context)
{
var propertyType = _expressionType.Get(context.Parent);
var targetElementType = propertyType.GetGenericArguments().First();
var targetElementType = propertyType?.GetGenericArguments().First() ?? typeof(object);
var contents = context.exprList().expr();
var items = contents.Select(x =>
{
Visit(x);
var objectContext = x.GetChild<ElsaParser.ObjectContext>(0);
return _expressionValue.Get(objectContext);
var stringContext = x.GetChild<ElsaParser.StringValueExprContext>(0) ?? x;
return _expressionValue.Get(stringContext);
}).ToList();
var stronglyTypedListType = typeof(ICollection<>).MakeGenericType(targetElementType);
var stronglyTypedListType = targetElementType;
var stronglyTypedList = items.ConvertTo(stronglyTypedListType);
_expressionValue.Put(context, stronglyTypedList);

View file

@ -1,9 +1,12 @@
using Elsa.Workflows.Core.Contracts;
using System.Text.Json;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
namespace Elsa.Dsl.Interpreters;
public partial class WorkflowDefinitionBuilderInterpreter
{
/// <inheritdoc />
public override IWorkflowBuilder VisitObjectExpr(ElsaParser.ObjectExprContext context)
{
VisitChildren(context);
@ -13,6 +16,7 @@ public partial class WorkflowDefinitionBuilderInterpreter
return DefaultResult;
}
/// <inheritdoc />
public override IWorkflowBuilder VisitObjectStat(ElsaParser.ObjectStatContext context)
{
VisitChildren(context);
@ -22,9 +26,34 @@ public partial class WorkflowDefinitionBuilderInterpreter
return DefaultResult;
}
/// <inheritdoc />
public override IWorkflowBuilder VisitObject(ElsaParser.ObjectContext context)
{
var @object = GetObject(context);
_object.Put(context, @object);
_expressionValue.Put(context, @object);
VisitChildren(context);
return DefaultResult;
}
private object GetObject(ElsaParser.ObjectContext context)
{
var objectTypeName = context.ID().GetText();
// First, check if the symbol matches an activity type.
var activityType = _activityRegistry.Find(x => x.Name == objectTypeName);
if (activityType != null)
{
// TODO: Refactor this to remove the dependency on JsonElement and JsonSerializerOptions.
// This limits the ability to use this class in other contexts, such as constructing activities from the DSL.
var jsonElement = JsonSerializer.Deserialize<JsonElement>("{}");
var ctorArgs = new ActivityConstructorContext(jsonElement, new JsonSerializerOptions());
return activityType.Constructor(ctorArgs);
}
var objectTypeDescriptor = _typeSystem.ResolveTypeName(objectTypeName);
if (objectTypeDescriptor == null)
@ -35,7 +64,7 @@ public partial class WorkflowDefinitionBuilderInterpreter
_expressionValue.Put(context, definedVariable.Value);
return DefaultResult;
}
// Or a workflow variable?
var workflowVariableQuery =
from container in _containerStack
@ -50,17 +79,13 @@ public partial class WorkflowDefinitionBuilderInterpreter
_expressionValue.Put(context, workflowVariable);
return DefaultResult;
}
throw new Exception($"Unknown type: {objectTypeName}");
}
var objectType = objectTypeDescriptor.Type;
var @object = Activator.CreateInstance(objectType)!;
_object.Put(context, @object);
_expressionValue.Put(context, @object);
VisitChildren(context);
return DefaultResult;
return @object;
}
}

View file

@ -1,5 +1,6 @@
using System.Reflection;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
@ -7,6 +8,7 @@ namespace Elsa.Dsl.Interpreters;
public partial class WorkflowDefinitionBuilderInterpreter
{
/// <inheritdoc />
public override IWorkflowBuilder VisitProperty(ElsaParser.PropertyContext context)
{
var @object = _object.Get(context.Parent.Parent.Parent);
@ -21,7 +23,9 @@ public partial class WorkflowDefinitionBuilderInterpreter
VisitChildren(context);
var propertyValue = _expressionValue.Get(context.expr());
SetPropertyValue(@object, propertyInfo, propertyValue);
var propertyType = propertyInfo.PropertyType;
var parsedPropertyValue = typeof(Input).IsAssignableFrom(propertyType) ? propertyValue : propertyValue.ConvertTo(propertyType);
SetPropertyValue(@object, propertyInfo, parsedPropertyValue);
return DefaultResult;
}
@ -37,19 +41,25 @@ public partial class WorkflowDefinitionBuilderInterpreter
private Input CreateInputValue(PropertyInfo propertyInfo, object? propertyValue)
{
var underlyingType = propertyInfo.PropertyType.GetGenericArguments().First();
var propertyValueType = propertyValue?.GetType();
var parsedPropertyValue = propertyValue.ConvertTo(underlyingType);
var propertyValueType = parsedPropertyValue?.GetType();
var inputType = typeof(Input<>).MakeGenericType(underlyingType);
if (propertyValueType != null)
{
var hasCtorWithSpecifiedType = inputType.GetConstructors().Any(x => x.GetParameters().Any(y => y.ParameterType.IsAssignableFrom(propertyValueType)));
// Create a literal value.
var literalType = typeof(Literal<>).MakeGenericType(underlyingType);
var hasCtorWithSpecifiedType = inputType.GetConstructors().Any(x => x.GetParameters().Any(y => y.ParameterType.IsAssignableFrom(literalType)));
if (hasCtorWithSpecifiedType)
return (Input)Activator.CreateInstance(inputType, propertyValue)!;
{
var literalValue = Activator.CreateInstance(literalType, parsedPropertyValue)!;
return (Input)Activator.CreateInstance(inputType, literalValue)!;
}
}
return propertyValue is ExternalExpressionReference externalExpressionReference
? (Input)Activator.CreateInstance(inputType, externalExpressionReference.Expression, externalExpressionReference.BlockReference)!
: (Input)Activator.CreateInstance(inputType, propertyValue.ConvertTo(underlyingType))!;
return parsedPropertyValue is ExternalExpressionReference externalExpressionReference
? (Input)Activator.CreateInstance(inputType, externalExpressionReference.Expression, externalExpressionReference.BlockReference)!
: (Input)Activator.CreateInstance(inputType, parsedPropertyValue)!;
}
}

View file

@ -1,7 +1,6 @@
using Antlr4.Runtime.Tree;
using Elsa.Dsl.Contracts;
using Elsa.Dsl.Models;
using Elsa.Expressions.Contracts;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
@ -10,8 +9,8 @@ namespace Elsa.Dsl.Interpreters;
public partial class WorkflowDefinitionBuilderInterpreter : ElsaParserBaseVisitor<IWorkflowBuilder>
{
private readonly ITypeSystem _typeSystem;
private readonly IActivityRegistry _activityRegistry;
private readonly IFunctionActivityRegistry _functionActivityRegistry;
private readonly IExpressionHandlerRegistry _expressionHandlerRegistry;
private readonly IWorkflowBuilder _workflowBuilder;
private readonly ParseTreeProperty<object> _object = new();
private readonly ParseTreeProperty<object?> _expressionValue = new();
@ -22,22 +21,17 @@ public partial class WorkflowDefinitionBuilderInterpreter : ElsaParserBaseVisito
/// <inheritdoc />
public WorkflowDefinitionBuilderInterpreter(
ITypeSystem typeSystem,
IFunctionActivityRegistry functionActivityRegistry,
IExpressionHandlerRegistry expressionHandlerRegistry,
IWorkflowBuilderFactory workflowBuilderFactory,
WorkflowDefinitionInterpreterSettings settings)
ITypeSystem typeSystem,
IActivityRegistry activityRegistry,
IFunctionActivityRegistry functionActivityRegistry,
IWorkflowBuilderFactory workflowBuilderFactory)
{
_typeSystem = typeSystem;
_activityRegistry = activityRegistry;
_functionActivityRegistry = functionActivityRegistry;
_expressionHandlerRegistry = expressionHandlerRegistry;
_workflowBuilder = workflowBuilderFactory.CreateBuilder();
}
/// <inheritdoc />
protected override IWorkflowBuilder DefaultResult => _workflowBuilder;
private void VisitMany(IEnumerable<IParseTree> contexts)
{
foreach (var parseTree in contexts) Visit(parseTree);
}
}

View file

@ -0,0 +1,12 @@
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Dsl.Models;
/// <summary>
/// Describes a function that is mapped to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="FunctionName">The name of the function.</param>
/// <param name="ActivityTypeName">The name of the activity type.</param>
/// <param name="PropertyNames">The names of the properties that are mapped to the function arguments.</param>
/// <param name="Configure">An optional action that can be used to configure the activity.</param>
public record FunctionActivityDescriptor(string FunctionName, string ActivityTypeName, IEnumerable<string>? PropertyNames = default, Action<IActivity>? Configure = default);

View file

@ -1,8 +1,6 @@
using Antlr4.Runtime;
using Elsa.Dsl.Contracts;
using Elsa.Dsl.Interpreters;
using Elsa.Dsl.Models;
using Elsa.Expressions.Contracts;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
@ -12,8 +10,8 @@ namespace Elsa.Dsl.Services;
public class DslEngine : IDslEngine
{
private readonly ITypeSystem _typeSystem;
private readonly IActivityRegistry _activityRegistry;
private readonly IFunctionActivityRegistry _functionActivityRegistry;
private readonly IExpressionHandlerRegistry _expressionHandlerRegistry;
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
/// <summary>
@ -21,13 +19,13 @@ public class DslEngine : IDslEngine
/// </summary>
public DslEngine(
ITypeSystem typeSystem,
IActivityRegistry activityRegistry,
IFunctionActivityRegistry functionActivityRegistry,
IExpressionHandlerRegistry expressionHandlerRegistry,
IWorkflowBuilderFactory workflowBuilderFactory)
{
_typeSystem = typeSystem;
_activityRegistry = activityRegistry;
_functionActivityRegistry = functionActivityRegistry;
_expressionHandlerRegistry = expressionHandlerRegistry;
_workflowBuilderFactory = workflowBuilderFactory;
}
@ -39,7 +37,13 @@ public class DslEngine : IDslEngine
var tokens = new CommonTokenStream(lexer);
var parser = new ElsaParser(tokens);
var tree = parser.program();
var interpreter = new WorkflowDefinitionBuilderInterpreter(_typeSystem, _functionActivityRegistry, _expressionHandlerRegistry, _workflowBuilderFactory, new WorkflowDefinitionInterpreterSettings());
var interpreter = new WorkflowDefinitionBuilderInterpreter(
_typeSystem,
_activityRegistry,
_functionActivityRegistry,
_workflowBuilderFactory);
var workflowBuilder = interpreter.Visit(tree);
var workflow = await workflowBuilder.BuildWorkflowAsync(cancellationToken);

View file

@ -1,60 +1,67 @@
using System.Reflection;
using System.Text.Json;
using Elsa.Dsl.Contracts;
using Elsa.Dsl.Interpreters;
using Elsa.Dsl.Models;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
namespace Elsa.Dsl.Services;
/// <inheritdoc />
public class FunctionActivityRegistry : IFunctionActivityRegistry
{
private readonly ITypeSystem _typeSystem;
private readonly IActivityRegistry _activityRegistry;
private readonly IDictionary<string, FunctionActivityDescriptor> _dictionary = new Dictionary<string, FunctionActivityDescriptor>();
public FunctionActivityRegistry(ITypeSystem typeSystem)
/// <summary>
/// Creates a new instance of the <see cref="FunctionActivityRegistry"/> class.
/// </summary>
public FunctionActivityRegistry(IActivityRegistry activityRegistry)
{
_typeSystem = typeSystem;
_activityRegistry = activityRegistry;
}
public void RegisterFunction(string functionName, string activityTypeName, IEnumerable<string>? propertyNames = default)
/// <inheritdoc />
public void RegisterFunction(string functionName, string activityTypeName, IEnumerable<string>? propertyNames = default, Action<IActivity>? configure = default)
{
var typeDescriptor = _typeSystem.ResolveTypeName(activityTypeName);
if (typeDescriptor == null)
throw new Exception($"Could not find activity type {activityTypeName}. Did you forget to register it?");
if (typeDescriptor.Kind != TypeKind.Activity)
throw new Exception($"Only activity types can be mapped to functions. You are trying to map {typeDescriptor.Type.Name}, which is a different kind: {typeDescriptor.Kind}");
var activityType = typeDescriptor.Type;
var propertyNameList = propertyNames?.ToList() ?? new List<string>();
var properties = propertyNameList.Select(propertyName =>
{
var property = activityType.GetProperties().FirstOrDefault(x => x.Name == propertyName);
if (property == null)
throw new Exception($"Activity type {typeDescriptor.Type.Name} does not have a property named {propertyName}");
return property;
}).ToList();
var descriptor = new FunctionActivityDescriptor(activityType, properties);
_dictionary.Add(functionName, descriptor);
var descriptor = new FunctionActivityDescriptor(functionName, activityTypeName, propertyNames, configure);
RegisterFunction(descriptor);
}
/// <inheritdoc />
public void RegisterFunction(FunctionActivityDescriptor descriptor)
{
_dictionary.Add(descriptor.FunctionName, descriptor);
}
/// <inheritdoc />
public IActivity ResolveFunction(string functionName, IEnumerable<object?>? arguments = default)
{
if (!_dictionary.TryGetValue(functionName, out var descriptor))
throw new Exception($"Could not resolve function {functionName}. Did you forget to register it?");
var activityType = descriptor.ActivityType;
var activity = (IActivity)Activator.CreateInstance(activityType)!;
var activityDescriptor = _activityRegistry.Find(x => x.Name == descriptor.ActivityTypeName || x.TypeName == descriptor.ActivityTypeName);
if (activityDescriptor == null)
throw new Exception($"Could not find activity descriptor for activity type {descriptor.ActivityTypeName}");
var propertyNameList = descriptor.PropertyNames?.ToList() ?? new List<string>();
var propertyDescriptors = activityDescriptor.Inputs.Cast<PropertyDescriptor>().Concat(activityDescriptor.Outputs).ToList();
var properties = propertyNameList
.Select(propertyName => propertyDescriptors.FirstOrDefault(x => x.Name == propertyName))
.Where(x => x != null)
.Select(x => x!)
.ToList();
var dummyJsonElement = JsonDocument.Parse("{}").RootElement;
var constructorContext = new ActivityConstructorContext(dummyJsonElement, new JsonSerializerOptions());
var activity = activityDescriptor.Constructor(constructorContext);
// Apply each argument in order of the described properties.
var index = 0;
var properties = descriptor.Properties.ToList();
if (arguments != null)
foreach (var argument in arguments)
@ -63,50 +70,57 @@ public class FunctionActivityRegistry : IFunctionActivityRegistry
SetPropertyValue(activity, property, argument);
}
descriptor.Configure?.Invoke(activity);
return activity;
}
private void SetPropertyValue(object target, PropertyInfo propertyInfo, object? value)
{
if (typeof(Input).IsAssignableFrom(propertyInfo.PropertyType))
value = CreateInputValue(propertyInfo, value);
else if (typeof(Output).IsAssignableFrom(propertyInfo.PropertyType))
value = CreateOutputValue(propertyInfo, value);
propertyInfo.SetValue(target, value, null);
private void SetPropertyValue(IActivity target, PropertyDescriptor propertyDescriptor, object? value)
{
value = propertyDescriptor switch
{
InputDescriptor inputDescriptor => CreateInputValue(inputDescriptor, value),
OutputDescriptor outputDescriptor => CreateOutputValue(outputDescriptor, value),
_ => value
};
propertyDescriptor.ValueSetter(target, value);
}
private Input CreateInputValue(PropertyInfo propertyInfo, object? propertyValue)
private Input CreateInputValue(InputDescriptor inputDescriptor, object? propertyValue)
{
if (propertyValue is Input input)
return input;
var underlyingType = propertyInfo.PropertyType.GetGenericArguments().First();
var propertyValueType = propertyValue?.GetType();
var underlyingType = inputDescriptor.Type;
var parsedPropertyValue = propertyValue.ConvertTo(underlyingType);
var propertyValueType = parsedPropertyValue?.GetType();
var inputType = typeof(Input<>).MakeGenericType(underlyingType);
if (propertyValue is ExternalExpressionReference externalExpressionReference)
if (parsedPropertyValue is ExternalExpressionReference externalExpressionReference)
return (Input)Activator.CreateInstance(inputType, externalExpressionReference.Expression, externalExpressionReference.BlockReference)!;
if (propertyValueType != null)
{
var hasCtorWithSpecifiedType = inputType.GetConstructors().Any(x => x.GetParameters().Any(y => y.ParameterType.IsAssignableFrom(propertyValueType)));
// Create a literal value.
var literalType = typeof(Literal<>).MakeGenericType(underlyingType);
var hasCtorWithSpecifiedType = inputType.GetConstructors().Any(x => x.GetParameters().Any(y => y.ParameterType.IsAssignableFrom(literalType)));
if (hasCtorWithSpecifiedType)
return (Input)Activator.CreateInstance(inputType, propertyValue)!;
{
var literalValue = Activator.CreateInstance(literalType, parsedPropertyValue)!;
return (Input)Activator.CreateInstance(inputType, literalValue)!;
}
}
var convertedValue = propertyValue.ConvertTo(underlyingType);
return (Input)Activator.CreateInstance(inputType, convertedValue)!;
return (Input)Activator.CreateInstance(inputType, parsedPropertyValue)!;
}
private Output CreateOutputValue(PropertyInfo propertyInfo, object? propertyValue)
private Output CreateOutputValue(OutputDescriptor outputDescriptor, object? propertyValue)
{
if (propertyValue is Output output)
return output;
var underlyingType = propertyInfo.PropertyType.GetGenericArguments().First();
var underlyingType = outputDescriptor.Type;
var propertyValueType = propertyValue?.GetType();
var outputType = typeof(Output<>).MakeGenericType(underlyingType);
@ -122,6 +136,4 @@ public class FunctionActivityRegistry : IFunctionActivityRegistry
return (Output)Activator.CreateInstance(outputType, convertedValue)!;
}
private record FunctionActivityDescriptor(Type ActivityType, ICollection<PropertyInfo> Properties);
}

View file

@ -15,7 +15,11 @@ public abstract class PersistenceFeatureBase<TDbContext> : FeatureBase where TDb
public bool UseContextPooling { get; set; }
public bool RunMigrations { get; set; } = true;
public ServiceLifetime DbContextFactoryLifetime { get; set; } = ServiceLifetime.Singleton;
public Action<IServiceProvider, DbContextOptionsBuilder> DbContextOptionsBuilder = (_, _) => { };
public Action<IServiceProvider, DbContextOptionsBuilder> DbContextOptionsBuilder = (_, options) => options
.UseSqlite("Data Source=elsa.sqlite.db;Cache=Shared;", sqlite => sqlite
.MigrationsAssembly("Elsa.EntityFrameworkCore.Sqlite")
.MigrationsHistoryTable(ElsaDbContextBase.MigrationsHistoryTable, ElsaDbContextBase.ElsaSchema));
public override void ConfigureHostedServices()
{

View file

@ -15,10 +15,10 @@
<ProjectReference Include="..\Elsa.Expressions\Elsa.Expressions.csproj" />
<ProjectReference Include="..\Elsa.Mediator\Elsa.Mediator.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Jint" Version="3.0.0-beta-2044" />
</ItemGroup>

View file

@ -4,7 +4,6 @@ using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.JavaScript.Activities;
using Elsa.JavaScript.Contracts;
using Elsa.JavaScript.Expressions;
using Elsa.JavaScript.Extensions;
@ -14,7 +13,6 @@ using Elsa.JavaScript.TypeDefinitions.Contracts;
using Elsa.JavaScript.TypeDefinitions.Providers;
using Elsa.JavaScript.TypeDefinitions.Services;
using Elsa.Mediator.Features;
using Elsa.Workflows.Core.Contracts;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.JavaScript.Features;
@ -31,12 +29,6 @@ public class JavaScriptFeature : FeatureBase
{
}
/// <inheritdoc />
public override void Configure()
{
Module.UseWorkflowManagement(management => management.AddActivitiesFrom<JavaScriptFeature>());
}
/// <inheritdoc />
public override void Apply()
{
@ -44,7 +36,6 @@ public class JavaScriptFeature : FeatureBase
Services
.AddSingleton<IExpressionSyntaxProvider, JavaScriptExpressionSyntaxProvider>()
.AddSingleton<IJavaScriptEvaluator, JintJavaScriptEvaluator>()
.AddSingleton<IActivityPropertyOptionsProvider, RunJavaScriptOptionsProvider>()
.AddSingleton<ITypeDefinitionService, TypeDefinitionService>()
.AddExpressionHandler<JavaScriptExpressionHandler, JavaScriptExpression>()
;

View file

@ -1,8 +1,6 @@
using System.Dynamic;
using Elsa.JavaScript.Contracts;
using Elsa.JavaScript.Extensions;
using Elsa.Workflows.Management.Options;
using Microsoft.Extensions.Options;
namespace Elsa.JavaScript.Services;
@ -14,7 +12,7 @@ public class TypeAliasRegistry : ITypeAliasRegistry
/// <summary>
/// Constructor.
/// </summary>
public TypeAliasRegistry(IOptions<ManagementOptions> managementOptions)
public TypeAliasRegistry()
{
this.RegisterType<object>("any");
this.RegisterType<ExpandoObject>("any");
@ -30,12 +28,6 @@ public class TypeAliasRegistry : ITypeAliasRegistry
this.RegisterType<DateTimeOffset>("Date");
this.RegisterType<DateOnly>("Date");
this.RegisterType<TimeOnly>("Date");
foreach (var variableDescriptor in managementOptions.Value.VariableDescriptors)
{
if(!_typeAliasDictionary.ContainsKey(variableDescriptor.Type))
RegisterType(variableDescriptor.Type, variableDescriptor.Type.Name);
}
}
/// <inheritdoc />

View file

@ -1,8 +1,8 @@
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Core.Models;
namespace Elsa.JavaScript.TypeDefinitions.Models;
/// <summary>
/// Provides context to intellisense providers.
/// </summary>
public record TypeDefinitionContext(WorkflowDefinition WorkflowDefinition, string? ActivityTypeName, string? PropertyName, CancellationToken CancellationToken);
public record TypeDefinitionContext(Workflow Workflow, string? ActivityTypeName, string? PropertyName, CancellationToken CancellationToken);

View file

@ -80,7 +80,7 @@ internal class CommonFunctionsDefinitionProvider : FunctionDefinitionProvider
.ReturnType("string"));
// Variable getter and setters.
foreach (var variable in context.WorkflowDefinition.Variables)
foreach (var variable in context.Workflow.Variables)
{
var pascalName = variable.Name.Pascalize();
var variableType = variable.GetVariableType();
@ -92,16 +92,5 @@ internal class CommonFunctionsDefinitionProvider : FunctionDefinitionProvider
// set{Variable}.
yield return CreateFunctionDefinition(builder => builder.Name($"set{pascalName}").Parameter("value", typeAlias));
}
// Input argument getters.
foreach (var input in context.WorkflowDefinition.Inputs)
{
var pascalName = input.Name.Pascalize();
var variableType = input.Type;
var typeAlias = _typeAliasRegistry.TryGetAlias(variableType, out var alias) ? alias : "any";
// get{Input}.
yield return CreateFunctionDefinition(builder => builder.Name($"get{pascalName}").ReturnType(typeAlias));
}
}
}

View file

@ -27,7 +27,7 @@ internal class VariableTypeDefinitionProvider : TypeDefinitionProvider
type => type == typeof(object)
};
var variables = context.WorkflowDefinition.Variables;
var variables = context.Workflow.Variables;
var variableTypeQuery =
from variable in variables

View file

@ -50,7 +50,7 @@ public class ConfigureJavaScriptEngine : INotificationHandler<EvaluatingJavaScri
/// <inheritdoc />
public ValueTask<IEnumerable<TypeDefinition>> GetTypeDefinitionsAsync(TypeDefinitionContext context)
{
var providerTypes = GetProviderTypes(context.WorkflowDefinition);
var providerTypes = GetProviderTypes(context.Workflow);
var contextTypes = providerTypes.Select(x => x.GetWorkflowContextType());
var typeDefinitions = contextTypes.Select(x => _typeDescriber.DescribeType(x));
return new(typeDefinitions);
@ -59,7 +59,7 @@ public class ConfigureJavaScriptEngine : INotificationHandler<EvaluatingJavaScri
/// <inheritdoc />
public ValueTask<IEnumerable<FunctionDefinition>> GetFunctionDefinitionsAsync(TypeDefinitionContext context)
{
var providerTypes = GetProviderTypes(context.WorkflowDefinition);
var providerTypes = GetProviderTypes(context.Workflow);
var functionDefinitions = BuildFunctionDefinitions(providerTypes);
return new(functionDefinitions);
}
@ -80,5 +80,5 @@ public class ConfigureJavaScriptEngine : INotificationHandler<EvaluatingJavaScri
}
private IEnumerable<Type> GetProviderTypes(WorkflowExecutionContext workflowExecutionContext) => workflowExecutionContext.Workflow.GetWorkflowContextProviderTypes();
private IEnumerable<Type> GetProviderTypes(WorkflowDefinition workflowDefinition) => workflowDefinition.GetWorkflowContextProviderTypes();
private IEnumerable<Type> GetProviderTypes(Workflow workflow) => workflow.GetWorkflowContextProviderTypes();
}

View file

@ -3,7 +3,7 @@ using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.WorkflowProviders.FluentStorage.Contracts;
using Elsa.WorkflowProviders.FluentStorage.Services;
using Elsa.WorkflowProviders.FluentStorage.Providers;
using Elsa.Workflows.Management.Features;
using FluentStorage;
using FluentStorage.Blobs;
@ -33,7 +33,7 @@ public class FluentStorageFeature : FeatureBase
public override void Apply()
{
Services.AddSingleton<IBlobStorageProvider>(sp => new BlobStorageProvider(BlobStorage(sp)));
Services.AddWorkflowDefinitionProvider<FluentStorageWorkflowDefinitionProvider>();
Services.AddWorkflowDefinitionProvider<FluentStorageWorkflowProvider>();
}
private static string GetDefaultWorkflowsDirectory()

View file

@ -1,7 +1,7 @@
using Elsa.WorkflowProviders.FluentStorage.Contracts;
using FluentStorage.Blobs;
namespace Elsa.WorkflowProviders.FluentStorage.Services;
namespace Elsa.WorkflowProviders.FluentStorage.Providers;
/// <summary>
/// A provider of <see cref="IBlobStorage"/>.

View file

@ -0,0 +1,110 @@
using Elsa.Common.Contracts;
using Elsa.Dsl.Contracts;
using Elsa.WorkflowProviders.FluentStorage.Contracts;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Models;
using FluentStorage.Blobs;
using JetBrains.Annotations;
namespace Elsa.WorkflowProviders.FluentStorage.Providers;
/// <summary>
/// A workflow definition provider that loads workflow definitions from a storage using FluentStorage (See https://github.com/robinrodricks/FluentStorage).
/// </summary>
[PublicAPI]
public class FluentStorageWorkflowProvider : IWorkflowProvider
{
private readonly IBlobStorageProvider _blobStorageProvider;
private readonly IActivitySerializer _activitySerializer;
private readonly IDslEngine _dslEngine;
private readonly ISystemClock _systemClock;
private readonly IHasher _hasher;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
/// <summary>
/// Initializes a new instance of the <see cref="FluentStorageWorkflowProvider"/> class.
/// </summary>
public FluentStorageWorkflowProvider(
IBlobStorageProvider blobStorageProvider,
IActivitySerializer activitySerializer,
IDslEngine dslEngine,
ISystemClock systemClock,
IHasher hasher,
WorkflowDefinitionMapper workflowDefinitionMapper,
VariableDefinitionMapper variableDefinitionMapper)
{
_blobStorageProvider = blobStorageProvider;
_activitySerializer = activitySerializer;
_dslEngine = dslEngine;
_systemClock = systemClock;
_hasher = hasher;
_workflowDefinitionMapper = workflowDefinitionMapper;
_variableDefinitionMapper = variableDefinitionMapper;
}
/// <inheritdoc />
public string Name => "FluentStorage";
/// <inheritdoc />
public async ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default)
{
var options = new ListOptions
{
Recurse = true,
};
var blobStorage = _blobStorageProvider.GetBlobStorage();
var blobs = await blobStorage.ListFilesAsync(options, cancellationToken);
var results = new List<MaterializedWorkflow>();
foreach (var blob in blobs)
{
var result = await ReadWorkflowDefinitionAsync(blob, cancellationToken);
results.Add(result);
}
return results;
}
private async Task<MaterializedWorkflow> ReadWorkflowDefinitionAsync(Blob blob, CancellationToken cancellationToken)
{
var blobStorage = _blobStorageProvider.GetBlobStorage();
var fileExtension = blob.FullPath.Split('.').Last();
var data = await blobStorage.ReadTextAsync(blob.FullPath, cancellationToken: cancellationToken);
if (string.Equals("json", fileExtension, StringComparison.OrdinalIgnoreCase))
return ReadJsonWorkflowDefinition(data);
if (string.Equals("elsa", fileExtension, StringComparison.OrdinalIgnoreCase))
return await ReadElsaDslWorkflowDefinitionAsync(blob, data, cancellationToken);
throw new NotSupportedException($"The file extension '{fileExtension}' is not supported.");
}
private async Task<MaterializedWorkflow> ReadElsaDslWorkflowDefinitionAsync(Blob blob, string dsl, CancellationToken cancellationToken)
{
var workflow = await _dslEngine.ParseAsync(dsl, cancellationToken);
// TODO: Extend the DSL with support for setting the ID from there.
workflow.Identity = workflow.Identity with
{
Id = blob.Name,
DefinitionId = blob.Name
};
return new MaterializedWorkflow(workflow, JsonWorkflowMaterializer.MaterializerName);
}
private MaterializedWorkflow ReadJsonWorkflowDefinition(string json)
{
var workflowDefinitionModel = _activitySerializer.Deserialize<WorkflowDefinitionModel>(json);
var workflow = _workflowDefinitionMapper.Map(workflowDefinitionModel);
return new MaterializedWorkflow(workflow, JsonWorkflowMaterializer.MaterializerName);
}
}

View file

@ -1,96 +0,0 @@
using Elsa.Common.Contracts;
using Elsa.WorkflowProviders.FluentStorage.Contracts;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Models;
using FluentStorage.Blobs;
using JetBrains.Annotations;
namespace Elsa.WorkflowProviders.FluentStorage.Services;
/// <summary>
/// A workflow definition provider that loads workflow definitions from a storage using FluentStorage (See https://github.com/robinrodricks/FluentStorage).
/// </summary>
[PublicAPI]
public class FluentStorageWorkflowDefinitionProvider : IWorkflowDefinitionProvider
{
private readonly IBlobStorageProvider _blobStorageProvider;
private readonly IActivitySerializer _activitySerializer;
private readonly ISystemClock _systemClock;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
/// <summary>
/// Initializes a new instance of the <see cref="FluentStorageWorkflowDefinitionProvider"/> class.
/// </summary>
public FluentStorageWorkflowDefinitionProvider(
IBlobStorageProvider blobStorageProvider,
IActivitySerializer activitySerializer,
ISystemClock systemClock,
WorkflowDefinitionMapper workflowDefinitionMapper,
VariableDefinitionMapper variableDefinitionMapper)
{
_blobStorageProvider = blobStorageProvider;
_activitySerializer = activitySerializer;
_systemClock = systemClock;
_workflowDefinitionMapper = workflowDefinitionMapper;
_variableDefinitionMapper = variableDefinitionMapper;
}
/// <inheritdoc />
public string Name => "FluentStorage";
/// <inheritdoc />
public async ValueTask<IEnumerable<WorkflowDefinitionResult>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default)
{
var options = new ListOptions
{
Recurse = true,
};
var blobStorage = _blobStorageProvider.GetBlobStorage();
var blobs = await blobStorage.ListFilesAsync(options, cancellationToken);
var results = new List<WorkflowDefinitionResult>();
foreach (var blob in blobs)
{
var result = await ReadWorkflowDefinitionAsync(blob, cancellationToken);
results.Add(result);
}
return results;
}
private async Task<WorkflowDefinitionResult> ReadWorkflowDefinitionAsync(Blob blob, CancellationToken cancellationToken)
{
var blobStorage = _blobStorageProvider.GetBlobStorage();
var workflowJson = await blobStorage.ReadTextAsync(blob.FullPath, cancellationToken: cancellationToken);
var workflowDefinitionModel = _activitySerializer.Deserialize<WorkflowDefinitionModel>(workflowJson);
var variables = _variableDefinitionMapper.Map(workflowDefinitionModel.Variables).ToList();
var rootJson = _activitySerializer.Serialize(workflowDefinitionModel.Root!);
var definition = new WorkflowDefinition
{
Id = workflowDefinitionModel.Id,
DefinitionId = workflowDefinitionModel.DefinitionId,
Version = workflowDefinitionModel.Version,
Name = workflowDefinitionModel.Name,
Description = workflowDefinitionModel.Description,
CustomProperties = workflowDefinitionModel.CustomProperties ?? new Dictionary<string, object>(),
Variables = variables,
IsLatest = workflowDefinitionModel.IsLatest,
IsPublished = workflowDefinitionModel.IsPublished,
CreatedAt = workflowDefinitionModel.CreatedAt == default ? _systemClock.UtcNow : workflowDefinitionModel.CreatedAt,
MaterializerName = JsonWorkflowMaterializer.MaterializerName,
StringData = rootJson
};
var workflow = _workflowDefinitionMapper.Map(definition);
return new WorkflowDefinitionResult(definition, workflow);
}
}

View file

@ -2,6 +2,7 @@ using System.Text;
using Elsa.Common.Models;
using Elsa.JavaScript.TypeDefinitions.Contracts;
using Elsa.JavaScript.TypeDefinitions.Models;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Contracts;
@ -18,13 +19,13 @@ namespace Elsa.Workflows.Api.Endpoints.Scripting.JavaScript.TypeDefinitions;
internal class Get : Endpoint<Request>
{
private readonly ITypeDefinitionService _typeDefinitionService;
private readonly IServiceProvider _serviceProvider;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
/// <inheritdoc />
public Get(ITypeDefinitionService typeDefinitionService, IServiceProvider serviceProvider)
public Get(ITypeDefinitionService typeDefinitionService, IWorkflowDefinitionService workflowDefinitionService)
{
_typeDefinitionService = typeDefinitionService;
_serviceProvider = serviceProvider;
_workflowDefinitionService = workflowDefinitionService;
}
/// <inheritdoc />
@ -53,11 +54,14 @@ internal class Get : Endpoint<Request>
await SendBytesAsync(data, fileName, "application/x-typescript", cancellation: cancellationToken);
}
private async Task<WorkflowDefinition?> GetWorkflowDefinition(string workflowDefinitionId, CancellationToken cancellationToken)
private async Task<Workflow?> GetWorkflowDefinition(string workflowDefinitionId, CancellationToken cancellationToken)
{
var workflowDefinitionService = _serviceProvider.GetService<IWorkflowDefinitionService>();
var workflowDefinition = workflowDefinitionService != null ? await workflowDefinitionService.FindAsync(workflowDefinitionId, VersionOptions.Latest, cancellationToken) : default;
return workflowDefinition;
var workflowDefinition = await _workflowDefinitionService.FindAsync(workflowDefinitionId, VersionOptions.Latest, cancellationToken);
if (workflowDefinition == null)
return null;
return await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken);
}
}

View file

@ -11,13 +11,54 @@ namespace Elsa.Workflows.Core.Models;
[DebuggerDisplay("{TypeName}")]
public class ActivityDescriptor
{
/// <summary>
/// The fully qualified name of the activity type.
/// </summary>
public string TypeName { get; set; } = default!;
/// <summary>
/// The namespace of the activity type.
/// </summary>
public string Namespace { get; set; } = default!;
/// <summary>
/// The name of the activity type.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// The version of the activity type.
/// </summary>
public int Version { get; set; }
/// <summary>
/// The category of the activity type.
/// </summary>
public string Category { get; set; } = default!;
/// <summary>
/// The display name of the activity type.
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// The description of the activity type.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The input properties of the activity type.
/// </summary>
public ICollection<InputDescriptor> Inputs { get; init; } = new List<InputDescriptor>();
/// <summary>
/// The output properties of the activity type.
/// </summary>
public ICollection<OutputDescriptor> Outputs { get; init; } = new List<OutputDescriptor>();
/// <summary>
/// The attributes of the activity type.
/// </summary>
[JsonIgnore] public ICollection<Attribute> Attributes { get; set; } = new List<Attribute>();
/// <summary>
@ -26,8 +67,19 @@ public class ActivityDescriptor
[JsonIgnore]
public Func<ActivityConstructorContext, IActivity> Constructor { get; init; } = default!;
/// <summary>
/// The kind of activity.
/// </summary>
public ActivityKind Kind { get; set; } = ActivityKind.Action;
/// <summary>
/// The ports of the activity type.
/// </summary>
public ICollection<Port> Ports { get; init; } = new List<Port>();
/// <summary>
/// The custom properties of the activity type.
/// </summary>
public IDictionary<string, object> CustomProperties { get; set; } = new Dictionary<string, object>();
/// <summary>
@ -41,4 +93,6 @@ public class ActivityDescriptor
public bool IsBrowsable { get; set; }
}
// TODO: Refactor this to remove the dependency on JsonElement and JsonSerializerOptions.
// This limits the ability to use this class in other contexts, such as constructing activities from the DSL.
public record ActivityConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions);

View file

@ -33,7 +33,7 @@ public class ActivityDescriber : IActivityDescriber
public async Task<ActivityDescriptor> DescribeActivityAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken = default)
{
var activityAttr = activityType.GetCustomAttribute<ActivityAttribute>();
var ns = activityAttr?.Namespace ?? ActivityTypeNameHelper.GenerateNamespace(activityType);
var ns = activityAttr?.Namespace ?? ActivityTypeNameHelper.GenerateNamespace(activityType) ?? "Elsa";
var typeName = activityAttr?.Type ?? activityType.Name;
var typeVersion = activityAttr?.Version ?? 1;
var fullTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType);
@ -73,9 +73,11 @@ public class ActivityDescriber : IActivityDescriber
var descriptor = new ActivityDescriptor
{
TypeName = fullTypeName,
Namespace = ns,
Name = typeName,
Category = category,
Description = description,
TypeName = fullTypeName,
Version = typeVersion,
DisplayName = displayName,
Kind = isTrigger ? ActivityKind.Trigger : activityAttr?.Kind ?? ActivityKind.Action,

View file

@ -18,6 +18,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Dsl\Elsa.Dsl.csproj" />
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
<ProjectReference Include="..\Elsa.Mediator\Elsa.Mediator.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
</ItemGroup>

View file

@ -23,7 +23,7 @@ public static class ModuleExtensions
});
return module;
}
/// <summary>
/// Adds the default workflow management feature to the specified module.
/// </summary>
@ -32,7 +32,7 @@ public static class ModuleExtensions
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Adds the workflow instance feature to workflow management module.
/// </summary>
@ -42,13 +42,31 @@ public static class ModuleExtensions
return feature;
}
/// <summary>
/// Adds the JavaScript integration feature.
/// </summary>
public static WorkflowManagementFeature UseJavaScriptIntegration(this WorkflowManagementFeature feature, Action<JavaScriptIntegrationFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Adds the Elsa DSL integration feature.
/// </summary>
public static WorkflowManagementFeature UseDslIntegration(this WorkflowManagementFeature feature, Action<DslIntegrationFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Adds all types implementing <see cref="IActivity"/> to the system.
/// </summary>
public static IModule AddActivitiesFrom<TMarkerType>(this IModule module) => module.UseWorkflowManagement(management => management.AddActivitiesFrom<TMarkerType>());
/// <summary>
/// Adds the specified activity type to the system.
/// </summary>
public static IModule AddActivity<T>(this IModule module) where T:IActivity => module.UseWorkflowManagement(management => management.AddActivity<T>());
public static IModule AddActivity<T>(this IModule module) where T : IActivity => module.UseWorkflowManagement(management => management.AddActivity<T>());
}

View file

@ -0,0 +1,54 @@
using Elsa.Dsl.Features;
using Elsa.Dsl.Models;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.HostedServices;
using Elsa.Workflows.Management.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Management.Features;
/// <summary>
/// Installs JavaScript activities.
/// </summary>
[DependsOn(typeof(DslFeature))]
[PublicAPI]
public class DslIntegrationFeature : FeatureBase
{
private readonly IDictionary<string, FunctionActivityDescriptor> _dictionary = new Dictionary<string, FunctionActivityDescriptor>();
/// <inheritdoc />
public DslIntegrationFeature(IModule module) : base(module)
{
}
/// <summary>
/// Maps a function to an activity.
/// </summary>
/// <param name="functionName">The name of the function.</param>
/// <param name="activityTypeName">The name of the activity type.</param>
/// <param name="propertyNames">The names of the properties that are mapped to the function arguments.</param>
/// <param name="configure">An optional action that can be used to configure the activity.</param>
public void MapActivityFunction(string functionName, string activityTypeName, IEnumerable<string>? propertyNames = default, Action<IActivity>? configure = default)
{
_dictionary.Add(functionName, new FunctionActivityDescriptor(functionName, activityTypeName, propertyNames, configure));
}
/// <inheritdoc />
public override void ConfigureHostedServices()
{
ConfigureHostedService<MapActivityDslFunctionsHostedService>(-1);
}
/// <inheritdoc />
public override void Apply()
{
Services.Configure<DslIntegrationOptions>(options =>
{
options.FunctionActivityDescriptors = _dictionary;
});
}
}

View file

@ -0,0 +1,47 @@
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.JavaScript.Activities;
using Elsa.JavaScript.Extensions;
using Elsa.JavaScript.Features;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.HostedServices;
using Elsa.Workflows.Management.Scripting.JavaScript;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Management.Features;
/// <summary>
/// Installs JavaScript activities.
/// </summary>
[DependsOn(typeof(JavaScriptFeature))]
[PublicAPI]
public class JavaScriptIntegrationFeature : FeatureBase
{
/// <inheritdoc />
public JavaScriptIntegrationFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Configure()
{
Module.UseWorkflowManagement(management => management.AddActivity<RunJavaScript>());
}
/// <inheritdoc />
public override void ConfigureHostedServices()
{
ConfigureHostedService<RegisterVariableTypesWithJavaScriptHostedService>();
}
/// <inheritdoc />
public override void Apply()
{
Services
.AddSingleton<IActivityPropertyOptionsProvider, RunJavaScriptOptionsProvider>()
.AddFunctionDefinitionProvider<InputFunctionsDefinitionProvider>();
}
}

View file

@ -13,6 +13,7 @@ using Elsa.Workflows.Core.Features;
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.HostedServices;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
@ -138,6 +139,7 @@ public class WorkflowManagementFeature : FeatureBase
.AddMemoryStore<WorkflowDefinition, MemoryWorkflowDefinitionStore>()
.AddMemoryStore<WorkflowInstance, MemoryWorkflowInstanceStore>()
.AddActivityProvider<TypedActivityProvider>()
.AddSingleton<IWorkflowDefinitionService, WorkflowDefinitionService>()
.AddSingleton<IWorkflowDefinitionPublisher, WorkflowDefinitionPublisher>()
.AddSingleton<IWorkflowDefinitionImporter, WorkflowDefinitionImporter>()
.AddSingleton<IWorkflowDefinitionManager, WorkflowDefinitionManager>()

View file

@ -0,0 +1,37 @@
using Elsa.Dsl.Contracts;
using Elsa.Dsl.Models;
using Elsa.Workflows.Management.Options;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Management.HostedServices;
/// <summary>
/// Registers function activity descriptors with the DSL.
/// </summary>
public class MapActivityDslFunctionsHostedService : IHostedService
{
private readonly IFunctionActivityRegistry _functionActivityRegistry;
private readonly IDictionary<string,FunctionActivityDescriptor> _descriptors;
/// <summary>
/// Initializes a new instance of the <see cref="MapActivityDslFunctionsHostedService"/> class.
/// </summary>
public MapActivityDslFunctionsHostedService(IOptions<DslIntegrationOptions> options, IFunctionActivityRegistry functionActivityRegistry)
{
_functionActivityRegistry = functionActivityRegistry;
_descriptors = options.Value.FunctionActivityDescriptors;
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
foreach (var descriptor in _descriptors.Values)
_functionActivityRegistry.RegisterFunction(descriptor);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View file

@ -0,0 +1,36 @@
using Elsa.JavaScript.Contracts;
using Elsa.Workflows.Management.Options;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Management.HostedServices;
/// <summary>
/// Registers variable types with the JavaScript type alias registry
/// </summary>
public class RegisterVariableTypesWithJavaScriptHostedService : IHostedService
{
private readonly ITypeAliasRegistry _typeAliasRegistry;
private readonly IOptions<ManagementOptions> _managementOptions;
/// <summary>
/// Initializes a new instance of the <see cref="RegisterVariableTypesWithJavaScriptHostedService"/> class.
/// </summary>
public RegisterVariableTypesWithJavaScriptHostedService(ITypeAliasRegistry typeAliasRegistry, IOptions<ManagementOptions> managementOptions)
{
_typeAliasRegistry = typeAliasRegistry;
_managementOptions = managementOptions;
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
foreach (var variableDescriptor in _managementOptions.Value.VariableDescriptors)
_typeAliasRegistry.RegisterType(variableDescriptor.Type, variableDescriptor.Type.Name);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View file

@ -44,6 +44,26 @@ public class WorkflowDefinitionMapper
source.CustomProperties);
}
/// <summary>
/// Maps a <see cref="WorkflowDefinitionModel"/> to a <see cref="Workflow"/>.
/// </summary>
/// <param name="source">The source <see cref="WorkflowDefinitionModel"/>.</param>
/// <returns>The mapped <see cref="Workflow"/>.</returns>
public Workflow Map(WorkflowDefinitionModel source)
{
var root = source.Root!;
var variables = _variableDefinitionMapper.Map(source.Variables).ToList();
return new(
new WorkflowIdentity(source.DefinitionId, source.Version, source.Id),
new WorkflowPublication(source.IsLatest, source.IsPublished),
new WorkflowMetadata(source.Name, source.Description, source.CreatedAt),
source.Options,
root,
variables,
source.CustomProperties ?? new Dictionary<string, object>());
}
/// <summary>
/// Maps a <see cref="WorkflowDefinition"/> to a <see cref="Workflow"/>.
/// </summary>

View file

@ -0,0 +1,14 @@
using Elsa.Dsl.Models;
namespace Elsa.Workflows.Management.Options;
/// <summary>
/// Options for the DSL integration.
/// </summary>
public class DslIntegrationOptions
{
/// <summary>
/// A collection of function activity descriptors that are available to the DSL.
/// </summary>
public IDictionary<string, FunctionActivityDescriptor> FunctionActivityDescriptors { get; set; } = new Dictionary<string, FunctionActivityDescriptor>();
}

View file

@ -0,0 +1,46 @@
using Elsa.Common.Models;
using Elsa.JavaScript.Contracts;
using Elsa.JavaScript.TypeDefinitions.Abstractions;
using Elsa.JavaScript.TypeDefinitions.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Humanizer;
namespace Elsa.Workflows.Management.Scripting.JavaScript;
/// <summary>
/// Produces <see cref="FunctionDefinition"/>s for common functions.
/// </summary>
internal class InputFunctionsDefinitionProvider : FunctionDefinitionProvider
{
private readonly ITypeAliasRegistry _typeAliasRegistry;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
public InputFunctionsDefinitionProvider(ITypeAliasRegistry typeAliasRegistry, IWorkflowDefinitionService workflowDefinitionService)
{
_typeAliasRegistry = typeAliasRegistry;
_workflowDefinitionService = workflowDefinitionService;
}
protected override async ValueTask<IEnumerable<FunctionDefinition>> GetFunctionDefinitionsAsync(TypeDefinitionContext context)
{
var cancellationToken = context.CancellationToken;
var workflow = context.Workflow;
var workflowDefinition = await _workflowDefinitionService.FindAsync(workflow.Identity.DefinitionId, VersionOptions.SpecificVersion(workflow.Identity.Version), cancellationToken);
return workflowDefinition == null ? Array.Empty<FunctionDefinition>() : GetFunctionDefinitionsAsync(workflowDefinition);
}
private IEnumerable<FunctionDefinition> GetFunctionDefinitionsAsync(WorkflowDefinition workflowDefinition)
{
// Input argument getters.
foreach (var input in workflowDefinition.Inputs)
{
var pascalName = input.Name.Pascalize();
var variableType = input.Type;
var typeAlias = _typeAliasRegistry.TryGetAlias(variableType, out var alias) ? alias : "any";
// get{Input}.
yield return CreateFunctionDefinition(builder => builder.Name($"get{pascalName}").ReturnType(typeAlias));
}
}
}

View file

@ -5,7 +5,7 @@ namespace Elsa.Workflows.Runtime.Contracts;
/// <summary>
/// Represents a source of workflow definitions.
/// </summary>
public interface IWorkflowDefinitionProvider
public interface IWorkflowProvider
{
/// <summary>
/// Gets the name of the provider.
@ -17,5 +17,5 @@ public interface IWorkflowDefinitionProvider
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The workflow definitions.</returns>
ValueTask<IEnumerable<WorkflowDefinitionResult>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default);
ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default);
}

View file

@ -1,4 +1,5 @@
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Providers;
using Elsa.Workflows.Runtime.Services;
// ReSharper disable once CheckNamespace
@ -10,10 +11,10 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class DependencyInjectionExtensions
{
/// <summary>
/// Adds the <see cref="ClrWorkflowDefinitionProvider"/> to the service collection.
/// Adds the <see cref="ClrWorkflowProvider"/> to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <typeparam name="T">The type of the workflow definition provider.</typeparam>
/// <returns>The service collection.</returns>
public static IServiceCollection AddWorkflowDefinitionProvider<T>(this IServiceCollection services) where T : class, IWorkflowDefinitionProvider => services.AddSingleton<IWorkflowDefinitionProvider, T>();
public static IServiceCollection AddWorkflowDefinitionProvider<T>(this IServiceCollection services) where T : class, IWorkflowProvider => services.AddSingleton<IWorkflowProvider, T>();
}

View file

@ -17,6 +17,7 @@ using Elsa.Workflows.Runtime.Handlers;
using Elsa.Workflows.Runtime.HostedServices;
using Elsa.Workflows.Runtime.Notifications;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.Providers;
using Elsa.Workflows.Runtime.Services;
using Medallion.Threading;
using Medallion.Threading.FileSystem;
@ -124,7 +125,6 @@ public class WorkflowRuntimeFeature : FeatureBase
// Core.
.AddSingleton<ITriggerIndexer, TriggerIndexer>()
.AddSingleton<IWorkflowInstanceFactory, WorkflowInstanceFactory>()
.AddSingleton<IWorkflowDefinitionService, WorkflowDefinitionService>()
.AddSingleton<IWorkflowHostFactory, WorkflowHostFactory>()
.AddSingleton<IBackgroundActivityInvoker, DefaultBackgroundActivityInvoker>()
.AddSingleton(WorkflowRuntime)
@ -140,7 +140,7 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddSingleton<IEventPublisher, EventPublisher>()
// Lazy services.
.AddSingleton<Func<IEnumerable<IWorkflowDefinitionProvider>>>(sp => sp.GetServices<IWorkflowDefinitionProvider>)
.AddSingleton<Func<IEnumerable<IWorkflowProvider>>>(sp => sp.GetServices<IWorkflowProvider>)
.AddSingleton<Func<IEnumerable<IWorkflowMaterializer>>>(sp => sp.GetServices<IWorkflowMaterializer>)
// Memory stores.
@ -153,7 +153,7 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddSingleton(DistributedLockProvider)
// Workflow definition providers.
.AddWorkflowDefinitionProvider<ClrWorkflowDefinitionProvider>()
.AddWorkflowDefinitionProvider<ClrWorkflowProvider>()
// Workflow state exporter.
.AddSingleton(WorkflowStateExporter)

View file

@ -1,34 +1,45 @@
using Elsa.Common.Contracts;
using Elsa.Common.Models;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Models;
using Microsoft.Extensions.Hosting;
using Open.Linq.AsyncExtensions;
namespace Elsa.Workflows.Runtime.HostedServices;
/// <summary>
/// Synchronously updates the workflow definition store from <see cref="IWorkflowDefinitionProvider"/> implementations and creates triggers.
/// Updates the workflow store from <see cref="IWorkflowProvider"/> implementations and creates triggers.
/// </summary>
public class PopulateWorkflowDefinitionStore : IHostedService
{
private readonly Func<IEnumerable<IWorkflowDefinitionProvider>> _workflowDefinitionProviders;
private readonly Func<IEnumerable<IWorkflowProvider>> _workflowDefinitionProviders;
private readonly ITriggerIndexer _triggerIndexer;
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
private readonly IActivitySerializer _activitySerializer;
private readonly IPayloadSerializer _payloadSerializer;
private readonly ISystemClock _systemClock;
/// <summary>
/// Constructor.
/// </summary>
public PopulateWorkflowDefinitionStore(
Func<IEnumerable<IWorkflowDefinitionProvider>> workflowDefinitionProviders,
Func<IEnumerable<IWorkflowProvider>> workflowDefinitionProviders,
ITriggerIndexer triggerIndexer,
IWorkflowDefinitionStore workflowDefinitionStore)
IWorkflowDefinitionStore workflowDefinitionStore,
IActivitySerializer activitySerializer,
IPayloadSerializer payloadSerializer,
ISystemClock systemClock)
{
_workflowDefinitionProviders = workflowDefinitionProviders;
_triggerIndexer = triggerIndexer;
_workflowDefinitionStore = workflowDefinitionStore;
_activitySerializer = activitySerializer;
_payloadSerializer = payloadSerializer;
_systemClock = systemClock;
}
/// <inheritdoc />
@ -41,43 +52,53 @@ public class PopulateWorkflowDefinitionStore : IHostedService
foreach (var result in results)
{
await AddOrUpdateAsync(result.Definition, cancellationToken);
await IndexTriggersAsync(result.Workflow, cancellationToken);
await AddOrUpdateAsync(result, cancellationToken);
await IndexTriggersAsync(result, cancellationToken);
}
}
}
private async Task AddOrUpdateAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
private async Task AddOrUpdateAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default)
{
// Check if there's already a workflow definition by the definition ID and version.
var workflow = materializedWorkflow.Workflow;
// Check if there's already a workflow materializedWorkflow by the materializedWorkflow ID and version.
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definition.DefinitionId,
VersionOptions = VersionOptions.SpecificVersion(definition.Version)
DefinitionId = workflow.Identity.DefinitionId,
VersionOptions = VersionOptions.SpecificVersion(workflow.Version)
};
var existingDefinition = await _workflowDefinitionStore.FindAsync(filter, cancellationToken);
if (existingDefinition == null)
// Serialize materializer context.
var materializerContext = materializedWorkflow.MaterializerContext;
var materializerContextJson = materializerContext != null ? _payloadSerializer.Serialize(materializerContext) : default;
// Serialize the workflow root.
var workflowJson = _activitySerializer.Serialize(workflow.Root);
// Check if there's already a workflow definition stored with this workflow.
var existingDefinition = await _workflowDefinitionStore.FindAsync(filter, cancellationToken) ?? new WorkflowDefinition
{
existingDefinition = definition;
}
else
{
existingDefinition.Description = definition.Description;
existingDefinition.Name = definition.Name;
existingDefinition.CustomProperties = definition.CustomProperties;
existingDefinition.Variables = definition.Variables;
existingDefinition.BinaryData = definition.BinaryData;
existingDefinition.StringData = definition.StringData;
existingDefinition.MaterializerContext = definition.MaterializerContext;
existingDefinition.MaterializerName = definition.MaterializerName;
}
DefinitionId = workflow.Identity.DefinitionId,
Id = workflow.Identity.Id,
Version = workflow.Identity.Version
};
existingDefinition.Description = workflow.WorkflowMetadata.Description;
existingDefinition.Name = workflow.WorkflowMetadata.Name;
existingDefinition.IsLatest = workflow.Publication.IsLatest;
existingDefinition.IsPublished = workflow.Publication.IsPublished;
existingDefinition.CustomProperties = workflow.CustomProperties;
existingDefinition.Variables = workflow.Variables;
existingDefinition.StringData = workflowJson;
existingDefinition.CreatedAt = workflow.WorkflowMetadata.CreatedAt == default ? _systemClock.UtcNow : workflow.WorkflowMetadata.CreatedAt;
existingDefinition.MaterializerContext = materializerContextJson;
existingDefinition.MaterializerName = materializedWorkflow.MaterializerName;
await _workflowDefinitionStore.SaveAsync(existingDefinition, cancellationToken);
}
private async Task IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(workflow, cancellationToken);
private async Task IndexTriggersAsync(MaterializedWorkflow workflow, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(workflow.Workflow, cancellationToken);
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

View file

@ -0,0 +1,12 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Entities;
namespace Elsa.Workflows.Runtime.Models;
/// <summary>
/// Represents a workflow definition and its workflow.
/// </summary>
/// <param name="Workflow">The workflow materialized from its workflow definition.</param>
/// <param name="MaterializerName">The name of the materializer that materialized the workflow.</param>
/// <param name="MaterializerContext">The context of the materializer that materialized the workflow.</param>
public record MaterializedWorkflow(Workflow Workflow, string MaterializerName, object? MaterializerContext = default);

View file

@ -1,11 +0,0 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Entities;
namespace Elsa.Workflows.Runtime.Models;
/// <summary>
/// Represents a workflow definition and its workflow.
/// </summary>
/// <param name="Definition">The workflow definition.</param>
/// <param name="Workflow">The workflow materialized from its workflow definition.</param>
public record WorkflowDefinitionResult(WorkflowDefinition Definition, Workflow Workflow);

View file

@ -0,0 +1,58 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Features;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Options;
using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Runtime.Providers;
/// <summary>
/// Provides workflows to the system that are registered with <see cref="WorkflowRuntimeFeature"/>
/// </summary>
public class ClrWorkflowProvider : IWorkflowProvider
{
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
private readonly IServiceProvider _serviceProvider;
private readonly RuntimeOptions _options;
/// <summary>
/// Constructor.
/// </summary>
public ClrWorkflowProvider(
IOptions<RuntimeOptions> options,
IWorkflowBuilderFactory workflowBuilderFactory,
IServiceProvider serviceProvider
)
{
_workflowBuilderFactory = workflowBuilderFactory;
_serviceProvider = serviceProvider;
_options = options.Value;
}
/// <inheritdoc />
public string Name => "CLR";
/// <inheritdoc />
public async ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default)
{
var workflowDefinitionTasks = _options.Workflows.Values.Select(async x => await BuildWorkflowDefinition(x, cancellationToken)).ToList();
var workflowDefinitions = await Task.WhenAll(workflowDefinitionTasks);
return workflowDefinitions;
}
private async Task<MaterializedWorkflow> BuildWorkflowDefinition(Func<IServiceProvider, ValueTask<IWorkflow>> workflowFactory, CancellationToken cancellationToken)
{
var builder = _workflowBuilderFactory.CreateBuilder();
var workflowBuilder = await workflowFactory(_serviceProvider);
var workflowBuilderType = workflowBuilder.GetType();
builder.DefinitionId = workflowBuilderType.Name;
await workflowBuilder.BuildAsync(builder, cancellationToken);
var workflow = await builder.BuildWorkflowAsync(cancellationToken);
var materializerContext = new ClrWorkflowMaterializerContext(workflowBuilder.GetType());
return new MaterializedWorkflow(workflow, ClrWorkflowMaterializer.MaterializerName, materializerContext);
}
}

View file

@ -1,90 +0,0 @@
using Elsa.Common.Contracts;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Features;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Options;
using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Runtime.Services;
/// <summary>
/// Provides workflows to the system that are registered with <see cref="WorkflowRuntimeFeature"/>
/// </summary>
public class ClrWorkflowDefinitionProvider : IWorkflowDefinitionProvider
{
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
private readonly IActivitySerializer _activitySerializer;
private readonly IPayloadSerializer _payloadSerializer;
private readonly ISystemClock _systemClock;
private readonly IServiceProvider _serviceProvider;
private readonly RuntimeOptions _options;
/// <summary>
/// Constructor.
/// </summary>
public ClrWorkflowDefinitionProvider(
IOptions<RuntimeOptions> options,
IWorkflowBuilderFactory workflowBuilderFactory,
IActivitySerializer activitySerializer,
IPayloadSerializer payloadSerializer,
ISystemClock systemClock,
IServiceProvider serviceProvider
)
{
_workflowBuilderFactory = workflowBuilderFactory;
_activitySerializer = activitySerializer;
_payloadSerializer = payloadSerializer;
_systemClock = systemClock;
_serviceProvider = serviceProvider;
_options = options.Value;
}
/// <inheritdoc />
public string Name => "CLR";
/// <inheritdoc />
public async ValueTask<IEnumerable<WorkflowDefinitionResult>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default)
{
var workflowDefinitionTasks = _options.Workflows.Values.Select(async x => await BuildWorkflowDefinition(x, cancellationToken)).ToList();
var workflowDefinitions = await Task.WhenAll(workflowDefinitionTasks);
return workflowDefinitions;
}
private async Task<WorkflowDefinitionResult> BuildWorkflowDefinition(Func<IServiceProvider, ValueTask<IWorkflow>> workflowFactory, CancellationToken cancellationToken)
{
var builder = _workflowBuilderFactory.CreateBuilder();
var workflowBuilder = await workflowFactory(_serviceProvider);
var workflowBuilderType = workflowBuilder.GetType();
builder.DefinitionId = workflowBuilderType.Name;
await workflowBuilder.BuildAsync(builder, cancellationToken);
var workflow = await builder.BuildWorkflowAsync(cancellationToken);
var workflowJson = _activitySerializer.Serialize(workflow.Root);
var materializerContext = new ClrWorkflowMaterializerContext(workflowBuilder.GetType());
var materializerContextJson = _payloadSerializer.Serialize(materializerContext);
var name = string.IsNullOrWhiteSpace(workflow.WorkflowMetadata.Name) ? workflowBuilderType.Name : workflow.WorkflowMetadata.Name.Trim();
var definition = new WorkflowDefinition
{
Id = workflow.Identity.Id,
DefinitionId = workflow.Identity.DefinitionId,
Version = workflow.Identity.Version,
Name = name,
Description = workflow.WorkflowMetadata.Description,
CustomProperties = workflow.Metadata,
Variables = workflow.Variables,
IsLatest = workflow.Publication.IsLatest,
IsPublished = workflow.Publication.IsPublished,
CreatedAt = workflow.WorkflowMetadata.CreatedAt == default ? _systemClock.UtcNow : workflow.WorkflowMetadata.CreatedAt,
MaterializerName = ClrWorkflowMaterializer.MaterializerName,
MaterializerContext = materializerContextJson,
StringData = workflowJson
};
return new WorkflowDefinitionResult(definition, workflow);
}
}

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.WorkflowProviders.FluentStorage\Elsa.WorkflowProviders.FluentStorage.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Workflows\hello-world.elsa">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\hello-world-functional.elsa">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,62 @@
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Http;
using Elsa.Workflows.Core.Activities;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
// Add Elsa services.
services.AddElsa(elsa => elsa
// Add the Fluent Storage workflow definition provider.
.UseFluentStorageProvider()
// Enable the Elsa DSL.
.UseWorkflowManagement(management =>
{
management.UseEntityFrameworkCore();
management.UseDslIntegration(dsl =>
{
dsl.MapActivityFunction("println", nameof(WriteLine), new[] { nameof(WriteLine.Text) });
dsl.MapActivityFunction("http_listen", nameof(HttpEndpoint), new[] { nameof(HttpEndpoint.Path), nameof(HttpEndpoint.SupportedMethods) }, activity => activity.SetCanStartWorkflow(true));
dsl.MapActivityFunction("http_write", nameof(WriteHttpResponse), new[] { nameof(WriteHttpResponse.StatusCode), nameof(WriteHttpResponse.Content) });
});
})
.UseWorkflowRuntime(runtime =>
{
runtime.UseEntityFrameworkCore();
runtime.UseExecutionLogRecords();
runtime.UseAsyncWorkflowStateExporter();
})
// Expose API endpoints.
.UseWorkflowsApi()
// Configure identity so that we can create a default admin user.
.UseIdentity(identity =>
{
identity.UseAdminUserProvider();
identity.TokenOptions = options =>
{
options.SigningKey = "secret-token-signing-key";
options.AccessTokenLifetime = TimeSpan.FromDays(1);
};
})
// Use default authentication (JWT).
.UseDefaultAuthentication(auth => auth.UseAdminApiKey())
// Use HTTP activities.
.UseHttp()
);
// Configure middleware pipeline.
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.UseWorkflows();
app.Run();

View file

@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:11864",
"sslPort": 44339
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5006",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7185;http://localhost:5006",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,8 @@
// Listen for HTTP requests on the path /hello-world-dsl.
http_listen("hello-world-functional-dsl", ["GET"]);
// Print a message to the console.
println("Hello functional DSL World!");
// Write an HTTP response.
http_write("200", "Hello functional DSL World!");

View file

@ -0,0 +1,20 @@
// Listen for HTTP requests on the path /hello-world-dsl.
HttpEndpoint
{
Path: "hello-world-dsl",
SupportedMethods: ["GET"],
CanStartWorkflow: "true"
};
// Write a message to the console.
WriteLine
{
Text: "Hello DSL World"
};
// Write an HTTP response.
WriteHttpResponse
{
StatusCode: "200",
Content: "Hello DSL World!"
};

View file

@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Elsa.Mediator": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"AllowedHosts": "*"
}