Dispose parsed activity JsonDocuments (#7713)

* Dispose parsed activity JsonDocuments

* Add ActivityJsonConverter disposal regression test

* Fix ActivityJsonConverter disposal regression test file

* address greptile test coverage feedback
This commit is contained in:
Sipke Schoorstra 2026-06-11 20:33:10 +02:00 committed by GitHub
parent ded765b379
commit 9c24f5efe5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 48 deletions

View file

@ -27,57 +27,70 @@ public class ActivityJsonConverter(
if (!JsonDocument.TryParseValue(ref reader, out var doc))
throw new JsonException("Failed to parse JsonDocument");
var activityRoot = doc.RootElement;
var activityTypeName = GetActivityDetails(activityRoot, out var activityTypeVersion, out var activityDescriptor);
var notFoundActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<NotFoundActivity>();
// If the activity type is a NotFoundActivity, try to extract the original activity type name and version.
if (activityTypeName.Equals(notFoundActivityTypeName) && activityRoot.TryGetProperty("originalActivityJson", out var originalActivityJson))
using (doc)
{
activityRoot = JsonDocument.Parse(originalActivityJson.GetString()!).RootElement;
activityTypeName = GetActivityDetails(activityRoot, out activityTypeVersion, out activityDescriptor);
}
JsonDocument? originalActivityDoc = null;
var clonedOptions = GetClonedOptions(options);
// If the activity type is not found, create a NotFoundActivity instead.
if (activityDescriptor == null)
{
var notFoundActivityDescriptor = activityRegistry.Find<NotFoundActivity>()!;
var notFoundActivityResult = JsonActivityConstructorContextHelper.CreateActivity<NotFoundActivity>(notFoundActivityDescriptor, activityRoot, clonedOptions);
LogExceptionsIfAny(notFoundActivityResult);
var notFoundActivity = notFoundActivityResult.Activity;
notFoundActivity.Type = notFoundActivityTypeName;
notFoundActivity.Version = 1;
notFoundActivity.MissingTypeName = activityTypeName;
notFoundActivity.MissingTypeVersion = activityTypeVersion;
notFoundActivity.OriginalActivityJson = activityRoot.ToString();
// Extract metadata from doc.RootElement rather than activityRoot.
// In round-trip scenarios, activityRoot may have been reassigned to the inner originalActivityJson (see line 37),
// but we want the metadata from the current activity being deserialized, which represents the NotFoundActivity
// placeholder's position and annotations in the designer.
if (doc.RootElement.TryGetProperty("metadata", out var outerMetadataElement))
try
{
var outerMetadata = JsonSerializer.Deserialize<IDictionary<string, object>>(outerMetadataElement.GetRawText(), clonedOptions);
if (outerMetadata != null)
var activityRoot = doc.RootElement;
var activityTypeName = GetActivityDetails(activityRoot, out var activityTypeVersion, out var activityDescriptor);
var notFoundActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<NotFoundActivity>();
// If the activity type is a NotFoundActivity, try to extract the original activity type name and version.
if (activityTypeName.Equals(notFoundActivityTypeName) && activityRoot.TryGetProperty("originalActivityJson", out var originalActivityJson))
{
notFoundActivity.Metadata = outerMetadata;
originalActivityDoc = JsonDocument.Parse(originalActivityJson.GetString()!);
activityRoot = originalActivityDoc.RootElement;
activityTypeName = GetActivityDetails(activityRoot, out activityTypeVersion, out activityDescriptor);
}
var clonedOptions = GetClonedOptions(options);
// If the activity type is not found, create a NotFoundActivity instead.
if (activityDescriptor == null)
{
var notFoundActivityDescriptor = activityRegistry.Find<NotFoundActivity>()!;
var notFoundActivityResult = JsonActivityConstructorContextHelper.CreateActivity<NotFoundActivity>(notFoundActivityDescriptor, activityRoot, clonedOptions);
LogExceptionsIfAny(notFoundActivityResult);
var notFoundActivity = notFoundActivityResult.Activity;
notFoundActivity.Type = notFoundActivityTypeName;
notFoundActivity.Version = 1;
notFoundActivity.MissingTypeName = activityTypeName;
notFoundActivity.MissingTypeVersion = activityTypeVersion;
notFoundActivity.OriginalActivityJson = activityRoot.ToString();
// Extract metadata from doc.RootElement rather than activityRoot.
// In round-trip scenarios, activityRoot may have been reassigned to the inner originalActivityJson (see line 42),
// but we want the metadata from the current activity being deserialized, which represents the NotFoundActivity
// placeholder's position and annotations in the designer.
if (doc.RootElement.TryGetProperty("metadata", out var outerMetadataElement))
{
var outerMetadata = JsonSerializer.Deserialize<IDictionary<string, object>>(outerMetadataElement.GetRawText(), clonedOptions);
if (outerMetadata != null)
{
notFoundActivity.Metadata = outerMetadata;
}
}
// Set display text and description after metadata assignment to ensure they always reflect the current state
notFoundActivity.SetDisplayText($"Not Found: {activityTypeName}");
notFoundActivity.SetDescription($"Could not find activity type {activityTypeName} with version {activityTypeVersion}");
return notFoundActivity;
}
var context = JsonActivityConstructorContextHelper.Create(activityDescriptor, activityRoot, clonedOptions);
var activityResult = activityDescriptor.Constructor(context);
LogExceptionsIfAny(activityResult);
return activityResult.Activity;
}
finally
{
originalActivityDoc?.Dispose();
}
// Set display text and description after metadata assignment to ensure they always reflect the current state
notFoundActivity.SetDisplayText($"Not Found: {activityTypeName}");
notFoundActivity.SetDescription($"Could not find activity type {activityTypeName} with version {activityTypeVersion}");
return notFoundActivity;
}
var context = JsonActivityConstructorContextHelper.Create(activityDescriptor, activityRoot, clonedOptions);
var activityResult = activityDescriptor.Constructor(context);
LogExceptionsIfAny(activityResult);
return activityResult.Activity;
}
void LogExceptionsIfAny(ActivityConstructionResult result)

View file

@ -1,4 +1,4 @@
using System.Text.Json;
using System.Text.Json;
using Elsa.Common.Serialization;
using Elsa.Expressions.Services;
using Elsa.Workflows.Activities;
@ -63,10 +63,31 @@ public sealed class ActivityJsonConverterTests
var notFoundActivity = (NotFoundActivity)result;
Assert.Equal(UnknownActivityTypeName, notFoundActivity.MissingTypeName);
Assert.Equal(0, notFoundActivity.MissingTypeVersion);
AssertEquivalentJson(UnknownActivityJson, notFoundActivity.OriginalActivityJson);
Assert.True(notFoundActivity.Metadata.ContainsKey("displayText"));
Assert.True(notFoundActivity.Metadata.ContainsKey("description"));
}
var expectedJsonDoc = JsonDocument.Parse(UnknownActivityJson);
var actualJsonDoc = JsonDocument.Parse(notFoundActivity.OriginalActivityJson);
Assert.Equal(expectedJsonDoc.RootElement.ToString(), actualJsonDoc.RootElement.ToString());
[Fact]
public void When_DeserializeNestedNotFoundActivity_Then_PreservesMissingActivityBehavior()
{
// Arrange
var activityRegistry = Substitute.For<IActivityRegistry>();
activityRegistry
.Find(NotFoundActivityTypeName)
.Returns(new ActivityDescriptor());
var sut = CreateSut(activityRegistry);
// Act
var result = Execute(sut, NestedNotFoundActivityJson);
// Assert
var notFoundActivity = Assert.IsType<NotFoundActivity>(result);
Assert.Equal(UnknownActivityTypeName, notFoundActivity.MissingTypeName);
Assert.Equal(0, notFoundActivity.MissingTypeVersion);
AssertEquivalentJson(UnknownActivityJson, notFoundActivity.OriginalActivityJson);
Assert.True(notFoundActivity.Metadata.ContainsKey("outerMarker"));
Assert.True(notFoundActivity.Metadata.ContainsKey("displayText"));
Assert.True(notFoundActivity.Metadata.ContainsKey("description"));
}
@ -123,6 +144,13 @@ public sealed class ActivityJsonConverterTests
static IActivity? Execute(ActivityJsonConverter sut, string json) =>
JsonSerializer.Deserialize<IActivity>(json, GetSerializerOptions(sut));
static void AssertEquivalentJson(string expectedJson, string actualJson)
{
using var expectedJsonDoc = JsonDocument.Parse(expectedJson);
using var actualJsonDoc = JsonDocument.Parse(actualJson);
Assert.Equal(expectedJsonDoc.RootElement.ToString(), actualJsonDoc.RootElement.ToString());
}
static IActivityRegistry CreateActivityRegistry(string typeName, IActivity activity, int? version = null)
{
var descriptor = new ActivityDescriptor { Constructor = _ => new(activity) };
@ -177,6 +205,18 @@ public sealed class ActivityJsonConverterTests
private static readonly WriteLine WriteLineActivity = new("Hello world!");
private static readonly string WriteLineActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<WriteLine>();
private static readonly string NotFoundActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<NotFoundActivity>();
private static readonly string NestedNotFoundActivityJson =
$$"""
{
"id": "wrapped-not-found",
"type": "{{NotFoundActivityTypeName}}",
"version": 1,
"originalActivityJson": {{JsonSerializer.Serialize(UnknownActivityJson)}},
"metadata": {
"outerMarker": "preserved"
}
}
""";
private const string WriteLineActivityJson_WithVersion =
"""