diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml
index 2869399e3..fb03e222b 100644
--- a/.github/workflows/packages.yml
+++ b/.github/workflows/packages.yml
@@ -51,7 +51,7 @@ jobs:
TAG_NAME=${TAG_NAME#refs/tags/} # remove the refs/tags/ prefix
echo "VERSION=${TAG_NAME}" >> $GITHUB_ENV
else
- echo "VERSION=3.2.1-blueberry.${{github.run_number}}" >> $GITHUB_ENV
+ echo "VERSION=3.2.2-blueberry.${{github.run_number}}" >> $GITHUB_ENV
fi
- name: Set up JDK 17
uses: actions/setup-java@v2
diff --git a/Directory.Build.props b/Directory.Build.props
index 76eef1f08..645da893e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -27,7 +27,7 @@
true
- 3.2.0-rc4.473
+ 3.2.1-preview.558
8.0.5
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4db1b43be..d61250a20 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -165,8 +165,8 @@
-
-
+
+
diff --git a/build/_build.csproj b/build/_build.csproj
index 4e4b70e2d..3ae4c9750 100644
--- a/build/_build.csproj
+++ b/build/_build.csproj
@@ -17,13 +17,12 @@
-
+
-
-
+
diff --git a/src/bundles/Elsa.Server.Web/Program.cs b/src/bundles/Elsa.Server.Web/Program.cs
index d1cb0bcdd..aef18a994 100644
--- a/src/bundles/Elsa.Server.Web/Program.cs
+++ b/src/bundles/Elsa.Server.Web/Program.cs
@@ -49,7 +49,7 @@ const bool runEFCoreMigrations = true;
const bool useMemoryStores = false;
const bool useCaching = true;
const bool useReadOnlyMode = false;
-const bool useSignalR = true;
+const bool useSignalR = false; // Disable until Elsa Studio is updated to send authenticated requests to the SignalR hub.
const bool useAzureServiceBus = false;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit;
const MassTransitBroker useMassTransitBroker = MassTransitBroker.Memory;
diff --git a/src/modules/Elsa.MongoDb/Common/PersistenceFeatureBase.cs b/src/modules/Elsa.MongoDb/Common/PersistenceFeatureBase.cs
index e08af18c4..3cad80172 100644
--- a/src/modules/Elsa.MongoDb/Common/PersistenceFeatureBase.cs
+++ b/src/modules/Elsa.MongoDb/Common/PersistenceFeatureBase.cs
@@ -1,5 +1,6 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
+using Elsa.MongoDb.Contracts;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
@@ -24,8 +25,7 @@ public abstract class PersistenceFeatureBase : FeatureBase
{
Services
.AddScoped>()
- .AddScoped()
- ;
+ .AddScoped();
}
///
@@ -35,8 +35,13 @@ public abstract class PersistenceFeatureBase : FeatureBase
/// The document type of the collection.
protected void AddCollection(string collectionName) where TDocument : class
{
- Services.AddScoped(
- sp => sp.GetRequiredService()
- .GetCollection(collectionName));
+ Services.AddScoped(sp =>
+ {
+ var collectionNamingStrategy = sp.GetRequiredService();
+ var formattedCollectionName = collectionNamingStrategy.GetCollectionName(collectionName);
+
+ return sp.GetRequiredService()
+ .GetCollection(formattedCollectionName);
+ });
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.MongoDb/Contracts/ICollectionNamingStrategy.cs b/src/modules/Elsa.MongoDb/Contracts/ICollectionNamingStrategy.cs
new file mode 100644
index 000000000..ae25b868d
--- /dev/null
+++ b/src/modules/Elsa.MongoDb/Contracts/ICollectionNamingStrategy.cs
@@ -0,0 +1,12 @@
+namespace Elsa.MongoDb.Contracts;
+
+///
+/// Represents a naming strategy to use when creating the name of a MongoDB collection.
+///
+public interface ICollectionNamingStrategy
+{
+ ///
+ /// Returns a collection name from the specified base collection name.
+ ///
+ string GetCollectionName(string collectionName);
+}
diff --git a/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs b/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs
index 2b8014974..e168e37cf 100644
--- a/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs
+++ b/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs
@@ -1,12 +1,17 @@
using System.Text.Json;
+using System.Text.Json.Nodes;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.KeyValues.Entities;
+using Elsa.MongoDb.Contracts;
+using Elsa.MongoDb.NamingStrategies;
using Elsa.MongoDb.Options;
using Elsa.MongoDb.Serializers;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Runtime.Entities;
+using Elsa.Workflows.Services;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
@@ -36,12 +41,22 @@ public class MongoDbFeature : FeatureBase
///
public Action Options { get; set; } = _ => { };
+ ///
+ /// A delegate that creates an instance of an implementation of .
+ ///
+ public Func CollectionNamingStrategy { get; set; } = sp => sp.GetRequiredService();
+
///
public override void Apply()
{
Services.Configure(Options);
- Services.AddScoped(sp => CreateDatabase(sp, ConnectionString));
+ var mongoUrl = new MongoUrl(ConnectionString);
+ Services.AddSingleton(sp => CreateMongoClient(sp, mongoUrl));
+ Services.AddScoped(sp => CreateDatabase(sp, mongoUrl));
+
+ Services.TryAddScoped();
+ Services.AddScoped(CollectionNamingStrategy);
RegisterSerializers();
RegisterClassMaps();
@@ -54,6 +69,7 @@ public class MongoDbFeature : FeatureBase
TryRegisterSerializerOrSkipWhenExist(typeof(Variable), new VariableSerializer());
TryRegisterSerializerOrSkipWhenExist(typeof(Version), new VersionSerializer());
TryRegisterSerializerOrSkipWhenExist(typeof(JsonElement), new JsonElementSerializer());
+ TryRegisterSerializerOrSkipWhenExist(typeof(JsonNode), new JsonNodeBsonConverter());
}
private static void RegisterClassMaps()
@@ -84,11 +100,10 @@ public class MongoDbFeature : FeatureBase
}
}
- private static IMongoDatabase CreateDatabase(IServiceProvider sp, string connectionString)
+ private static IMongoClient CreateMongoClient(IServiceProvider sp, MongoUrl mongoUrl)
{
var options = sp.GetRequiredService>().Value;
- var mongoUrl = new MongoUrl(connectionString);
var settings = MongoClientSettings.FromUrl(mongoUrl);
settings.ClusterConfigurator = cb => cb.Subscribe(new DiagnosticsActivityEventSubscriber());
@@ -100,8 +115,13 @@ public class MongoDbFeature : FeatureBase
settings.RetryWrites = options.RetryWrites;
settings.SslSettings = options.SslSettings;
- var mongoClient = new MongoClient(settings);
- return mongoClient.GetDatabase(mongoUrl.DatabaseName);
+ return new MongoClient(settings);
+ }
+
+ private static IMongoDatabase CreateDatabase(IServiceProvider sp, MongoUrl mongoUrl)
+ {
+ var client = sp.GetRequiredService();
+ return client.GetDatabase(mongoUrl.DatabaseName);
}
private static string GetApplicationName(MongoClientSettings settings) =>
diff --git a/src/modules/Elsa.MongoDb/NamingStrategies/DefaultNamingStrategy.cs b/src/modules/Elsa.MongoDb/NamingStrategies/DefaultNamingStrategy.cs
new file mode 100644
index 000000000..e266d4cbf
--- /dev/null
+++ b/src/modules/Elsa.MongoDb/NamingStrategies/DefaultNamingStrategy.cs
@@ -0,0 +1,17 @@
+using Elsa.MongoDb.Contracts;
+
+namespace Elsa.MongoDb.NamingStrategies;
+
+///
+/// Returns the same collection name, without modifying it.
+///
+public class DefaultNamingStrategy : ICollectionNamingStrategy
+{
+ ///
+ /// Returns the same collection name, without modifying it.
+ ///
+ public string GetCollectionName(string collectionName)
+ {
+ return collectionName;
+ }
+}
diff --git a/src/modules/Elsa.MongoDb/Serializers/JsonNodeBsonSerializer.cs b/src/modules/Elsa.MongoDb/Serializers/JsonNodeBsonSerializer.cs
new file mode 100644
index 000000000..51d793298
--- /dev/null
+++ b/src/modules/Elsa.MongoDb/Serializers/JsonNodeBsonSerializer.cs
@@ -0,0 +1,135 @@
+using System.Text.Json.Nodes;
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+
+namespace Elsa.MongoDb.Serializers;
+
+///
+/// Serializes a .
+///
+public class JsonNodeBsonConverter : IBsonSerializer
+{
+ ///
+ public Type ValueType => typeof(JsonNode);
+
+ ///
+ public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JsonNode value)
+ {
+ if (value == null!)
+ {
+ context.Writer.WriteNull();
+ return;
+ }
+
+ context.Writer.WriteStartDocument();
+ context.Writer.WriteName("type");
+
+ switch (value)
+ {
+ case JsonObject jsonObject:
+ context.Writer.WriteString("JsonObject");
+ context.Writer.WriteName("value");
+ context.Writer.WriteString(jsonObject.ToJsonString());
+ break;
+
+ case JsonArray jsonArray:
+ context.Writer.WriteString("JsonArray");
+ context.Writer.WriteName("value");
+ context.Writer.WriteString(jsonArray.ToJsonString());
+ break;
+
+ case JsonValue jsonValue:
+ context.Writer.WriteString("JsonValue");
+ context.Writer.WriteName("value");
+ if (jsonValue.TryGetValue(out string? stringValue))
+ context.Writer.WriteString(stringValue);
+ else if (jsonValue.TryGetValue(out int intValue))
+ context.Writer.WriteInt32(intValue);
+ else if (jsonValue.TryGetValue(out int longValue))
+ context.Writer.WriteInt64(longValue);
+ else if (jsonValue.TryGetValue(out double doubleValue))
+ context.Writer.WriteDouble(doubleValue);
+ else if (jsonValue.TryGetValue(out bool boolValue))
+ context.Writer.WriteBoolean(boolValue);
+ else if (jsonValue.TryGetValue(out DateTimeOffset dateTimeOffsetValue))
+ context.Writer.WriteDateTime(dateTimeOffsetValue.ToUnixTimeMilliseconds());
+ else if (jsonValue.TryGetValue(out DateTime dateTimeValue))
+ context.Writer.WriteDateTime(new DateTimeOffset(dateTimeValue).ToUnixTimeMilliseconds());
+ else
+ throw new BsonSerializationException("Unsupported JsonValue type");
+ break;
+
+ default:
+ throw new BsonSerializationException($"Unexpected JsonNode type: {value.GetType()}");
+ }
+
+ context.Writer.WriteEndDocument();
+ }
+
+ public JsonNode Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ context.Reader.ReadStartDocument();
+ var type = context.Reader.ReadString();
+ context.Reader.ReadName(Utf8NameDecoder.Instance);
+
+ JsonNode result;
+ switch (type)
+ {
+ case "JsonObject":
+ var jsonObjectString = context.Reader.ReadString();
+ result = JsonNode.Parse(jsonObjectString);
+ break;
+
+ case "JsonArray":
+ var jsonArrayString = context.Reader.ReadString();
+ result = JsonNode.Parse(jsonArrayString);
+ break;
+
+ case "JsonValue":
+ var bsonType = context.Reader.GetCurrentBsonType();
+ switch (bsonType)
+ {
+ case BsonType.String:
+ result = JsonValue.Create(context.Reader.ReadString());
+ break;
+ case BsonType.Int32:
+ result = JsonValue.Create(context.Reader.ReadInt32());
+ break;
+ case BsonType.Int64:
+ result = JsonValue.Create(context.Reader.ReadInt64());
+ break;
+ case BsonType.Double:
+ result = JsonValue.Create(context.Reader.ReadDouble());
+ break;
+ case BsonType.Boolean:
+ result = JsonValue.Create(context.Reader.ReadBoolean());
+ break;
+ case BsonType.DateTime:
+ result = JsonValue.Create(context.Reader.ReadDateTime());
+ break;
+ default:
+ throw new BsonSerializationException($"Unsupported BSON type: {bsonType}");
+ }
+ break;
+
+ default:
+ throw new BsonSerializationException($"Unsupported JsonNode type: {type}");
+ }
+
+ context.Reader.ReadEndDocument();
+ return result!;
+ }
+
+ ///
+ public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
+ {
+ Serialize(context, args, (JsonNode)value);
+ }
+
+ object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ return Deserialize(context, args);
+ }
+}
+
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs
index 1c0baac30..d5deb003f 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs
@@ -74,7 +74,7 @@ internal class Export : ElsaEndpoint
foreach (var definition in definitions)
{
var model = await CreateWorkflowModelAsync(definition, cancellationToken);
- var binaryJson = SerializeWorkflowDefinition(model);
+ var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken);
var fileName = GetFileName(model);
var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal);
await using var entryStream = entry.Open();
@@ -103,7 +103,7 @@ internal class Export : ElsaEndpoint
}
var model = await CreateWorkflowModelAsync(definition, cancellationToken);
- var binaryJson = SerializeWorkflowDefinition(model);
+ var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken);
var fileName = GetFileName(model);
await SendBytesAsync(binaryJson, fileName, cancellation: cancellationToken);
@@ -117,10 +117,25 @@ internal class Export : ElsaEndpoint
return fileName;
}
- private byte[] SerializeWorkflowDefinition(WorkflowDefinitionModel model)
+ private async Task SerializeWorkflowDefinitionAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
- JsonSerializerOptions serializerOptions = _serializer.GetOptions();
- var binaryJson = JsonSerializer.SerializeToUtf8Bytes(model, serializerOptions);
+ var serializerOptions = _serializer.GetOptions();
+ var document = JsonSerializer.SerializeToDocument(model, serializerOptions);
+ var rootElement = document.RootElement;
+
+ using var output = new MemoryStream();
+ await using var writer = new Utf8JsonWriter(output);
+
+ writer.WriteStartObject();
+ writer.WriteString("$schema", "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json");
+
+ foreach (var property in rootElement.EnumerateObject())
+ property.WriteTo(writer);
+
+ writer.WriteEndObject();
+
+ await writer.FlushAsync(cancellationToken);
+ var binaryJson = output.ToArray();
return binaryJson;
}
diff --git a/src/modules/Elsa.Workflows.Api/RealTime/Hubs/WorkflowInstanceHub.cs b/src/modules/Elsa.Workflows.Api/RealTime/Hubs/WorkflowInstanceHub.cs
index 835a955f7..3426c07ef 100644
--- a/src/modules/Elsa.Workflows.Api/RealTime/Hubs/WorkflowInstanceHub.cs
+++ b/src/modules/Elsa.Workflows.Api/RealTime/Hubs/WorkflowInstanceHub.cs
@@ -1,6 +1,7 @@
using Elsa.Workflows.Api.RealTime.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Elsa.Workflows.Api.RealTime.Hubs;
@@ -9,6 +10,7 @@ namespace Elsa.Workflows.Api.RealTime.Hubs;
/// Represents a SignalR hub for receiving workflow events on the client.
///
[PublicAPI]
+[Authorize]
public class WorkflowInstanceHub : Hub
{
private readonly IWorkflowRuntime _workflowRuntime;
diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs b/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs
index 6a5404312..da96c7e30 100644
--- a/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs
+++ b/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs
@@ -1,6 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Workflows.Serialization.Converters;
+using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Serialization.Configurators;
@@ -8,23 +9,14 @@ namespace Elsa.Workflows.Serialization.Configurators;
///
/// Add additional objects.
///
-public class AdditionalConvertersConfigurator : SerializationOptionsConfiguratorBase
+[UsedImplicitly]
+public class AdditionalConvertersConfigurator(IServiceProvider serviceProvider) : SerializationOptionsConfiguratorBase
{
- private readonly IServiceProvider _serviceProvider;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public AdditionalConvertersConfigurator(IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- }
-
///
public override void Configure(JsonSerializerOptions options)
{
options.Converters.Add(Create());
}
- private T Create() => ActivatorUtilities.CreateInstance(_serviceProvider);
+ private T Create() => ActivatorUtilities.CreateInstance(serviceProvider);
}
\ No newline at end of file