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