diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 982cb5090..9426ac521 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -44,7 +44,7 @@ jobs: run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/main + git branch --remote --contains | grep origin/patch/3.1.1 else git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/${BRANCH_NAME} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a2a114dff..057ba4b50 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,6 +13,13 @@ services: ports: - "5432:5432" + mongodb: + image: mongo:latest + ports: + - "127.0.0.1:27017:27017" + volumes: + - mongodb_data:/data/db + cockroachdb: image: cockroachdb/cockroach:v22.1.0 command: start-single-node --insecure @@ -67,3 +74,4 @@ services: volumes: postgres-data: cockroachdb-data: + mongodb_data: diff --git a/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj index e8e800fcd..837512f2b 100644 --- a/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -8,52 +8,52 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - + diff --git a/src/common/Elsa.Features/Implementations/Module.cs b/src/common/Elsa.Features/Implementations/Module.cs index 9f13efc07..1728843cc 100644 --- a/src/common/Elsa.Features/Implementations/Module.cs +++ b/src/common/Elsa.Features/Implementations/Module.cs @@ -34,6 +34,18 @@ public class Module : IModule /// public IDictionary Properties { get; } = new Dictionary(); + /// + public bool HasFeature() where T : class, IFeature + { + return HasFeature(typeof(T)); + } + + /// + public bool HasFeature(Type featureType) + { + return _features.ContainsKey(featureType); + } + /// public T Configure(Action? configure = default) where T : class, IFeature => Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure); diff --git a/src/common/Elsa.Features/Services/IModule.cs b/src/common/Elsa.Features/Services/IModule.cs index 22af7e7c0..48d45795a 100644 --- a/src/common/Elsa.Features/Services/IModule.cs +++ b/src/common/Elsa.Features/Services/IModule.cs @@ -17,6 +17,16 @@ public interface IModule /// A dictionary into which features can stash away values for later use. /// IDictionary Properties { get; } + + /// + /// Returns true if a feature of the specified type has been configured. + /// + bool HasFeature() where T : class, IFeature; + + /// + /// Returns true if a feature of the specified type has been configured. + /// + bool HasFeature(Type featureType); /// /// Creates and configures a feature of the specified type. diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs index 1e727d578..4efe536a3 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs @@ -36,6 +36,7 @@ public abstract class PersistenceFeatureBase : FeatureBase where TDb /// Gets or sets the callback used to configure the . /// public Action DbContextOptionsBuilder = (_, options) => options + .UseElsaDbContextOptions(default) .UseSqlite("Data Source=elsa.sqlite.db;Cache=Shared;", sqlite => sqlite .MigrationsAssembly("Elsa.EntityFrameworkCore.Sqlite") .MigrationsHistoryTable(ElsaDbContextBase.MigrationsHistoryTable, ElsaDbContextBase.ElsaSchema)); diff --git a/src/modules/Elsa.MassTransit.RabbitMq/Features/RabbitMqServiceBusFeature.cs b/src/modules/Elsa.MassTransit.RabbitMq/Features/RabbitMqServiceBusFeature.cs index 4a49012f9..dc8348d8a 100644 --- a/src/modules/Elsa.MassTransit.RabbitMq/Features/RabbitMqServiceBusFeature.cs +++ b/src/modules/Elsa.MassTransit.RabbitMq/Features/RabbitMqServiceBusFeature.cs @@ -78,7 +78,10 @@ public class RabbitMqServiceBusFeature : FeatureBase }); } - configurator.SetupWorkflowDispatcherEndpoints(context); + // Only configure the dispatcher endpoints if the Masstransit Workflow Dispatcher feature is enabled. + if (Module.HasFeature()) + configurator.SetupWorkflowDispatcherEndpoints(context); + configurator.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter("Elsa", false)); }); }; diff --git a/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs b/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs index 4cb6ddd76..8648f0153 100644 --- a/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs +++ b/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs @@ -5,7 +5,9 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; using Elsa.MassTransit.Consumers; +using Elsa.MassTransit.Contracts; using Elsa.MassTransit.Extensions; +using Elsa.MassTransit.Formatters; using Elsa.MassTransit.Models; using Elsa.MassTransit.Options; using Elsa.MassTransit.Services; @@ -37,6 +39,11 @@ public class MassTransitFeature : FeatureBase /// A delegate that can be set to configure MassTransit's . Used by transport-level features such as AzureServiceBusFeature and RabbitMqServiceBusFeature. /// public Action? BusConfigurator { get; set; } + + /// + /// A factory that creates a . + /// + public Func ChannelQueueFormatterFactory { get; set; } = _ => new DefaultEndpointChannelFormatter(); /// public override void Configure() @@ -48,6 +55,7 @@ public class MassTransitFeature : FeatureBase { var messageTypes = this.GetMessages(); + Services.AddSingleton(ChannelQueueFormatterFactory); Services.Configure(x => { }); Services.AddActivityProvider(); _runInMemory = BusConfigurator is null; diff --git a/src/modules/Elsa.MassTransit/Features/MassTransitWorkflowDispatcherFeature.cs b/src/modules/Elsa.MassTransit/Features/MassTransitWorkflowDispatcherFeature.cs index 1e39520ee..032dbdc23 100644 --- a/src/modules/Elsa.MassTransit/Features/MassTransitWorkflowDispatcherFeature.cs +++ b/src/modules/Elsa.MassTransit/Features/MassTransitWorkflowDispatcherFeature.cs @@ -4,8 +4,6 @@ using Elsa.Features.Attributes; using Elsa.Features.Services; using Elsa.MassTransit.ConsumerDefinitions; using Elsa.MassTransit.Consumers; -using Elsa.MassTransit.Contracts; -using Elsa.MassTransit.Formatters; using Elsa.MassTransit.Options; using Elsa.MassTransit.Services; using Elsa.Workflows.Runtime.Contracts; @@ -31,11 +29,7 @@ public class MassTransitWorkflowDispatcherFeature : FeatureBase /// Configures the MassTransit workflow dispatcher. /// public Action? ConfigureDispatcherOptions { get; set; } - - /// - /// A factory that creates a . - /// - public Func ChannelQueueFormatterFactory { get; set; } = _ => new DefaultEndpointChannelFormatter(); + /// public override void Configure() @@ -62,7 +56,6 @@ public class MassTransitWorkflowDispatcherFeature : FeatureBase if (ConfigureDispatcherOptions != null) options.Configure(ConfigureDispatcherOptions); - Services.AddSingleton(ChannelQueueFormatterFactory); Services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs index 8a9051797..5c34e049a 100644 --- a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs +++ b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs @@ -15,18 +15,18 @@ namespace Elsa.MongoDb.Common; public class MongoDbStore where TDocument : class { private readonly IMongoCollection _collection; - + /// public MongoDbStore(IMongoCollection collection) { _collection = collection; } - + /// /// Returns a queryable collection of documents. /// public IMongoCollection GetCollection() => _collection; - + /// /// Saves the document. /// @@ -37,7 +37,7 @@ public class MongoDbStore where TDocument : class await _collection.InsertOneAsync(document, new InsertOneOptions(), cancellationToken); return document; } - + /// /// Saves a list of documents. /// @@ -47,7 +47,7 @@ public class MongoDbStore where TDocument : class { await _collection.InsertManyAsync(documents, new InsertManyOptions(), cancellationToken); } - + /// /// Saves the document. /// @@ -55,7 +55,11 @@ public class MongoDbStore where TDocument : class /// The cancellation token. public async Task SaveAsync(TDocument document, CancellationToken cancellationToken = default) { - return await _collection.FindOneAndReplaceAsync(document.BuildIdFilter(), document, new FindOneAndReplaceOptions{ ReturnDocument = ReturnDocument.After, IsUpsert = true }, cancellationToken); + return await _collection.FindOneAndReplaceAsync(document.BuildIdFilter(), document, new FindOneAndReplaceOptions + { + ReturnDocument = ReturnDocument.After, + IsUpsert = true + }, cancellationToken); } /// @@ -66,9 +70,13 @@ public class MongoDbStore where TDocument : class /// The cancellation token. public async Task SaveAsync(TDocument document, Expression> selector, CancellationToken cancellationToken = default) { - return await _collection.FindOneAndReplaceAsync(document.BuildExpression(selector), document, new FindOneAndReplaceOptions{ ReturnDocument = ReturnDocument.After, IsUpsert = true }, cancellationToken); + return await _collection.FindOneAndReplaceAsync(document.BuildExpression(selector), document, new FindOneAndReplaceOptions + { + ReturnDocument = ReturnDocument.After, + IsUpsert = true + }, cancellationToken); } - + /// /// Saves the specified documents. /// @@ -80,10 +88,16 @@ public class MongoDbStore where TDocument : class foreach (var document in documents) { - var replacement = new ReplaceOneModel(document.BuildIdFilter(), document) { IsUpsert = true }; + var replacement = new ReplaceOneModel(document.BuildIdFilter(), document) + { + IsUpsert = true + }; writes.Add(replacement); } + if (!writes.Any()) + return; + await _collection.BulkWriteAsync(writes, cancellationToken: cancellationToken); } @@ -99,10 +113,16 @@ public class MongoDbStore where TDocument : class foreach (var document in documents) { - var replacement = new ReplaceOneModel(document.BuildFilter(primaryKey), document) { IsUpsert = true }; + var replacement = new ReplaceOneModel(document.BuildFilter(primaryKey), document) + { + IsUpsert = true + }; writes.Add(replacement); } + if (!writes.Any()) + return; + await _collection.BulkWriteAsync(writes, cancellationToken: cancellationToken); } @@ -112,60 +132,60 @@ public class MongoDbStore where TDocument : class /// The predicate to use. /// The cancellation token. /// The document if found, otherwise null. - public async Task FindAsync(Expression> predicate, CancellationToken cancellationToken = default) => + public async Task FindAsync(Expression> predicate, CancellationToken cancellationToken = default) => await _collection.AsQueryable().Where(predicate).FirstOrDefaultAsync(cancellationToken); - + /// /// Finds a single document using a query /// /// The query to use /// The cancellation token /// The document if found, otherwise null - public async Task FindAsync(Func, IMongoQueryable> query, CancellationToken cancellationToken = default) => + public async Task FindAsync(Func, IMongoQueryable> query, CancellationToken cancellationToken = default) => await query(_collection.AsQueryable()).FirstOrDefaultAsync(cancellationToken); /// /// Finds a list of documents matching the specified predicate /// - public async Task> FindManyAsync(Expression> predicate, CancellationToken cancellationToken = default) => + public async Task> FindManyAsync(Expression> predicate, CancellationToken cancellationToken = default) => await _collection.AsQueryable().Where(predicate).ToListAsync(cancellationToken); - + /// /// Queries the database using a query and a selector. /// - public async Task> FindManyAsync(Func, IMongoQueryable> query, Expression> selector, CancellationToken cancellationToken = default) => + public async Task> FindManyAsync(Func, IMongoQueryable> query, Expression> selector, CancellationToken cancellationToken = default) => await query(_collection.AsQueryable()).Select(selector).ToListAsync(cancellationToken); /// /// Finds a list of documents using a query /// - public async Task> FindManyAsync(Func, IMongoQueryable> query, CancellationToken cancellationToken = default) => + public async Task> FindManyAsync(Func, IMongoQueryable> query, CancellationToken cancellationToken = default) => await query(_collection.AsQueryable()).ToListAsync(cancellationToken); - + /// /// Queries the database using a query and a selector. /// - public async Task> FindMany(Func, IMongoQueryable> query, Expression> selector, CancellationToken cancellationToken = default) => + public async Task> FindMany(Func, IMongoQueryable> query, Expression> selector, CancellationToken cancellationToken = default) => await query(_collection.AsQueryable()).Select(selector).ToListAsync(cancellationToken); - + /// /// Counts documents in the collection using a filter. /// - public async Task CountAsync(Func, IMongoQueryable> query, CancellationToken cancellationToken = default) => + public async Task CountAsync(Func, IMongoQueryable> query, CancellationToken cancellationToken = default) => await query(_collection.AsQueryable()).LongCountAsync(cancellationToken); - + /// /// Counts documents in the collection using a filter and distinct by a key selector. /// - public async Task CountAsync(Func, IMongoQueryable> query, Expression> propertySelector, CancellationToken cancellationToken = default) => + public async Task CountAsync(Func, IMongoQueryable> query, Expression> propertySelector, CancellationToken cancellationToken = default) => await query((IMongoQueryable)_collection.AsQueryable().DistinctBy(propertySelector)).LongCountAsync(cancellationToken); - + /// /// Checks if any documents exist. /// - public async Task AnyAsync(Expression> predicate, CancellationToken cancellationToken = default) => + public async Task AnyAsync(Expression> predicate, CancellationToken cancellationToken = default) => await _collection.AsQueryable().Where(predicate).AnyAsync(cancellationToken); - + /// /// Deletes documents using a predicate. /// @@ -174,12 +194,12 @@ public class MongoDbStore where TDocument : class { var documentsToDelete = await _collection.AsQueryable().Where(predicate).ToListAsync(cancellationToken); var count = documentsToDelete.LongCount(); - + await _collection.DeleteManyAsync(predicate, cancellationToken); return count; } - + /// /// Deletes documents using a query. /// @@ -189,7 +209,7 @@ public class MongoDbStore where TDocument : class var key = keySelector.GetPropertyName(); return await DeleteWhereAsync(query, key, cancellationToken); } - + /// /// Deletes documents using a query. /// diff --git a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs index 2be0f17c2..9f1a3240d 100644 --- a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs +++ b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs @@ -1,4 +1,3 @@ -using Elsa.Common.Contracts; using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; @@ -6,10 +5,8 @@ using Elsa.Features.Services; using Elsa.Http.Features; using Elsa.SasTokens.Features; using Elsa.Workflows.Api.Serialization; -using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Features; -using Elsa.Workflows.Services; -using Microsoft.Extensions.DependencyInjection; +using Elsa.Workflows.Runtime.Features; namespace Elsa.Workflows.Api.Features; @@ -18,6 +15,7 @@ namespace Elsa.Workflows.Api.Features; /// [DependsOn(typeof(WorkflowInstancesFeature))] [DependsOn(typeof(WorkflowManagementFeature))] +[DependsOn(typeof(WorkflowRuntimeFeature))] [DependsOn(typeof(HttpFeature))] [DependsOn(typeof(SasTokensFeature))] public class WorkflowsApiFeature : FeatureBase diff --git a/src/modules/Elsa.Workflows.Core/Models/DynamicActivity.cs b/src/modules/Elsa.Workflows.Core/Activities/DynamicActivity.cs similarity index 93% rename from src/modules/Elsa.Workflows.Core/Models/DynamicActivity.cs rename to src/modules/Elsa.Workflows.Core/Activities/DynamicActivity.cs index 593bd5490..346abdffc 100644 --- a/src/modules/Elsa.Workflows.Core/Models/DynamicActivity.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/DynamicActivity.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using Elsa.Workflows.Services; -namespace Elsa.Workflows.Models; +namespace Elsa.Workflows.Activities; /// /// A dynamically provided activity with custom properties. This is experimental and may be removed. diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs index 1c40937ea..078be3bea 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using Elsa.Workflows.Activities.Flowchart.Models; + namespace Elsa.Workflows; /// @@ -21,4 +24,19 @@ public record ActivityCompletedContext(ActivityExecutionContext TargetContext, A /// A cancellation token to use when invoking asynchronous operations. /// public CancellationToken CancellationToken => WorkflowExecutionContext.CancellationTokens.ApplicationCancellationToken; + + /// + /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public async ValueTask CompleteActivityAsync(object? result = default) + { + await TargetContext.CompleteActivityAsync(result); + } + + /// + /// Complete the current activity with the specified outcomes. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public ValueTask CompleteActivityWithOutcomesAsync(params string[] outcomes) => CompleteActivityAsync(new Outcomes(outcomes)); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs index 082935672..808577679 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs @@ -19,9 +19,17 @@ public partial class ActivityExecutionContext _ = Task.Run(async () => await CancelActivityAsync()); } + + private bool CanCancelActivity() + { + return Status is not ActivityStatus.Canceled and not ActivityStatus.Completed; + } private async Task CancelActivityAsync() { + if(!CanCancelActivity()) + return; + // Select all child contexts. var childContexts = WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == this).ToList(); diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs new file mode 100644 index 000000000..f3963ed91 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs @@ -0,0 +1,109 @@ +using System.Diagnostics.CodeAnalysis; +using Elsa.Extensions; +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Signals; + +namespace Elsa.Workflows; + +public partial class ActivityExecutionContext +{ + /// + /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public async ValueTask CompleteActivityAsync(object? result = default) + { + var outcomes = result as Outcomes; + + // If the activity is executing in the background, simply capture the result and return. + if (this.GetIsBackgroundExecution()) + { + if (outcomes != null) + this.SetBackgroundOutcomes(outcomes.Names); + else + this.SetBackgroundCompletion(); + return; + } + + // If the activity is not running, do nothing. + if (Status != ActivityStatus.Running) + return; + + // Cancel any non-completed child activities. + var childContexts = WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == this && x.CanCancelActivity()).ToList(); + + foreach (var childContext in childContexts) + await childContext.CancelActivityAsync(); + + // Mark the activity as complete. + TransitionTo(ActivityStatus.Completed); + + // Record the outcomes, if any. + if (outcomes != null) + JournalData["Outcomes"] = outcomes.Names; + + // Record the output, if any. + var activity = Activity; + var expressionExecutionContext = ExpressionExecutionContext; + var activityDescriptor = ActivityDescriptor; + var outputDescriptors = activityDescriptor.Outputs; + var outputs = outputDescriptors.ToDictionary(x => x.Name, x => activity.GetOutput(expressionExecutionContext, x.Name)!); + var serializer = GetRequiredService(); + + foreach (var outputDescriptor in outputDescriptors) + { + if (outputDescriptor.IsSerializable == false) + continue; + + var outputName = outputDescriptor.Name; + var outputValue = outputs[outputName]; + + if (outputValue == null!) + continue; + + var serializedOutputValue = await serializer.SerializeAsync(outputValue, CancellationToken); + JournalData[outputName] = serializedOutputValue; + } + + // Add an execution log entry. + AddExecutionLogEntry("Completed", payload: JournalData); + + // Send a signal. + await this.SendSignalAsync(new ActivityCompleted(result)); + + // Clear bookmarks. + ClearBookmarks(); + WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityInstanceId == Id); + + // Remove completion callbacks. + ClearCompletionCallbacks(); + + // Remove all associated variables, unless this is the root context - in which case we want to keep the variables since we're not deleting that one. + if (ParentActivityExecutionContext != null) + { + var variablePersistenceManager = GetRequiredService(); + await variablePersistenceManager.DeleteVariablesAsync(this); + } + + // Update the completed at timestamp. + CompletedAt = WorkflowExecutionContext.SystemClock.UtcNow; + } + + /// + /// Complete the current activity with the specified outcomes. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public ValueTask CompleteActivityWithOutcomesAsync(params string[] outcomes) + { + return CompleteActivityAsync(new Outcomes(outcomes)); + } + + /// + /// Complete the current composite activity with the specified outcome. + /// + public async ValueTask CompleteCompositeAsync(params string[] outcomes) + { + await this.SendSignalAsync(new CompleteCompositeSignal(new Outcomes(outcomes))); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index 2fc1d4d94..2be9c0b3d 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -136,13 +136,11 @@ public partial class ActivityExecutionContext : IExecutionContext public void TransitionTo(ActivityStatus status) { Status = status; - - if (Status is ActivityStatus.Completed - or ActivityStatus.Canceled - or ActivityStatus.Faulted) + + if (Status is ActivityStatus.Completed or ActivityStatus.Canceled or ActivityStatus.Faulted) _cancellationRegistration.Dispose(); } - + /// /// Gets or sets the exception that occurred during the activity execution, if any. /// @@ -254,15 +252,17 @@ public partial class ActivityExecutionContext : IExecutionContext { ActivityNodeId = activityNode?.NodeId, OwnerActivityInstanceId = owner?.Id, - Options = options != null ? new ScheduledActivityOptions - { - CompletionCallback = options?.CompletionCallback?.Method.Name, - Tag = options?.Tag, - ExistingActivityInstanceId = options?.ExistingActivityExecutionContext?.Id, - PreventDuplicateScheduling = options?.PreventDuplicateScheduling ?? false, - Variables = options?.Variables?.ToList(), - Input = options?.Input - } : default + Options = options != null + ? new ScheduledActivityOptions + { + CompletionCallback = options?.CompletionCallback?.Method.Name, + Tag = options?.Tag, + ExistingActivityInstanceId = options?.ExistingActivityExecutionContext?.Id, + PreventDuplicateScheduling = options?.PreventDuplicateScheduling ?? false, + Variables = options?.Variables?.ToList(), + Input = options?.Input + } + : default }; var scheduledActivities = this.GetBackgroundScheduledActivities().ToList(); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 4d934abcf..5bcc6f3c1 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; using System.Text.Json; @@ -6,7 +7,6 @@ using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Mediator.Contracts; using Elsa.Workflows; -using Elsa.Workflows.Activities.Flowchart.Models; using Elsa.Workflows.Attributes; using Elsa.Workflows.Contracts; using Elsa.Workflows.Memory; @@ -406,95 +406,7 @@ public static class ActivityExecutionContextExtensions /// /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. /// - public static async ValueTask CompleteActivityAsync(this ActivityCompletedContext context, object? result = default) - { - await context.TargetContext.CompleteActivityAsync(result); - } - - /// - /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. - /// - public static async ValueTask CompleteActivityAsync(this ActivityExecutionContext context, object? result = default) - { - var outcomes = result as Outcomes; - - // If the activity is executing in the background, simply capture the result and return. - if (context.GetIsBackgroundExecution()) - { - if (outcomes != null) - context.SetBackgroundOutcomes(outcomes.Names); - else - context.SetBackgroundCompletion(); - return; - } - - // If the activity is not running, do nothing. - if (context.Status != ActivityStatus.Running) - return; - - // Update all child contexts. - var childContexts = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context).ToList(); - - foreach (var childContext in childContexts) - await childContext.CancelActivityAsync(); - - // Mark the activity as complete. - context.TransitionTo(ActivityStatus.Completed); - - // Record the outcomes, if any. - if (outcomes != null) - context.JournalData["Outcomes"] = outcomes.Names; - - // Record the output, if any. - var activity = context.Activity; - var expressionExecutionContext = context.ExpressionExecutionContext; - var activityDescriptor = context.ActivityDescriptor; - var outputDescriptors = activityDescriptor.Outputs; - var outputs = outputDescriptors.ToDictionary(x => x.Name, x => activity.GetOutput(expressionExecutionContext, x.Name)!); - var serializer = context.GetRequiredService(); - - foreach (var outputDescriptor in outputDescriptors) - { - if (outputDescriptor.IsSerializable == false) - continue; - - var outputName = outputDescriptor.Name; - var outputValue = outputs[outputName]; - - if (outputValue == null!) - continue; - - var serializedOutputValue = await serializer.SerializeAsync(outputValue); - context.JournalData[outputName] = serializedOutputValue; - } - - // Add an execution log entry. - context.AddExecutionLogEntry("Completed", payload: context.JournalData); - - // Send a signal. - await context.SendSignalAsync(new ActivityCompleted(result)); - - // Clear bookmarks. - context.ClearBookmarks(); - context.WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityInstanceId == context.Id); - - // Remove completion callbacks. - context.ClearCompletionCallbacks(); - - // Remove all associated variables, unless this is the root context - in which case we want to keep the variables since we're not deleting that one. - if (context.ParentActivityExecutionContext != null) - { - var variablePersistenceManager = context.GetRequiredService(); - await variablePersistenceManager.DeleteVariablesAsync(context); - } - - // Update the completed at timestamp. - context.CompletedAt = context.WorkflowExecutionContext.SystemClock.UtcNow; - } - - /// - /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. - /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] public static async ValueTask ScheduleOutcomesAsync(this ActivityExecutionContext context, params string[] outcomes) { var cancellationToken = context.CancellationToken; @@ -525,22 +437,7 @@ public static class ActivityExecutionContextExtensions // Send a signal. await context.SendSignalAsync(new ScheduleActivityOutcomes(outcomes)); } - - /// - /// Complete the current activity with the specified outcome. - /// - public static ValueTask CompleteActivityWithOutcomesAsync(this ActivityCompletedContext context, params string[] outcomes) => context.CompleteActivityAsync(new Outcomes(outcomes)); - - /// - /// Complete the current activity with the specified outcome. - /// - public static ValueTask CompleteActivityWithOutcomesAsync(this ActivityExecutionContext context, params string[] outcomes) => context.CompleteActivityAsync(new Outcomes(outcomes)); - - /// - /// Complete the current composite activity with the specified outcome. - /// - public static async ValueTask CompleteCompositeAsync(this ActivityExecutionContext context, params string[] outcomes) => await context.SendSignalAsync(new CompleteCompositeSignal(new Outcomes(outcomes))); - + /// /// Cancel the activity. For blocking activities, it means their bookmarks will be removed. For job activities, the background work will be cancelled. /// diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs index da916646c..279f5bce2 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExtensions.cs @@ -137,6 +137,21 @@ public static class ActivityExtensions var outputName = outputExpression.GetPropertyName(); return ((IActivity)activity!).GetOutput(context, outputName); } + + /// + /// Gets the output with the specified name. + /// + /// The activity. + /// The context. + /// The output expression. + /// The type of the activity. + /// The type of the output. + /// The output value. + public static T? GetOutput(this TActivity activity, ExpressionExecutionContext context, Expression> outputExpression) + { + var outputName = outputExpression.GetPropertyName(); + return ((IActivity)activity!).GetOutput(context, outputName); + } /// /// Gets the Result output of the specified activity. diff --git a/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs index aef490975..422e4f2c6 100644 --- a/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs +++ b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs @@ -16,14 +16,14 @@ public class CreateBookmarkArgs /// An optional name to associate with the bookmark. public string? BookmarkName { get; set; } - /// Whether or not the bookmark should be automatically burned when triggered. + /// Whether the bookmark should be automatically burned when triggered. public bool AutoBurn { get; set; } = true; - /// Whether or not the activity instance ID should be included in the bookmark payload. + /// Whether the activity instance ID should be included in the bookmark payload. public bool IncludeActivityInstanceId { get; set; } /// - /// Whether or not the activity being resumed should be automatically completed if CallBack is not specified. + /// Whether the activity being resumed should be automatically completed if CallBack is not specified. /// public bool AutoComplete { get; set; } = true; diff --git a/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs b/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs index 997c70296..7c14eb238 100644 --- a/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs +++ b/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Elsa.Expressions.Contracts; using Elsa.Workflows.Contracts; @@ -30,6 +31,7 @@ public class BookmarkHasher : IBookmarkHasher } /// + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(Object, Type, JsonSerializerOptions)")] public string Hash(string activityTypeName, object? payload, string? activityInstanceId = default) { var json = payload != null ? Serialize(payload) : null; @@ -47,5 +49,12 @@ public class BookmarkHasher : IBookmarkHasher return hash; } - private string Serialize(object payload) => JsonSerializer.Serialize(payload, payload.GetType(), _settings); + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(Object, Type, JsonSerializerOptions)")] + private string Serialize(object payload) + { + if(payload is string s) + return s; + + return JsonSerializer.Serialize(payload, payload.GetType(), _settings); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs index 93a9795d7..5ff845575 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs @@ -189,13 +189,13 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime var startOptions = new StartWorkflowRuntimeParams { - CorrelationId = options.CorrelationId, - Input = options.Input, - Properties = options.Properties, + CorrelationId = options?.CorrelationId, + Input = options?.Input, + Properties = options?.Properties, VersionOptions = VersionOptions.Published, TriggerActivityId = trigger.ActivityId, - InstanceId = options.WorkflowInstanceId, - CancellationTokens = options.CancellationTokens + InstanceId = options?.WorkflowInstanceId, + CancellationTokens = options?.CancellationTokens ?? default }; var canStartResult = await CanStartWorkflowAsync(definitionId, startOptions); @@ -276,10 +276,10 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime /// public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options) { - var hash = _hasher.Hash(activityTypeName, bookmarkPayload, options.ActivityInstanceId); - var correlationId = options.CorrelationId; - var workflowInstanceId = options.WorkflowInstanceId; - var activityInstanceId = options.ActivityInstanceId; + var hash = _hasher.Hash(activityTypeName, bookmarkPayload, options?.ActivityInstanceId); + var correlationId = options?.CorrelationId; + var workflowInstanceId = options?.WorkflowInstanceId; + var activityInstanceId = options?.ActivityInstanceId; var filter = new BookmarkFilter { Hash = hash, @@ -287,15 +287,15 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime WorkflowInstanceId = workflowInstanceId, ActivityInstanceId = activityInstanceId }; - var bookmarks = await _bookmarkStore.FindManyAsync(filter, options.CancellationTokens.SystemCancellationToken); + var bookmarks = await _bookmarkStore.FindManyAsync(filter, options?.CancellationTokens.SystemCancellationToken ?? default); return await ResumeWorkflowsAsync( bookmarks, new ResumeWorkflowRuntimeParams { CorrelationId = correlationId, - Input = options.Input, - CancellationTokens = options.CancellationTokens + Input = options?.Input, + CancellationTokens = options?.CancellationTokens ?? default }); }