diff --git a/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj index 9c538db64..4a446863f 100644 --- a/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj +++ b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Elsa.Samples.AspNet.WorkflowServer.csproj @@ -14,4 +14,8 @@ + + + + \ No newline at end of file diff --git a/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs index a093abdf5..ead62e546 100644 --- a/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs +++ b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs @@ -40,6 +40,9 @@ builder.Services.AddElsa(elsa => // Register custom activities. elsa.AddActivitiesFrom(); + + // Register custom workflows. + elsa.AddWorkflowsFrom(); }); // Configure CORS to allow designer app hosted on a different origin to invoke the APIs. diff --git a/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Workflows/MysteriousPondWorkflow.cs b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Workflows/MysteriousPondWorkflow.cs new file mode 100644 index 000000000..fd7cd1c78 --- /dev/null +++ b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Workflows/MysteriousPondWorkflow.cs @@ -0,0 +1,56 @@ +using System.Net.Mime; +using Elsa.Expressions.Models; +using Elsa.Http; +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; + +namespace Elsa.Samples.AspNet.WorkflowServer.Workflows; + +/// +/// Represents a mysterious pond workflow. +/// +public class MysteriousPondWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + var investment = builder.WithVariable(); + + builder.Root = new Sequence + { + Activities = + { + new HttpEndpoint + { + Path = new("mysterious_pond"), + SupportedMethods = new([HttpMethod.Post.Method]), + ParsedContent = new(investment), + CanStartWorkflow = true + }, + new WriteLine(context => $"Received {GetRupees(context)} rupees"), + new Switch + { + Cases = + { + new SwitchCase("Great Luck", context => GetRupees(context) >= 1000, WriteHttpResponse("For today, you will have great luck.")), + new SwitchCase("Good Luck", context => GetRupees(context) >= 500, WriteHttpResponse("For today, you will have good luck.")), + new SwitchCase("A Little Luck", context => GetRupees(context) >= 250, WriteHttpResponse("For today, you will have a little luck.")), + new SwitchCase("Bad Luck", context => GetRupees(context) < 250, WriteHttpResponse("For today, you will have bad luck.")), + + } + } + } + }; + return; + + int GetRupees(ExpressionExecutionContext context) => investment.Get(context)!.Rupees; + } + + private static WriteHttpResponse WriteHttpResponse(string message) => new() + { + Content = new(message), + ContentType = new(MediaTypeNames.Text.Plain), + }; + + private record Investment(int Rupees); +}; \ No newline at end of file diff --git a/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/mysterious_pond.http b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/mysterious_pond.http new file mode 100644 index 000000000..1a5c9f72c --- /dev/null +++ b/samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/mysterious_pond.http @@ -0,0 +1,31 @@ +### POST 1000 rupees to the mysterious pond. +POST https://localhost:5001/workflows/mysterious_pond +Content-Type: application/json + +{ + "rupees": 1000 +} + +### POST 500 rupees to the mysterious pond. +POST https://localhost:5001/workflows/mysterious_pond +Content-Type: application/json + +{ + "rupees": 500 +} + +### POST 250 rupees to the mysterious pond. +POST https://localhost:5001/workflows/mysterious_pond +Content-Type: application/json + +{ + "rupees": 250 +} + +### POST 100 rupees to the mysterious pond. +POST https://localhost:5001/workflows/mysterious_pond +Content-Type: application/json + +{ + "rupees": 100 +} diff --git a/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj index 1b9a8a9bc..189840cc2 100644 --- a/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -59,6 +59,7 @@ + diff --git a/src/modules/Elsa.Expressions/Extensions/JsonElementExtensions.cs b/src/modules/Elsa.Expressions/Extensions/JsonElementExtensions.cs new file mode 100644 index 000000000..e1975c2d8 --- /dev/null +++ b/src/modules/Elsa.Expressions/Extensions/JsonElementExtensions.cs @@ -0,0 +1,31 @@ +using System.Text.Json; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Parses a into a .NET object. +/// +public static class JsonElementExtensions +{ + /// + /// Parses a into a .NET object. + /// + /// The JSON element to parse. + /// The parsed object. + public static object? GetValue(this JsonElement jsonElement) + { + return jsonElement.ValueKind switch + { + JsonValueKind.String => jsonElement.GetString(), + JsonValueKind.Number => jsonElement.GetDecimal(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Undefined => null, + JsonValueKind.Null => null, + JsonValueKind.Object => jsonElement.GetRawText(), + JsonValueKind.Array => jsonElement.GetRawText(), + _ => jsonElement.GetRawText() + }; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs b/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs index b63ac9d7a..084c3612e 100644 --- a/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs +++ b/src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Elsa.Expressions.Contracts; +using Elsa.Extensions; namespace Elsa.Expressions.Models; @@ -16,9 +17,13 @@ public class ExpressionDescriptor // Default deserialization function. Deserialize = context => { - return context.JsonElement.ValueKind == JsonValueKind.Object - ? context.JsonElement.Deserialize((JsonSerializerOptions?)context.Options)! - : new Expression(context.ExpressionType, null!); + var expression = new Expression(context.ExpressionType, null); + + if (context.JsonElement.ValueKind == JsonValueKind.Object) + if (context.JsonElement.TryGetProperty("value", out var expressionValueElement)) + expression.Value = expressionValueElement.GetValue(); + + return expression; }; } diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs index 4859f023f..62b6a2223 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs @@ -217,6 +217,7 @@ public class ActivityJsonConverter : JsonConverter var newOptions = new JsonSerializerOptions(options); newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); newOptions.Converters.Add(new OutputJsonConverterFactory(_serviceProvider)); + newOptions.Converters.Add(new ExpressionJsonConverterFactory(_expressionDescriptorRegistry)); return _options = newOptions; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExpressionJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExpressionJsonConverter.cs new file mode 100644 index 000000000..70fe6ddf5 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExpressionJsonConverter.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Models; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows.Serialization.Converters; + +/// +/// Serializes objects. +/// +public class ExpressionJsonConverter : JsonConverter +{ + private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry; + + /// + public ExpressionJsonConverter(IExpressionDescriptorRegistry expressionDescriptorRegistry) + { + _expressionDescriptorRegistry = expressionDescriptorRegistry; + } + + /// + public override bool CanConvert(Type typeToConvert) => typeof(Input).IsAssignableFrom(typeToConvert); + + /// + public override Expression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + return default!; + + var expressionElement = doc.RootElement; + var expressionTypeNameElement = expressionElement.TryGetProperty("type", out var expressionTypeNameElementValue) ? expressionTypeNameElementValue : default; + var expressionTypeName = expressionTypeNameElement.ValueKind != JsonValueKind.Undefined ? expressionTypeNameElement.GetString() ?? "Literal" : default; + var expressionDescriptor = expressionTypeName != null ? _expressionDescriptorRegistry.Find(expressionTypeName) : default; + var memoryBlockReference = expressionDescriptor?.MemoryBlockReferenceFactory?.Invoke(); + + if (memoryBlockReference == null) + return default!; + + var memoryBlockType = memoryBlockReference.GetType(); + var context = new ExpressionSerializationContext(expressionTypeName!, expressionElement, options, memoryBlockType); + var expression = expressionDescriptor!.Deserialize(context); + + return expression; + } + + /// + public override void Write(Utf8JsonWriter writer, Expression value, JsonSerializerOptions options) + { + var expression = value; + + if(expression == null) + { + writer.WriteNullValue(); + return; + } + + var expressionType = expression.Type; + var expressionDescriptor = expressionType != null ? _expressionDescriptorRegistry.Find(expressionType) : null; + + if (expressionDescriptor == null) + throw new JsonException($"Could not find an expression descriptor for expression type '{expressionType}'."); + + var expressionValue = expressionDescriptor.IsSerializable ? expression.Value : null; + + var model = new + { + Type = expressionType, + Value = expressionValue + }; + + JsonSerializer.Serialize(writer, model, options); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExpressionJsonConverterFactory.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExpressionJsonConverterFactory.cs new file mode 100644 index 000000000..0af35b397 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExpressionJsonConverterFactory.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Models; + +namespace Elsa.Workflows.Serialization.Converters; + +/// +/// A JSON converter factory that creates instances. +/// +public class ExpressionJsonConverterFactory : JsonConverterFactory +{ + private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry; + + /// + public ExpressionJsonConverterFactory(IExpressionDescriptorRegistry expressionDescriptorRegistry) + { + _expressionDescriptorRegistry = expressionDescriptorRegistry; + } + + /// + public override bool CanConvert(Type typeToConvert) => typeof(Expression).IsAssignableFrom(typeToConvert); + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new ExpressionJsonConverter(_expressionDescriptorRegistry); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs index f0f58f70f..f1c6c80d9 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/InputJsonConverter.cs @@ -49,7 +49,6 @@ public class InputJsonConverter : JsonConverter> var memoryBlockType = memoryBlockReference.GetType(); var context = new ExpressionSerializationContext(expressionTypeName!, expressionElement, options, memoryBlockType); var expression = expressionDescriptor!.Deserialize(context); - return (Input)Activator.CreateInstance(typeof(Input), expression, memoryBlockReference)!; } @@ -61,14 +60,14 @@ public class InputJsonConverter : JsonConverter> public override void Write(Utf8JsonWriter writer, Input value, JsonSerializerOptions options) { var expression = value.Expression; - - if(expression == null) + + if (expression == null) { writer.WriteNullValue(); return; } - - var expressionType = expression?.Type; + + var expressionType = expression.Type; var expressionDescriptor = expressionType != null ? _expressionDescriptorRegistry.Find(expressionType) : default; if (expressionDescriptor == null) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs index c206fefd6..704b48bca 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs @@ -39,6 +39,8 @@ public class JsonActivitySerializer(IServiceProvider serviceProvider) : Configur { options.Converters.Add(CreateInstance()); options.Converters.Add(CreateInstance()); + options.Converters.Add(CreateInstance()); + options.Converters.Add(CreateInstance()); } private JsonSerializerOptions GetOptionsInternal() diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs index 12a79f64b..a9437dba0 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs @@ -2,9 +2,11 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Elsa.Common.Serialization; +using Elsa.Expressions.Contracts; using Elsa.Expressions.Services; using Elsa.Workflows.Contracts; using Elsa.Workflows.Serialization.Converters; +using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Serialization.Serializers; @@ -51,8 +53,11 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer /// protected override void AddConverters(JsonSerializerOptions options) { + var expressionDescriptorRegistry = ServiceProvider.GetRequiredService(); + options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); options.Converters.Add(new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault())); options.Converters.Add(new SafeValueConverterFactory()); + options.Converters.Add(new ExpressionJsonConverterFactory(expressionDescriptorRegistry)); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs index c0d738ba5..e85742a7c 100644 --- a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs +++ b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionDescriptorProvider.cs @@ -32,20 +32,7 @@ public class DefaultExpressionDescriptorProvider : IExpressionDescriptorProvider deserialize: context => { var elementValue = context.JsonElement.TryGetProperty("value", out var v) ? v : default; - - var value = (object?)(elementValue.ValueKind switch - { - JsonValueKind.String => elementValue.GetString(), - JsonValueKind.Number => elementValue.GetDecimal(), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Undefined => null, - JsonValueKind.Null => null, - JsonValueKind.Object => elementValue.GetRawText(), - JsonValueKind.Array => elementValue.GetRawText(), - _ => v.GetRawText() - }); - + var value = elementValue.GetValue(); return new Expression("Literal", value); }); } @@ -93,7 +80,7 @@ public class DefaultExpressionDescriptorProvider : IExpressionDescriptorProvider MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference()) }; - if (deserialize != null) + if (deserialize != null) descriptor.Deserialize = deserialize; if (monacoLanguage != null) diff --git a/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowProvider.cs b/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowProvider.cs index c20d8cbdd..7125fabcf 100644 --- a/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowProvider.cs +++ b/src/modules/Elsa.Workflows.Runtime/Providers/ClrWorkflowProvider.cs @@ -51,6 +51,7 @@ public class ClrWorkflowProvider : IWorkflowProvider var workflowBuilderType = workflowBuilder.GetType(); builder.DefinitionId = workflowBuilderType.Name; + builder.Id = $"{workflowBuilderType.Name}:1.0"; await workflowBuilder.BuildAsync(builder, cancellationToken); var workflow = await builder.BuildWorkflowAsync(cancellationToken);