Skip to content

Commit

Permalink
Handle user-usage of $id key in JSON objects
Browse files Browse the repository at this point in the history
  • Loading branch information
sfmskywalker committed Nov 23, 2023
1 parent c10e1b4 commit 14d7ec1
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS

if (isNewtonsoftObject)
{
var parsedModel = JsonElement.ParseValue(ref reader)!;
var parsedModel = JsonElement.ParseValue(ref reader);
var newtonsoftJson = parsedModel.GetProperty(IslandPropertyName).GetString();
return !string.IsNullOrWhiteSpace(newtonsoftJson) ? JObject.Parse(newtonsoftJson) : new JObject();
}
Expand All @@ -68,7 +68,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS

if (isNewtonsoftArray)
{
var parsedModel = JsonElement.ParseValue(ref reader)!;
var parsedModel = JsonElement.ParseValue(ref reader);
var newtonsoftJson = parsedModel.GetProperty(IslandPropertyName).GetString();
return !string.IsNullOrWhiteSpace(newtonsoftJson) ? JArray.Parse(newtonsoftJson) : new JArray();
}
Expand All @@ -78,7 +78,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS

if (isJsonObject)
{
var parsedModel = JsonElement.ParseValue(ref reader)!;
var parsedModel = JsonElement.ParseValue(ref reader);
var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonObject.Parse(systemTextJson) : new JsonObject();
}
Expand All @@ -87,7 +87,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS

if (isJsonArray)
{
var parsedModel = JsonElement.ParseValue(ref reader)!;
var parsedModel = JsonElement.ParseValue(ref reader);
var systemTextJson = parsedModel.GetProperty(IslandPropertyName).GetString();
return !string.IsNullOrWhiteSpace(systemTextJson) ? JsonArray.Parse(systemTextJson) : new JsonArray();
}
Expand Down Expand Up @@ -327,7 +327,7 @@ private object ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions optio
}
case JsonTokenType.StartObject:
var dict = new ExpandoObject() as IDictionary<string, object>;
var referenceResolver = (options.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver();
var referenceResolver = (CustomPreserveReferenceResolver)(options.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver()!;
while (reader.Read())
{
switch (reader.TokenType)
Expand All @@ -344,13 +344,15 @@ private object ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions optio
if (key == RefPropertyName)
{
var referenceId = reader.GetString();
var reference = referenceResolver!.ResolveReference(referenceId!);
var reference = referenceResolver.ResolveReference(referenceId!);
dict.Add(key, reference);
}
else if (key == IdPropertyName)
{
var referenceId = reader.GetString()!;
referenceResolver!.AddReference(referenceId, dict);

// Attempt to add the reference; if not found, we can ignore it and assume that the user is using the $id property for something else, such as in JSON $schema.
referenceResolver.TryAddReference(referenceId, dict);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ public override void AddReference(string referenceId, object value)
if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
throw new JsonException();
}

/// <summary>
/// Tries to add a reference.
/// </summary>
public bool TryAddReference(string referenceId, object value)
{
return _referenceIdToObjectMap.TryAdd(referenceId, value);
}

/// <inheritdoc />
public override string GetReference(object value, out bool alreadyExists)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
<None Update="Scenarios\ParentChildOutputMapping\Workflows\consumer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\JsonObjectSerialization\Workflows\instance-serialization.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>



Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;
using Elsa.Expressions.Contracts;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Core.Serialization.ReferenceHandlers;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;

namespace Elsa.IntegrationTests.Scenarios.JsonObjectSerialization;

/// <summary>
/// Tests for serializing and deserializing JSON objects containing reserved keywords such as $id.
/// </summary>
public class Tests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
private readonly IWorkflowStateSerializer _workflowStateSerializer;

public Tests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
_workflowStateSerializer = _services.GetRequiredService<IWorkflowStateSerializer>();
}

[Fact(DisplayName = "User-objects containing $id don't break deserialization into ExpandoObject.")]
public async Task Test1()
{
// Populate registries.
await _services.PopulateRegistriesAsync();

// Import workflow.
var workflowFileName = "Scenarios/JsonObjectSerialization/Workflows/instance-serialization.json";
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync(workflowFileName);

// Execute.
var workflowState = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);

// Serialize workflow state.
var serializedWorkflowState = await _workflowStateSerializer.SerializeAsync(workflowState);

// Attempt to deserialize workflow state.
await _workflowStateSerializer.DeserializeAsync<WorkflowState>(serializedWorkflowState);

// If we reach this point, the test has passed. Otherwise, an exception would have been thrown.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{
"id": "b78e443339b5bf1e",
"definitionId": "861e51cdbe07931",
"name": "Instance Serialization",
"description": "This workflow stores a user-JSON object containing $id as a field, which should not affect the deserialization process form JSON back into ExpandoObject.",
"createdAt": "2023-11-23T15:18:12.805077+00:00",
"version": 13,
"toolVersion": "3.0.0.0",
"variables": [
{
"id": "d4f37df0c6781767",
"name": "Variable1",
"typeName": "Object",
"isArray": false,
"storageDriverTypeName": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core"
},
{
"id": "4e256dab27d33eac",
"name": "Variable2",
"typeName": "Object",
"isArray": false,
"storageDriverTypeName": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core"
}
],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isLatest": true,
"isPublished": false,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "35cdc2a3529d9b55",
"nodeId": "Workflow1:35cdc2a3529d9b55",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:47",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"variable": {
"id": "d4f37df0c6781767",
"name": "Variable1",
"typeName": "Object",
"storageDriverTypeName": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core"
},
"value": {
"typeName": "Object",
"expression": {
"type": "JavaScript",
"value": "return {\n \u0022inputSchema\u0022: {\n \u0022$schema\u0022: \u0022http:https://json-schema.org/draft-07/schema#\u0022,\n \u0022title\u0022: \u0022Inform front-office afd 2.0 input schema\u0022,\n \u0022description\u0022: \u0022Schema of the AFD 2.0 message that is sent to the front-office\u0022,\n \u0022$id\u0022: \u0022#/definitions/inform-front-office-input-afd\u0022,\n \u0022type\u0022: \u0022object\u0022\n }\n}"
},
"memoryReference": {
"id": "d4f32ee0ad659ba5:input-0"
}
},
"id": "d4f32ee0ad659ba5",
"nodeId": "Workflow1:35cdc2a3529d9b55:d4f32ee0ad659ba5",
"name": "SetVariable1",
"type": "Elsa.SetVariable",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -456.5988464355469,
"y": -104.93463134765625
},
"size": {
"width": 153.828125,
"height": 50
}
}
}
},
{
"variable": {
"id": "4e256dab27d33eac",
"name": "Variable2",
"typeName": "Object",
"storageDriverTypeName": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core"
},
"value": {
"typeName": "Object",
"expression": {
"type": "JavaScript",
"value": "return getVariable1()"
},
"memoryReference": {
"id": "31c60a181a1a66d8:input-0"
}
},
"id": "31c60a181a1a66d8",
"nodeId": "Workflow1:35cdc2a3529d9b55:31c60a181a1a66d8",
"name": "SetVariable2",
"type": "Elsa.SetVariable",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -210.69132232666016,
"y": -104.93463134765625
},
"size": {
"width": 153.828125,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "d4f32ee0ad659ba5",
"port": "Done"
},
"target": {
"activity": "31c60a181a1a66d8",
"port": "In"
}
}
]
}
}

0 comments on commit 14d7ec1

Please sign in to comment.