Skip to content

Commit

Permalink
Add JSON Serialization for Elsa expression (#5490)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
sfmskywalker committed Jun 3, 2024
1 parent 038d29b commit 100ece8
Show file tree
Hide file tree
Showing 15 changed files with 252 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="App_Data\locks\" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions samples/aspnet/Elsa.Samples.AspNet.WorkflowServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
// 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

<ItemGroup>
<Folder Include="App_Data\"/>
<Folder Include="Workflows\" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions src/modules/Elsa.Expressions/Extensions/JsonElementExtensions.cs
Original file line number Diff line number Diff line change
@@ -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()
};
}
}
11 changes: 8 additions & 3 deletions src/modules/Elsa.Expressions/Models/ExpressionDescriptor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using Elsa.Expressions.Contracts;
using Elsa.Extensions;

namespace Elsa.Expressions.Models;

Expand All @@ -16,9 +17,13 @@ public 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;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ private JsonSerializerOptions GetClonedOptions(JsonSerializerOptions options)
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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public override Input<T> Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
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)!;
}

Expand All @@ -61,14 +60,14 @@ public override Input<T> Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ protected override void AddConverters(JsonSerializerOptions options)
{
options.Converters.Add(CreateInstance<TypeJsonConverter>());
options.Converters.Add(CreateInstance<InputJsonConverterFactory>());
options.Converters.Add(CreateInstance<OutputJsonConverterFactory>());
options.Converters.Add(CreateInstance<ExpressionJsonConverterFactory>());
}

private JsonSerializerOptions GetOptionsInternal()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
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;

Expand Down Expand Up @@ -51,8 +53,11 @@ public ValueTask<T> DeserializeAsync<T>(JsonElement element, CancellationToken c
/// <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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,7 @@ private ExpressionDescriptor CreateLiteralDescriptor()
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);
});
}
Expand Down Expand Up @@ -93,7 +80,7 @@ private ExpressionDescriptor CreateVariableDescriptor()
MemoryBlockReferenceFactory = memoryBlockReferenceFactory ?? (() => new MemoryBlockReference())
};

if (deserialize != null)
if (deserialize != null)
descriptor.Deserialize = deserialize;

if (monacoLanguage != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private async Task<MaterializedWorkflow> BuildWorkflowAsync(Func<IServiceProvide
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);
Expand Down

0 comments on commit 100ece8

Please sign in to comment.