diff --git a/src/common/Elsa.Features/Implementations/Module.cs b/src/common/Elsa.Features/Implementations/Module.cs index c64056133..4821e91d7 100644 --- a/src/common/Elsa.Features/Implementations/Module.cs +++ b/src/common/Elsa.Features/Implementations/Module.cs @@ -47,11 +47,13 @@ public class Module : IModule } /// - public T Configure(Action? configure = default) where T : class, IFeature - => Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure); + public T Configure(Action? configure = null) where T : class, IFeature + { + return Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure); + } /// - public T Configure(Func factory, Action? configure = default) where T : class, IFeature + public T Configure(Func factory, Action? configure = null) where T : class, IFeature { if (!_features.TryGetValue(typeof(T), out var feature)) { @@ -81,7 +83,7 @@ public class Module : IModule /// public IModule ConfigureHostedService(Type hostedServiceType, int priority = 0) { - _hostedServiceDescriptors.Add(new HostedServiceDescriptor(priority, hostedServiceType)); + _hostedServiceDescriptors.Add(new(priority, hostedServiceType)); return this; } @@ -121,7 +123,7 @@ public class Module : IModule var ns = "Elsa"; var displayName = type.GetCustomAttribute()?.DisplayName ?? name; var description = type.GetCustomAttribute()?.Description; - registry.Add(new FeatureDescriptor(name, ns, displayName, description)); + registry.Add(new(name, ns, displayName, description)); } Services.AddSingleton(registry); diff --git a/src/common/Elsa.Features/Services/IModule.cs b/src/common/Elsa.Features/Services/IModule.cs index 48d45795a..da070d3f6 100644 --- a/src/common/Elsa.Features/Services/IModule.cs +++ b/src/common/Elsa.Features/Services/IModule.cs @@ -31,12 +31,12 @@ public interface IModule /// /// Creates and configures a feature of the specified type. /// - T Configure(Action? configure = default) where T : class, IFeature; + T Configure(Action? configure = null) where T : class, IFeature; /// /// Creates and configures a feature of the specified type. /// - T Configure(Func factory, Action? configure = default) where T : class, IFeature; + T Configure(Func factory, Action? configure = null) where T : class, IFeature; /// /// Configures a using an optional priority to control in which order it will be registered with the service container. diff --git a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs index e7135b2c9..a188ee69a 100644 --- a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs +++ b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs @@ -134,10 +134,11 @@ public class MongoDbStore(IMongoCollection collection, ITe await collection.BulkWriteAsync(writes, cancellationToken: cancellationToken); } - public async Task UpdatePartialAsync( + public async Task UpdatePartialAsync( string id, IDictionary updatedFields, string primaryKey = nameof(Entity.Id), + bool throwIfNotFound = true, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(id)) @@ -154,7 +155,14 @@ public class MongoDbStore(IMongoCollection collection, ITe var updateResult = await collection.UpdateOneAsync(filter, updateDefinition, cancellationToken: cancellationToken); if (updateResult.MatchedCount == 0) + { + if (!throwIfNotFound) + return false; + throw new InvalidOperationException($"No document found with ID '{id}'."); + } + + return updateResult.ModifiedCount > 0; } /// diff --git a/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs b/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs index 09c7afba3..0d9b3b057 100644 --- a/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs +++ b/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs @@ -1,20 +1,16 @@ -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.HostedServices; using Elsa.MongoDb.NamingStrategies; using Elsa.MongoDb.Options; -using Elsa.MongoDb.Serializers; -using Elsa.Workflows.Memory; +using Elsa.Workflows.Activities.Flowchart.Models; using Elsa.Workflows.Runtime.Entities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using MongoDB.Bson; using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; namespace Elsa.MongoDb.Features; @@ -27,7 +23,7 @@ public class MongoDbFeature(IModule module) : FeatureBase(module) /// /// The MongoDB connection string. /// - public string ConnectionString { get; set; } = default!; + public string ConnectionString { get; set; } = null!; /// /// A delegate that configures MongoDb. @@ -39,6 +35,11 @@ public class MongoDbFeature(IModule module) : FeatureBase(module) /// public Func CollectionNamingStrategy { get; set; } = sp => sp.GetRequiredService(); + public override void ConfigureHostedServices() + { + Module.ConfigureHostedService(-10); + } + /// public override void Apply() { @@ -47,24 +48,13 @@ public class MongoDbFeature(IModule module) : FeatureBase(module) var mongoUrl = new MongoUrl(ConnectionString); Services.AddSingleton(sp => CreateMongoClient(sp, mongoUrl)); Services.AddScoped(sp => CreateDatabase(sp, mongoUrl)); - + Services.TryAddScoped(); Services.AddScoped(CollectionNamingStrategy); - RegisterSerializers(); RegisterClassMaps(); } - private static void RegisterSerializers() - { - TryRegisterSerializerOrSkipWhenExist(typeof(object), new PolymorphicSerializer()); - TryRegisterSerializerOrSkipWhenExist(typeof(Type), new TypeSerializer()); - TryRegisterSerializerOrSkipWhenExist(typeof(Variable), new VariableSerializer()); - TryRegisterSerializerOrSkipWhenExist(typeof(Version), new VersionSerializer()); - TryRegisterSerializerOrSkipWhenExist(typeof(JsonElement), new JsonElementSerializer()); - TryRegisterSerializerOrSkipWhenExist(typeof(JsonNode), new JsonNodeBsonConverter()); - } - private static void RegisterClassMaps() { BsonClassMap.TryRegisterClassMap(cm => @@ -78,19 +68,14 @@ public class MongoDbFeature(IModule module) : FeatureBase(module) map.SetIgnoreExtraElements(true); // Needed for missing ID property map.MapProperty(x => x.Key); // Needed for non-setter property }); + + BsonClassMap.TryRegisterClassMap(map => + { + map.AutoMap(); + map.SetIgnoreExtraElements(true); + }); } - private static void TryRegisterSerializerOrSkipWhenExist(Type type, IBsonSerializer serializer) - { - try - { - BsonSerializer.TryRegisterSerializer(type, serializer); - } - catch (BsonSerializationException ex) - { - } - } - private static IMongoClient CreateMongoClient(IServiceProvider sp, MongoUrl mongoUrl) { var options = sp.GetRequiredService>().Value; @@ -99,7 +84,7 @@ public class MongoDbFeature(IModule module) : FeatureBase(module) // TODO: Uncomment once https://github.com/jbogard/MongoDB.Driver.Core.Extensions.DiagnosticSources/pull/41 is merged and deployed. //settings.ClusterConfigurator = cb => cb.Subscribe(new DiagnosticsActivityEventSubscriber()); - + settings.ApplicationName = GetApplicationName(settings); settings.WriteConcern = options.WriteConcern; settings.ReadConcern = options.ReadConcern; diff --git a/src/modules/Elsa.MongoDb/Helpers/ExpressionHelpers.cs b/src/modules/Elsa.MongoDb/Helpers/ExpressionHelpers.cs index e6514a25c..88db796fc 100644 --- a/src/modules/Elsa.MongoDb/Helpers/ExpressionHelpers.cs +++ b/src/modules/Elsa.MongoDb/Helpers/ExpressionHelpers.cs @@ -5,10 +5,10 @@ using Elsa.Workflows.Runtime.Entities; namespace Elsa.MongoDb.Helpers; -internal class ExpressionHelpers +internal static class ExpressionHelpers { public static readonly Expression> WorkflowDefinitionSummary = - workflowDefinition => new WorkflowDefinitionSummary + workflowDefinition => new() { Id = workflowDefinition.Id, DefinitionId = workflowDefinition.DefinitionId, @@ -18,11 +18,14 @@ internal class ExpressionHelpers IsLatest = workflowDefinition.IsLatest, IsPublished = workflowDefinition.IsPublished, MaterializerName = workflowDefinition.MaterializerName, - CreatedAt = workflowDefinition.CreatedAt + CreatedAt = workflowDefinition.CreatedAt, + IsReadonly = workflowDefinition.IsReadonly, + ProviderName = workflowDefinition.ProviderName, + ToolVersion = workflowDefinition.ToolVersion }; public static readonly Expression> WorkflowInstanceSummary = - workflowInstance => new WorkflowInstanceSummary + workflowInstance => new() { Id = workflowInstance.Id, DefinitionId = workflowInstance.DefinitionId, @@ -34,16 +37,17 @@ internal class ExpressionHelpers Name = workflowInstance.Name, CreatedAt = workflowInstance.CreatedAt, UpdatedAt = workflowInstance.UpdatedAt, - FinishedAt = workflowInstance.FinishedAt + FinishedAt = workflowInstance.FinishedAt, + IncidentCount = workflowInstance.IncidentCount, }; - public static readonly Expression> WorkflowInstanceId = workflowInstance => new WorkflowInstanceId + public static readonly Expression> WorkflowInstanceId = workflowInstance => new() { Id = workflowInstance.Id }; public static readonly Expression> ActivityExecutionRecordSummary = - workflowInstance => new ActivityExecutionRecordSummary + workflowInstance => new() { Id = workflowInstance.Id, Status = workflowInstance.Status, @@ -53,6 +57,11 @@ internal class ExpressionHelpers ActivityTypeVersion = workflowInstance.ActivityTypeVersion, ActivityName = workflowInstance.ActivityName, StartedAt = workflowInstance.StartedAt, - HasBookmarks = workflowInstance.HasBookmarks + HasBookmarks = workflowInstance.HasBookmarks, + CompletedAt = workflowInstance.CompletedAt, + AggregateFaultCount = workflowInstance.AggregateFaultCount, + Metadata = workflowInstance.Metadata, + WorkflowInstanceId = workflowInstance.WorkflowInstanceId, + TenantId = workflowInstance.TenantId, }; } \ No newline at end of file diff --git a/src/modules/Elsa.MongoDb/HostedServices/ConfigureMongoDbSerializers.cs b/src/modules/Elsa.MongoDb/HostedServices/ConfigureMongoDbSerializers.cs new file mode 100644 index 000000000..927bc81b0 --- /dev/null +++ b/src/modules/Elsa.MongoDb/HostedServices/ConfigureMongoDbSerializers.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Elsa.MongoDb.Serializers; +using Elsa.Workflows; +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Memory; +using JetBrains.Annotations; +using Microsoft.Extensions.Hosting; +using MongoDB.Bson.Serialization.Serializers; +using static MongoDB.Bson.Serialization.BsonSerializer; + +namespace Elsa.MongoDb.HostedServices; + +/// +/// A hosted service that configures and registers custom MongoDB serializers for various types. +/// +/// +/// This class implements and is responsible for registering serializers to handle +/// specific types such as , , , , +/// , , and . +/// It uses helper methods to register these serializers during the application's startup process. +/// +[UsedImplicitly] +public class ConfigureMongoDbSerializers(IPayloadSerializer payloadSerializer) : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken) + { + TryRegisterSerializer(typeof(object), new PolymorphicSerializer()); + TryRegisterSerializer(typeof(Type), new TypeSerializer()); + TryRegisterSerializer(typeof(Variable), new VariableSerializer()); + TryRegisterSerializer(typeof(Version), new VersionSerializer()); + TryRegisterSerializer(typeof(JsonElement), new JsonElementSerializer()); + TryRegisterSerializer(typeof(JsonNode), new JsonNodeBsonConverter()); + TryRegisterSerializer(typeof(FlowScope), new FlowScopeSerializer(payloadSerializer)); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.MongoDb/Modules/Management/WorkflowInstanceStore.cs b/src/modules/Elsa.MongoDb/Modules/Management/WorkflowInstanceStore.cs index 6913ff35f..03e95c3a5 100644 --- a/src/modules/Elsa.MongoDb/Modules/Management/WorkflowInstanceStore.cs +++ b/src/modules/Elsa.MongoDb/Modules/Management/WorkflowInstanceStore.cs @@ -8,6 +8,7 @@ using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Management.Models; using JetBrains.Annotations; +using Microsoft.Extensions.Logging; using MongoDB.Driver; using MongoDB.Driver.Linq; using Open.Linq.AsyncExtensions; @@ -18,7 +19,7 @@ namespace Elsa.MongoDb.Modules.Management; /// A MongoDb implementation of . /// [UsedImplicitly] -public class MongoWorkflowInstanceStore(MongoDbStore mongoDbStore) : IWorkflowInstanceStore +public class MongoWorkflowInstanceStore(MongoDbStore mongoDbStore, ILogger logger) : IWorkflowInstanceStore { /// public async ValueTask FindAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default) @@ -130,7 +131,10 @@ public class MongoWorkflowInstanceStore(MongoDbStore mongoDbSt [nameof(WorkflowInstance.UpdatedAt)] = value }; - await mongoDbStore.UpdatePartialAsync(workflowInstanceId, props, cancellationToken: cancellationToken); + var updated = await mongoDbStore.UpdatePartialAsync(workflowInstanceId, props, throwIfNotFound: false, cancellationToken: cancellationToken); + + if (!updated) + logger.LogDebug("Failed to update the 'UpdatedAt' timestamp for workflow instance with ID '{WorkflowInstanceId}'. This means this workflow does not yet exist in the DB.", workflowInstanceId); } /// diff --git a/src/modules/Elsa.MongoDb/Serializers/FlowScopeSerializer.cs b/src/modules/Elsa.MongoDb/Serializers/FlowScopeSerializer.cs new file mode 100644 index 000000000..f6d78e931 --- /dev/null +++ b/src/modules/Elsa.MongoDb/Serializers/FlowScopeSerializer.cs @@ -0,0 +1,52 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities.Flowchart.Models; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; + +namespace Elsa.MongoDb.Serializers; + +/// +/// Serializes a . +/// +public class FlowScopeSerializer(IPayloadSerializer payloadSerializer) : IBsonSerializer +{ + /// + public Type ValueType => typeof(FlowScope); + + void IBsonSerializer.Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) => Serialize(context, args, (FlowScope)value); + object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) => Deserialize(context, args); + + /// + public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, FlowScope? value) + { + if (value is null) + context.Writer.WriteNull(); + else + { + var json = payloadSerializer.Serialize(value); + context.Writer.WriteString(json); + } + } + + /// + public FlowScope Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var reader = context.Reader; + var bsonType = reader.GetCurrentBsonType(); + + if (bsonType == BsonType.Null) + { + reader.ReadNull(); + return new(); + } + + if(bsonType == BsonType.String) + { + var json = context.Reader.ReadString(); + + return string.IsNullOrEmpty(json) ? new() : payloadSerializer.Deserialize(json); + } + + return new(); + } +} \ No newline at end of file