Add JSON Serialization for Elsa expression (#5490)
* Add JSON Serialization for Elsa expression Extended the expression serialization. Added new classes ExpressionJsonConverter and ExpressionJsonConverterFactory implementing serialization of expression objects. Also, made respective changes in different serializers and related files for seamless integration. * Fix XML comments * Set initial builder Id in ClrWorkflowProvider This commit involves a modification in ClrWorkflowProvider.cs where an Id was set for the builder. The Id was set with the format `workflowBuilderType.Name`:1.0, providing a deterministic identifier for each builder instance. * Add MysteriousPondWorkflow and associated HTTP endpoints A new workflow, MysteriousPondWorkflow, has been introduced along with HTTP endpoints to interact with it. The workflow simulates throwing an arbitrary amount of rupees into a mysterious pond and getting a luck prediction for the day based on the amount. An integration of this workflow is registered in the main Program.cs file, and the necessary directories to handle this workflow have been added to relevant project files.
This commit is contained in:
parent
038d29b792
commit
100ece8278
|
|
@ -14,4 +14,8 @@
|
|||
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="App_Data\locks\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -40,6 +40,9 @@ builder.Services.AddElsa(elsa =>
|
|||
|
||||
// Register custom activities.
|
||||
elsa.AddActivitiesFrom<Program>();
|
||||
|
||||
// Register custom workflows.
|
||||
elsa.AddWorkflowsFrom<Program>();
|
||||
});
|
||||
|
||||
// Configure CORS to allow designer app hosted on a different origin to invoke the APIs.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mysterious pond workflow.
|
||||
/// </summary>
|
||||
public class MysteriousPondWorkflow : WorkflowBase
|
||||
{
|
||||
protected override void Build(IWorkflowBuilder builder)
|
||||
{
|
||||
var investment = builder.WithVariable<Investment>();
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Folder Include="App_Data\"/>
|
||||
<Folder Include="Workflows\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
using System.Text.Json;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="JsonElement"/> into a .NET object.
|
||||
/// </summary>
|
||||
public static class JsonElementExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a <see cref="JsonElement"/> into a .NET object.
|
||||
/// </summary>
|
||||
/// <param name="jsonElement">The JSON element to parse.</param>
|
||||
/// <returns>The parsed object.</returns>
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Expression>((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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ public class ActivityJsonConverter : JsonConverter<IActivity>
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes <see cref="Expression"/> objects.
|
||||
/// </summary>
|
||||
public class ExpressionJsonConverter : JsonConverter<Expression>
|
||||
{
|
||||
private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExpressionJsonConverter(IExpressionDescriptorRegistry expressionDescriptorRegistry)
|
||||
{
|
||||
_expressionDescriptorRegistry = expressionDescriptorRegistry;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert) => typeof(Input).IsAssignableFrom(typeToConvert);
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A JSON converter factory that creates <see cref="ExpressionJsonConverter"/> instances.
|
||||
/// </summary>
|
||||
public class ExpressionJsonConverterFactory : JsonConverterFactory
|
||||
{
|
||||
private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExpressionJsonConverterFactory(IExpressionDescriptorRegistry expressionDescriptorRegistry)
|
||||
{
|
||||
_expressionDescriptorRegistry = expressionDescriptorRegistry;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert) => typeof(Expression).IsAssignableFrom(typeToConvert);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return new ExpressionJsonConverter(_expressionDescriptorRegistry);
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,6 @@ public class InputJsonConverter<T> : JsonConverter<Input<T>>
|
|||
var memoryBlockType = memoryBlockReference.GetType();
|
||||
var context = new ExpressionSerializationContext(expressionTypeName!, expressionElement, options, memoryBlockType);
|
||||
var expression = expressionDescriptor!.Deserialize(context);
|
||||
|
||||
return (Input<T>)Activator.CreateInstance(typeof(Input<T>), expression, memoryBlockReference)!;
|
||||
}
|
||||
|
||||
|
|
@ -61,14 +60,14 @@ public class InputJsonConverter<T> : JsonConverter<Input<T>>
|
|||
public override void Write(Utf8JsonWriter writer, Input<T> 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)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ public class JsonActivitySerializer(IServiceProvider serviceProvider) : Configur
|
|||
{
|
||||
options.Converters.Add(CreateInstance<TypeJsonConverter>());
|
||||
options.Converters.Add(CreateInstance<InputJsonConverterFactory>());
|
||||
options.Converters.Add(CreateInstance<OutputJsonConverterFactory>());
|
||||
options.Converters.Add(CreateInstance<ExpressionJsonConverterFactory>());
|
||||
}
|
||||
|
||||
private JsonSerializerOptions GetOptionsInternal()
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// <inheritdoc />
|
||||
protected override void AddConverters(JsonSerializerOptions options)
|
||||
{
|
||||
var expressionDescriptorRegistry = ServiceProvider.GetRequiredService<IExpressionDescriptorRegistry>();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue