Serialization improvements

This commit is contained in:
Sipke Schoorstra 2022-04-09 22:15:48 +02:00
parent 120f7a0341
commit 60e292fac6
32 changed files with 254 additions and 45 deletions

View file

@ -1,8 +1,8 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Management.Serialization;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Requests;
using Elsa.Serialization;
using Microsoft.AspNetCore.Http;
namespace Elsa.Api.Endpoints.WorkflowInstances;

View file

@ -1,9 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Management.Serialization;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Entities;
using Elsa.Persistence.Requests;
using Elsa.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -1,9 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Management.Serialization;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Models;
using Elsa.Persistence.Requests;
using Elsa.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -1,10 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Management.Serialization;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Models;
using Elsa.Persistence.Requests;
using Elsa.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -1,9 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Management.Serialization;
using Elsa.Mediator.Contracts;
using Elsa.Persistence.Models;
using Elsa.Persistence.Requests;
using Elsa.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -3,8 +3,8 @@ using System.Threading.Tasks;
using Elsa.Activities;
using Elsa.Contracts;
using Elsa.Management.Contracts;
using Elsa.Management.Serialization;
using Elsa.Persistence.Mappers;
using Elsa.Serialization;
using Microsoft.AspNetCore.Http;
namespace Elsa.Api.Endpoints.Workflows;

View file

@ -1,6 +1,6 @@
using System.Text.Json;
namespace Elsa.Management.Contracts;
namespace Elsa.Contracts;
public interface ISerializationOptionsConfigurator
{

View file

@ -9,10 +9,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dahomey.Json" Version="1.12.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="6.0.0-preview.4.21253.7" />
<PackageReference Include="System.Text.Json" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Serialization\Converters" />
</ItemGroup>
</Project>

View file

@ -1,4 +1,3 @@
using System;
using System.Linq.Expressions;
using System.Reflection;

View file

@ -1,5 +1,11 @@
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dahomey.Json;
using Dahomey.Json.Serialization.Conventions;
using Elsa.Exceptions;
using DahomeyJsonObject = System.Text.Json.JsonObject;
using DahomeyJsonNode = System.Text.Json.JsonNode;
namespace Elsa.Helpers;
@ -11,17 +17,34 @@ public static class ObjectConverter
{
if (value == null)
return default!;
var sourceType = value.GetType();
if (sourceType == targetType)
return value;
var options = new JsonSerializerOptions();
options.SetupExtensions().SetReferenceHandling(ReferenceHandling.Preserve);
var registry = options.GetDiscriminatorConventionRegistry();
registry.ClearConventions();
registry.RegisterConvention(new DefaultDiscriminatorConvention<string>(options, "_type"));
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.ReferenceHandler = ReferenceHandler.Preserve;
options.PropertyNameCaseInsensitive = true;
options.Converters.Add(new JsonStringEnumConverter());
if (value is DahomeyJsonNode { ValueKind: JsonValueKind.Object } dahomyJsonObject)
return ToObject(dahomyJsonObject, targetType, options);
if (value is JsonElement { ValueKind: JsonValueKind.Object } jsonObject)
return jsonObject.Deserialize(targetType, options);
var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (targetType == typeof(object))
return value;
if (underlyingTargetType.IsInstanceOfType(value))
return value;
@ -29,7 +52,7 @@ public static class ObjectConverter
if (underlyingSourceType == underlyingTargetType)
return value;
var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType);
if (targetTypeConverter.CanConvertFrom(underlyingSourceType))
@ -55,4 +78,10 @@ public static class ObjectConverter
throw new TypeConversionException($"Failed to convert an object of type {sourceType} to {underlyingTargetType}", value, underlyingTargetType, e);
}
}
private static object? ToObject(this DahomeyJsonNode node, Type type, JsonSerializerOptions? options = null)
{
using var arrayBufferWriter = new Dahomey.Json.Util.ArrayBufferWriter<byte>();
return JsonSerializer.Deserialize(node.ToString(), type, options);
}
}

View file

@ -20,7 +20,7 @@ public class Variable : RegisterLocationReference
}
public string? Name { get; set; }
public object? DefaultValue { get; }
public object? DefaultValue { get; set; }
public override RegisterLocation Declare() => new(DefaultValue);
}

View file

@ -0,0 +1,35 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dahomey.Json;
using Dahomey.Json.Serialization.Conventions;
namespace Elsa.Serialization;
public class CustomDiscriminatorConvention : IDiscriminatorConvention
{
private readonly JsonSerializerOptions _options;
private readonly JsonConverter<string> _jsonConverter;
public CustomDiscriminatorConvention(JsonSerializerOptions options)
{
_options = options;
_jsonConverter = options.GetConverter<string>();
}
public ReadOnlySpan<byte> MemberName => Encoding.UTF8.GetBytes("$type");
public bool TryRegisterType(Type type) => true;
public Type ReadDiscriminator(ref Utf8JsonReader reader)
{
var discriminator = _jsonConverter.Read(ref reader, typeof(string), _options)!;
return Type.GetType(discriminator)!;
}
public void WriteDiscriminator(Utf8JsonWriter writer, Type actualType)
{
var discriminator = actualType.AssemblyQualifiedName!;
_jsonConverter.Write(writer, discriminator, _options);
}
}

View file

@ -1,39 +1,47 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Management.Contracts;
using Elsa.Management.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;
using Dahomey.Json;
using Dahomey.Json.Attributes;
using Dahomey.Json.Serialization.Conventions;
using Elsa.Contracts;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Management.Serialization;
namespace Elsa.Serialization;
public class WorkflowSerializerOptionsProvider
{
private readonly IEnumerable<ISerializationOptionsConfigurator> _configurators;
private readonly IServiceProvider _serviceProvider;
public WorkflowSerializerOptionsProvider(IEnumerable<ISerializationOptionsConfigurator> configurators, IServiceProvider serviceProvider)
{
_configurators = configurators;
_serviceProvider = serviceProvider;
}
public JsonSerializerOptions CreateApiOptions() => CreateDefaultOptions();
public JsonSerializerOptions CreatePersistenceOptions() => CreateDefaultOptions(ReferenceHandler.Preserve);
public JsonSerializerOptions CreateDefaultOptions(ReferenceHandler? referenceHandler = default)
public JsonSerializerOptions CreateApiOptions() => CreateDefaultOptions(ReferenceHandling.Ignore);
public JsonSerializerOptions CreatePersistenceOptions() => CreateDefaultOptions(ReferenceHandling.Preserve);
public JsonSerializerOptions CreateDefaultOptions(ReferenceHandling referenceHandling)
{
var options = new JsonSerializerOptions()
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReferenceHandler = referenceHandler,
Converters =
{
Create<JsonStringEnumConverter>(),
Create<TypeJsonConverter>(),
Create<ActivityJsonConverterFactory>(),
Create<ExpressionJsonConverterFactory>()
}
PropertyNameCaseInsensitive = true
};
options.Converters.Add(Create<JsonStringEnumConverter>());
options.Converters.Add(JsonMetadataServices.TimeSpanConverter);
// Dahomey.
options.SetupExtensions();
options.SetReferenceHandling(referenceHandling);
// Setup polymorphic serialization.
var registry = options.GetDiscriminatorConventionRegistry();
registry.RegisterConvention(new DefaultDiscriminatorConvention<string>(options));
registry.DiscriminatorPolicy = DiscriminatorPolicy.Auto;
// Give external packages a chance to further configure the serializer options. E.g. to add additional converters.
foreach (var configurator in _configurators) configurator.Configure(options);

View file

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dahomey.Json" Version="1.12.2" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Text.Json" Version="6.0.2" />

View file

@ -4,6 +4,7 @@ using Elsa.Management.Options;
using Elsa.Management.Providers;
using Elsa.Management.Serialization;
using Elsa.Management.Services;
using Elsa.Serialization;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Management.Extensions;
@ -26,6 +27,7 @@ public static class ServiceCollectionExtensions
.AddSingleton<IExpressionSyntaxRegistry, ExpressionSyntaxRegistry>()
.AddSingleton<IExpressionSyntaxProvider, DefaultExpressionSyntaxProvider>()
.AddSingleton<IExpressionSyntaxRegistryPopulator, ExpressionSyntaxRegistryPopulator>()
.AddSingleton<ISerializationOptionsConfigurator, SerializationOptionsConfigurator>()
.AddSingleton<WorkflowSerializerOptionsProvider>();
}

View file

@ -0,0 +1,25 @@
using System.Text.Json;
using Elsa.Contracts;
using Elsa.Management.Serialization.Converters;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Management.Serialization;
public class SerializationOptionsConfigurator : ISerializationOptionsConfigurator
{
private readonly IServiceProvider _serviceProvider;
public SerializationOptionsConfigurator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Configure(JsonSerializerOptions options)
{
options.Converters.Add(Create<TypeJsonConverter>());
options.Converters.Add(Create<ActivityJsonConverterFactory>());
options.Converters.Add(Create<ExpressionJsonConverterFactory>());
}
private T Create<T>() => ActivatorUtilities.CreateInstance<T>(_serviceProvider);
}

View file

@ -1,6 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Management.Contracts;
using Elsa.Contracts;
using Elsa.Modules.Activities.Converters;
using Microsoft.Extensions.DependencyInjection;

View file

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\..\runtime\Elsa.Runtime\Elsa.Runtime.csproj" />
</ItemGroup>

View file

@ -1,5 +1,4 @@
using Elsa.Contracts;
using Elsa.Management.Contracts;
using Elsa.Modules.Activities.Configurators;
using Elsa.Modules.Activities.Resolvers;
using Microsoft.Extensions.DependencyInjection;

View file

@ -2,7 +2,6 @@ using System.Threading;
using System.Threading.Tasks;
using Elsa.Mediator.Contracts;
using Elsa.Modules.Scheduling.Contracts;
using Elsa.Runtime.Middleware;
using Elsa.Runtime.Models;
using Elsa.Runtime.Notifications;

View file

@ -1,9 +1,9 @@
using System.Text.Json;
using Elsa.Contracts;
using Elsa.Management.Serialization;
using Elsa.Models;
using Elsa.Persistence.Entities;
using Elsa.Persistence.EntityFrameworkCore.Contracts;
using Elsa.Serialization;
namespace Elsa.Persistence.EntityFrameworkCore.Handlers.Serialization;

View file

@ -1,7 +1,7 @@
using System.Text.Json;
using Elsa.Management.Serialization;
using Elsa.Persistence.Entities;
using Elsa.Persistence.EntityFrameworkCore.Contracts;
using Elsa.Serialization;
namespace Elsa.Persistence.EntityFrameworkCore.Handlers.Serialization;

View file

@ -1,8 +1,8 @@
using System.Text.Json;
using Elsa.Management.Serialization;
using Elsa.Persistence.Entities;
using Elsa.Persistence.EntityFrameworkCore.Contracts;
using Elsa.Persistence.Models;
using Elsa.Serialization;
using Elsa.State;
namespace Elsa.Persistence.EntityFrameworkCore.Handlers.Serialization;

View file

@ -48,7 +48,10 @@ public class WorkflowInstanceActor : IActor
private PID GetWorkflowOperatorPid(IContext context, string workflowInstanceId)
{
var actorName = $"workflow-operator:{workflowInstanceId}";
var pid = context.System.ProcessRegistry.SearchByName(actorName).FirstOrDefault();
var pid = context.System.ProcessRegistry.Find(x =>
{
return x == actorName;
}).FirstOrDefault();
if (pid != null)
return pid;

View file

@ -5,7 +5,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Contracts;
using Elsa.Management.Serialization;
using Elsa.Mediator.Contracts;
using Elsa.Models;
using Elsa.Persistence.Models;
@ -13,6 +12,7 @@ using Elsa.Persistence.Requests;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.ProtoActor.Extensions;
using Elsa.Runtime.ProtoActor.Messages;
using Elsa.Serialization;
using Elsa.State;
using Proto;
using Bookmark = Elsa.Runtime.ProtoActor.Messages.Bookmark;

View file

@ -0,0 +1,4 @@
namespace Elsa.Samples.Web1.Models;
public record Order(string Id, int Number, string CustomerId, OrderItem[] Items);
public record OrderItem(string ProductId, int Quantity);

View file

@ -1,11 +1,15 @@
using System;
using System.Text.Json;
using Elsa.Activities;
using Elsa.Api.Extensions;
using Elsa.Contracts;
using Elsa.Extensions;
using Elsa.Jobs.Extensions;
using Elsa.Management.Contracts;
using Elsa.Management.Extensions;
using Elsa.Modules.Activities.Activities.Console;
using Elsa.Modules.Activities.Activities.Workflows;
using Elsa.Modules.Activities.Configurators;
using Elsa.Modules.AzureServiceBus.Activities;
using Elsa.Modules.AzureServiceBus.Extensions;
using Elsa.Modules.Hangfire.Services;
@ -14,6 +18,7 @@ using Elsa.Modules.Http.Extensions;
using Elsa.Modules.JavaScript.Activities;
using Elsa.Modules.Quartz.Services;
using Elsa.Modules.Scheduling.Activities;
using Elsa.Modules.Scheduling.Extensions;
using Elsa.Modules.WorkflowContexts.Extensions;
using Elsa.Persistence.EntityFrameworkCore.Extensions;
using Elsa.Persistence.EntityFrameworkCore.Sqlite;
@ -21,9 +26,12 @@ using Elsa.Pipelines.WorkflowExecution.Components;
using Elsa.Runtime.Extensions;
using Elsa.Runtime.ProtoActor.Extensions;
using Elsa.Samples.Web1.Activities;
using Elsa.Samples.Web1.Models;
using Elsa.Samples.Web1.Serialization;
using Elsa.Samples.Web1.Workflows;
using Elsa.Scripting.JavaScript.Extensions;
using Elsa.Scripting.Liquid.Extensions;
using Elsa.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -45,13 +53,14 @@ services
.IndexWorkflowTriggers()
.AddElsaManagement()
.AddJobServices(new QuartzJobSchedulerProvider(), new HangfireJobQueueProvider(sqlServerConnectionString))
.AddSchedulingServices()
.AddHttpActivityServices()
.AddAzureServiceBusServices(options => configuration.GetSection("AzureServiceBus").Bind(options))
.ConfigureWorkflowRuntime(options =>
{
// Register workflows.
options.Workflows.Add<HelloWorldWorkflow>();
options.Workflows.Add<HeartbeatWorkflow>();
//options.Workflows.Add<HeartbeatWorkflow>();
options.Workflows.Add<HttpWorkflow>();
options.Workflows.Add<ForkedHttpWorkflow>();
options.Workflows.Add<CompositeActivitiesWorkflow>();
@ -60,6 +69,8 @@ services
options.Workflows.Add<RunJavaScriptWorkflow>();
options.Workflows.Add<WorkflowContextsWorkflow>();
options.Workflows.Add<SubmitJobWorkflow>();
options.Workflows.Add<DelayWorkflow>();
options.Workflows.Add<OrderProcessingWorkflow>();
});
// Testing only: allow client app to connect from anywhere.
@ -86,6 +97,10 @@ services
.AddJavaScriptExpressions()
.AddLiquidExpressions();
// Register serialization configurator for configuring what types to allow to be serialized.
services.AddSingleton<ISerializationOptionsConfigurator, CustomSerializationOptionConfigurator>();
services.AddSingleton<ISerializationOptionsConfigurator, SerializationOptionsConfigurator>();
// Configure middleware pipeline.
var app = builder.Build();
var serviceProvider = app.Services;
@ -97,6 +112,11 @@ wellKnownTypeRegistry.RegisterType<float>("float");
wellKnownTypeRegistry.RegisterType<bool>("boolean");
wellKnownTypeRegistry.RegisterType<string>("string");
var order = new Order("order-1", 1, "customer-1", new[] { new OrderItem("product-i1", 2) });
var serializationOptions = serviceProvider.GetRequiredService<WorkflowSerializerOptionsProvider>().CreatePersistenceOptions();
var json = JsonSerializer.Serialize(order, serializationOptions);
Console.WriteLine(json);
// Configure workflow engine execution pipeline.
serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline =>
pipeline

View file

@ -0,0 +1,19 @@
using System.Text.Json;
using Dahomey.Json;
using Dahomey.Json.Attributes;
using Elsa.Contracts;
using Elsa.Samples.Web1.Models;
namespace Elsa.Samples.Web1.Serialization;
public class SerializationOptionsConfigurator : ISerializationOptionsConfigurator
{
public void Configure(JsonSerializerOptions options)
{
options.GetObjectMappingRegistry().Register<Order>(objectMapping => objectMapping
.AutoMap()
.SetDiscriminator("Order")
.SetDiscriminatorPolicy(DiscriminatorPolicy.Always)
.AddDiscriminatorMapping());
}
}

View file

@ -0,0 +1,23 @@
using Elsa.Activities;
using Elsa.Contracts;
using Elsa.Modules.Activities.Activities.Console;
using Elsa.Modules.Scheduling.Activities;
using Elsa.Runtime.Contracts;
namespace Elsa.Samples.Web1.Workflows;
public class DelayWorkflow : IWorkflow
{
public void Build(IWorkflowDefinitionBuilder workflow)
{
workflow.WithRoot(new Sequence
{
Activities =
{
new WriteLine("Sleeping for 5 seconds..."),
Delay.FromSeconds(5),
new WriteLine(context => $"Continuing at {context.GetRequiredService<ISystemClock>().UtcNow}")
}
});
}
}

View file

@ -0,0 +1,29 @@
using Elsa.Activities;
using Elsa.Contracts;
using Elsa.Models;
using Elsa.Modules.Activities.Activities.Console;
using Elsa.Modules.Scheduling.Activities;
using Elsa.Runtime.Contracts;
using Elsa.Samples.Web1.Models;
namespace Elsa.Samples.Web1.Workflows;
public class OrderProcessingWorkflow : IWorkflow
{
public void Build(IWorkflowDefinitionBuilder workflow)
{
var orderVariable = new Variable<Order>();
workflow.WithRoot(new Sequence
{
Variables = { orderVariable },
Activities =
{
new WriteLine("Creating order..."),
new SetVariable<Order>(orderVariable, new Order("order-1", 1, "customer-1", new[] { new OrderItem("product-1", 3) })),
Delay.FromSeconds(5),
new WriteLine(context => $"Shipping order {orderVariable.Get(context)!.Id}."),
}
});
}
}

View file

@ -6,6 +6,7 @@ using Elsa.Contracts;
using Elsa.Jobs.Abstractions;
using Elsa.Jobs.Contracts;
using Elsa.Jobs.Models;
using Elsa.Modules.Activities.Activities.Console;
using Elsa.Runtime.Contracts;
namespace Elsa.Samples.Web1.Workflows;
@ -14,12 +15,20 @@ public class SubmitJobWorkflow : IWorkflow
{
public void Build(IWorkflowDefinitionBuilder workflow)
{
workflow.WithRoot(new Inline(async context =>
workflow.WithRoot(new Sequence
{
var jobQueue = context.GetRequiredService<IJobQueue>();
var job = new ReadTheInternetJob();
await jobQueue.SubmitJobAsync(job, cancellationToken: context.CancellationToken);
}));
Activities =
{
new WriteLine("Downloading..."),
new Inline(async context =>
{
var jobQueue = context.GetRequiredService<IJobQueue>();
var job = new ReadTheInternetJob();
await jobQueue.SubmitJobAsync(job, cancellationToken: context.CancellationToken);
})
// TODO: Implement a "job" activity that blocks the workflow until the job completes.
}
});
}
}

View file

@ -4,7 +4,6 @@ using System.Linq;
using System.Threading.Tasks;
using Elsa.Contracts;
using Elsa.Extensions;
using Elsa.Mediator.Extensions;
using Elsa.Models;
using Elsa.Persistence.InMemory.Extensions;
using Elsa.Pipelines.ActivityExecution.Components;