diff --git a/Elsa.sln b/Elsa.sln index eb2b7ecd7..07c79be7e 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -150,6 +150,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Webhooks.Workf EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Webhooks.ExternalApp", "src\samples\aspnet\Elsa.Samples.Webhooks.ExternalApp\Elsa.Samples.Webhooks.ExternalApp.csproj", "{036D287A-33E1-4B28-BE13-14AEA16BC91F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.MassTransitActivities", "src\samples\aspnet\Elsa.Samples.MassTransitActivities\Elsa.Samples.MassTransitActivities.csproj", "{A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -380,6 +382,10 @@ Global {036D287A-33E1-4B28-BE13-14AEA16BC91F}.Debug|Any CPU.Build.0 = Debug|Any CPU {036D287A-33E1-4B28-BE13-14AEA16BC91F}.Release|Any CPU.ActiveCfg = Release|Any CPU {036D287A-33E1-4B28-BE13-14AEA16BC91F}.Release|Any CPU.Build.0 = Release|Any CPU + {A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} @@ -445,5 +451,6 @@ Global {2BBFDE36-28A7-4875-8EBE-CC25C76B97FF} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {130A7A00-A9AF-4EA8-8107-BBEA07F166DF} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} {036D287A-33E1-4B28-BE13-14AEA16BC91F} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} EndGlobalSection EndGlobal diff --git a/src/bundles/Elsa.WorkflowServer.Web/Activities/GetPizza.cs b/src/bundles/Elsa.WorkflowServer.Web/Activities/GetPizza.cs deleted file mode 100644 index d6dfd6385..000000000 --- a/src/bundles/Elsa.WorkflowServer.Web/Activities/GetPizza.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Elsa.Extensions; -using Elsa.Scheduling.Activities; -using Elsa.Workflows.Core.Activities; -using Elsa.Workflows.Core.Attributes; -using Elsa.Workflows.Core.Models; -using Elsa.Workflows.Management.Models; -using Elsa.WorkflowServer.Web.Models; - -namespace Elsa.WorkflowServer.Web.Activities; - -[Activity("Demo", "Models the entire process of ordering, preparing, and delivering a pizza.")] -public class GetPizza : Composite -{ - [Input( - UIHint = InputUIHints.Dropdown, - Options = new[] { "Margaritha", "Fungi", "Veggie", "Carbonara", "Pepperoni", "Hawaii" }, - DefaultValue = "Margaritha" - )] - public Input Flavor { get; set; } = new("Margaritha"); - - [Input( - UIHint = InputUIHints.Dropdown, - Options = new[] { 20, 30, 40, 80 } - )] - public Input Size { get; set; } = default!; - - public GetPizza() - { - Root = new Sequence - { - Activities = - { - new WriteLine("Submitting order..."), - Delay.FromSeconds(2), - new WriteLine("Order submitted"), - Delay.FromSeconds(2), - new Fork - { - JoinMode = ForkJoinMode.WaitAll, - Branches = - { - new Sequence - { - Activities = - { - new WriteLine("Heating oven..."), - Delay.FromSeconds(2), - new WriteLine("Oven heated."), - } - }, - new Sequence - { - Activities = - { - new WriteLine("Preparing dough and toppings..."), - Delay.FromSeconds(2), - new WriteLine("Pizza ready for heating."), - } - } - } - }, - new WriteLine("Heating pizza in oven..."), - Delay.FromSeconds(2), - new WriteLine("Pizza is ready for delivery!"), - Delay.FromSeconds(2), - Inline(context => context.Set(Result, new Pizza(Size.Get(context), Flavor.Get(context)))), - } - }; - } -} \ No newline at end of file diff --git a/src/bundles/Elsa.WorkflowServer.Web/Activities/ProcessVideo.cs b/src/bundles/Elsa.WorkflowServer.Web/Activities/ProcessVideo.cs deleted file mode 100644 index 7d2ab0f64..000000000 --- a/src/bundles/Elsa.WorkflowServer.Web/Activities/ProcessVideo.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Elsa.Workflows.Core.Attributes; -using Elsa.Workflows.Core.Models; - -namespace Elsa.WorkflowServer.Web.Activities; - -/// -/// A sample activity that simulates doing some very heavy lifting. -/// -[Activity("Demo", "Demo", "Simulates very heavy lifting, which takes 15 seconds to complete.", Kind = ActivityKind.Task)] -public class ProcessVideo : Activity -{ - protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) - { - const int frameCount = 15; - Console.WriteLine("Begin processing..."); - for(var frame = 0; frame < frameCount; frame++) - { - await Task.Delay(500); - Console.WriteLine("Processing frame {0}", frame + 1); - } - Console.WriteLine("Finished processing"); - } -} \ No newline at end of file diff --git a/src/bundles/Elsa.WorkflowServer.Web/Jobs/IndexBlockchainJob.cs b/src/bundles/Elsa.WorkflowServer.Web/Jobs/IndexBlockchainJob.cs deleted file mode 100644 index be996b30a..000000000 --- a/src/bundles/Elsa.WorkflowServer.Web/Jobs/IndexBlockchainJob.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Elsa.Jobs.Abstractions; -using Elsa.Jobs.Activities.Features; -using Elsa.Jobs.Models; -using Elsa.Jobs.Services; - -namespace Elsa.WorkflowServer.Web.Jobs; - -/// -/// Jobs can be scheduled manually using , -/// but when enabling the , these jobs become available as activities too. -/// -public class IndexBlockchainJob : Job -{ - protected override async ValueTask ExecuteAsync(JobExecutionContext context) - { - Console.WriteLine("Indexing blockchain..."); - await Task.Delay(1000); - Console.WriteLine("Finished indexing blockchain."); - } -} \ No newline at end of file diff --git a/src/bundles/Elsa.WorkflowServer.Web/Models/Pizza.cs b/src/bundles/Elsa.WorkflowServer.Web/Models/Pizza.cs deleted file mode 100644 index c4ec7d760..000000000 --- a/src/bundles/Elsa.WorkflowServer.Web/Models/Pizza.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Elsa.WorkflowServer.Web.Models; - -public record Pizza(int Size, string Flavor); \ No newline at end of file diff --git a/src/bundles/Elsa.WorkflowServer.Web/Models/User.cs b/src/bundles/Elsa.WorkflowServer.Web/Models/User.cs deleted file mode 100644 index 3baccd644..000000000 --- a/src/bundles/Elsa.WorkflowServer.Web/Models/User.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.WorkflowServer.Web.Models; - -/// -/// A user class for security demo purposes. -/// -public record User(string FullName); \ No newline at end of file diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index b9f9453ac..4a9c145bd 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -1,15 +1,10 @@ using Elsa.EntityFrameworkCore.Extensions; using Elsa.Extensions; -using Elsa.Identity; using Elsa.Identity.Options; -using Elsa.Jobs.Activities.Services; using Elsa.EntityFrameworkCore.Modules.ActivityDefinitions; using Elsa.EntityFrameworkCore.Modules.Labels; using Elsa.EntityFrameworkCore.Modules.Management; using Elsa.EntityFrameworkCore.Modules.Runtime; -using Elsa.Requirements; -using Elsa.WorkflowServer.Web.Jobs; -using Microsoft.AspNetCore.Authorization; using Microsoft.Data.Sqlite; using Proto.Persistence.Sqlite; @@ -17,7 +12,6 @@ var builder = WebApplication.CreateBuilder(args); var services = builder.Services; var configuration = builder.Configuration; var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!; -var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!; var identityOptions = new IdentityOptions(); var identityTokenOptions = new IdentityTokenOptions(); var identitySection = configuration.GetSection("Identity"); @@ -43,13 +37,12 @@ services runtime.UseAsyncWorkflowStateExporter(); runtime.UseMassTransitDispatcher(); }) - .UseMassTransit(massTransit => massTransit.UseRabbitMq(rabbitMqConnectionString)) .UseLabels(labels => labels.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) .UseActivityDefinitions(feature => feature.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) .UseJobs(jobs => jobs.ConfigureOptions = options => options.WorkerCount = 10) .UseJobActivities() .UseScheduling() - .UseWorkflowsApi() + .UseWorkflowsApi(api => api.AddFastEndpointsAssembly()) .UseJavaScript() .UseLiquid() .UseHttp() @@ -62,11 +55,6 @@ services.AddHttpContextAccessor(); // Configure middleware pipeline. var app = builder.Build(); -var serviceProvider = app.Services; - -// Register a dummy job for demo purposes. -var jobRegistry = serviceProvider.GetRequiredService(); -jobRegistry.Add(typeof(IndexBlockchainJob)); if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage(); diff --git a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json index a7e3aec5c..28cc21ee6 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json +++ b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json @@ -8,8 +8,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;", - "RabbitMq": "rabbitmq://guest:guest@localhost" + "Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;" }, "Identity": { "CreateDefaultAdmin": true, diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index cc835066a..224b11395 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -193,8 +193,6 @@ export namespace Components { interface ElsaMultiTextInput { "inputContext": ActivityInputContext; } - interface ElsaNewButton { - } interface ElsaNotificationsManager { "modalState": boolean; } @@ -382,10 +380,6 @@ export interface ElsaMonacoEditorCustomEvent extends CustomEvent { detail: T; target: HTMLElsaMonacoEditorElement; } -export interface ElsaNewButtonCustomEvent extends CustomEvent { - detail: T; - target: HTMLElsaNewButtonElement; -} export interface ElsaPagerCustomEvent extends CustomEvent { detail: T; target: HTMLElsaPagerElement; @@ -619,12 +613,6 @@ declare global { prototype: HTMLElsaMultiTextInputElement; new (): HTMLElsaMultiTextInputElement; }; - interface HTMLElsaNewButtonElement extends Components.ElsaNewButton, HTMLStencilElement { - } - var HTMLElsaNewButtonElement: { - prototype: HTMLElsaNewButtonElement; - new (): HTMLElsaNewButtonElement; - }; interface HTMLElsaNotificationsManagerElement extends Components.ElsaNotificationsManager, HTMLStencilElement { } var HTMLElsaNotificationsManagerElement: { @@ -830,7 +818,6 @@ declare global { "elsa-monaco-editor": HTMLElsaMonacoEditorElement; "elsa-multi-line-input": HTMLElsaMultiLineInputElement; "elsa-multi-text-input": HTMLElsaMultiTextInputElement; - "elsa-new-button": HTMLElsaNewButtonElement; "elsa-notifications-manager": HTMLElsaNotificationsManagerElement; "elsa-pager": HTMLElsaPagerElement; "elsa-panel": HTMLElsaPanelElement; @@ -1029,9 +1016,6 @@ declare namespace LocalJSX { interface ElsaMultiTextInput { "inputContext"?: ActivityInputContext; } - interface ElsaNewButton { - "onNewClicked"?: (event: ElsaNewButtonCustomEvent) => void; - } interface ElsaNotificationsManager { "modalState"?: boolean; } @@ -1189,7 +1173,6 @@ declare namespace LocalJSX { "elsa-monaco-editor": ElsaMonacoEditor; "elsa-multi-line-input": ElsaMultiLineInput; "elsa-multi-text-input": ElsaMultiTextInput; - "elsa-new-button": ElsaNewButton; "elsa-notifications-manager": ElsaNotificationsManager; "elsa-pager": ElsaPager; "elsa-panel": ElsaPanel; @@ -1255,7 +1238,6 @@ declare module "@stencil/core" { "elsa-monaco-editor": LocalJSX.ElsaMonacoEditor & JSXBase.HTMLAttributes; "elsa-multi-line-input": LocalJSX.ElsaMultiLineInput & JSXBase.HTMLAttributes; "elsa-multi-text-input": LocalJSX.ElsaMultiTextInput & JSXBase.HTMLAttributes; - "elsa-new-button": LocalJSX.ElsaNewButton & JSXBase.HTMLAttributes; "elsa-notifications-manager": LocalJSX.ElsaNotificationsManager & JSXBase.HTMLAttributes; "elsa-pager": LocalJSX.ElsaPager & JSXBase.HTMLAttributes; "elsa-panel": LocalJSX.ElsaPanel & JSXBase.HTMLAttributes; diff --git a/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs b/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs index 8bb472ebf..bf6e54679 100644 --- a/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs +++ b/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs @@ -1,3 +1,6 @@ +using System.Collections.Concurrent; +using System.Reflection; + // ReSharper disable once CheckNamespace namespace Elsa.Extensions; @@ -6,6 +9,21 @@ namespace Elsa.Extensions; /// public static class TypeExtensions { + private static readonly ConcurrentDictionary SimpleAssemblyQualifiedTypeNameCache = new(); + + /// + /// Gets the assembly-qualified name of the type, without any version info etc. + /// E.g. "System.String, System.Private.CoreLib" + /// + public static string GetSimpleAssemblyQualifiedName(this Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, GetSimpleAssemblyQualifiedNameInternal); + } + + private static string GetSimpleAssemblyQualifiedNameInternal(Type type) => $"{type.FullName}, {Assembly.GetAssembly(type)!.GetName().Name}"; + /// /// Returns the default value for the specified type. /// diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 52617e796..6ce447466 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -47,6 +47,7 @@ public static class ObjectConverter options.Converters.Add(new JsonStringEnumConverter()); var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType; if (value is JsonElement jsonNumber && jsonNumber.ValueKind == JsonValueKind.Number && underlyingTargetType == typeof(string)) return jsonNumber.ToString().ConvertTo(underlyingTargetType); @@ -55,9 +56,24 @@ public static class ObjectConverter { if (jsonObject.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string)) return jsonObject.GetString().ConvertTo(underlyingTargetType); - + return jsonObject.Deserialize(targetType, options); } + + if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive) + { + var stringValue = (string)value; + + try + { + if (stringValue.TrimStart().StartsWith("{")) + return JsonSerializer.Deserialize(stringValue, underlyingTargetType); + } + catch (Exception e) + { + throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", value, underlyingTargetType, e); + } + } if (targetType == typeof(object)) return value; @@ -65,8 +81,6 @@ public static class ObjectConverter if (underlyingTargetType.IsInstanceOfType(value)) return value; - var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType; - if (underlyingSourceType == underlyingTargetType) return value; @@ -94,20 +108,7 @@ public static class ObjectConverter return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int))); } - if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive) - { - var stringValue = (string)value; - - try - { - if (stringValue.TrimStart().StartsWith("{")) - return JsonSerializer.Deserialize(stringValue, underlyingTargetType); - } - catch (Exception e) - { - throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", value, underlyingTargetType, e); - } - } + if (value is string s && string.IsNullOrWhiteSpace(s)) return null; diff --git a/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs b/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs index edbe389a2..6aab05bdc 100644 --- a/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs +++ b/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs @@ -9,26 +9,51 @@ using Elsa.Workflows.Runtime.Entities; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; +/// +/// Adds extensions to . +/// public static class RouteTableExtensions { + private static readonly JsonSerializerOptions SerializerOptions; + + static RouteTableExtensions() + { + SerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + } + + /// + /// Adds routes from the specified set of triggers. + /// public static void AddRoutes(this IRouteTable routeTable, IEnumerable triggers) { - var paths = Filter(triggers).Select(Deserialize).Select(x => x.Path).ToList(); + var paths = Filter(triggers).Select(Deserialize).Select(x => x.Path).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); routeTable.AddRange(paths); } + /// + /// Adds routes from the specified set of bookmarks. + /// public static void AddRoutes(this IRouteTable routeTable, IEnumerable bookmarks) { var paths = Filter(bookmarks).Select(Deserialize).Select(x => x.Path).ToList(); routeTable.AddRange(paths); } + /// + /// Removes routes from the specified set of triggers. + /// public static void RemoveRoutes(this IRouteTable routeTable, IEnumerable triggers) { var paths = Filter(triggers).Select(Deserialize).Select(x => x.Path).ToList(); routeTable.RemoveRange(paths); } + /// + /// Removes routes from the specified set of bookmarks. + /// public static void RemoveRoutes(this IRouteTable routeTable, IEnumerable bookmarks) { var paths = Filter(bookmarks).Select(Deserialize).Select(x => x.Path).ToList(); @@ -39,5 +64,5 @@ public static class RouteTableExtensions private static IEnumerable Filter(IEnumerable triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName()); private static HttpEndpointBookmarkPayload Deserialize(StoredTrigger trigger) => Deserialize(trigger.Data!); private static HttpEndpointBookmarkPayload Deserialize(Bookmark bookmark) => Deserialize(bookmark.Data!); - private static HttpEndpointBookmarkPayload Deserialize(string model) => JsonSerializer.Deserialize(model)!; + private static HttpEndpointBookmarkPayload Deserialize(string model) => JsonSerializer.Deserialize(model, SerializerOptions)!; } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Implementations/RouteTable.cs b/src/modules/Elsa.Http/Implementations/RouteTable.cs index 0cfc7ba65..c830280b3 100644 --- a/src/modules/Elsa.Http/Implementations/RouteTable.cs +++ b/src/modules/Elsa.Http/Implementations/RouteTable.cs @@ -5,26 +5,38 @@ using Microsoft.Extensions.Caching.Memory; namespace Elsa.Http.Implementations; +/// public class RouteTable : IRouteTable { private static readonly object Key = new(); private readonly IMemoryCache _cache; + /// + /// Constructor. + /// + /// public RouteTable(IMemoryCache cache) => _cache = cache; - private ConcurrentDictionary Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary()); + private ConcurrentDictionary Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary())!; + + /// public void Add(string path) => Routes.TryAdd(path, path); + + /// public void Remove(string path) => Routes.TryRemove(path, out _); + /// public void AddRange(IEnumerable paths) { foreach (var path in paths) Add(path); } + /// public void RemoveRange(IEnumerable paths) { foreach (var path in paths) Remove(path); } + /// public IEnumerator GetEnumerator() => Routes.Values.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Parsers/JsonElementHttpResponseContentReader.cs b/src/modules/Elsa.Http/Parsers/JsonElementHttpResponseContentReader.cs index df5160594..6f91694e5 100644 --- a/src/modules/Elsa.Http/Parsers/JsonElementHttpResponseContentReader.cs +++ b/src/modules/Elsa.Http/Parsers/JsonElementHttpResponseContentReader.cs @@ -9,7 +9,7 @@ public class JsonElementHttpResponseContentReader : IHttpResponseContentReader public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase); public async Task ReadAsync(HttpResponseMessage response, object context, CancellationToken cancellationToken) { - var json = (await response.Content.ReadAsStringAsync()).Trim(); + var json = (await response.Content.ReadAsStringAsync(cancellationToken)).Trim(); return JsonDocument.Parse(json).RootElement; } } \ No newline at end of file diff --git a/src/modules/Elsa.Jobs.Activities/Attributes/JobAttribute.cs b/src/modules/Elsa.Jobs.Activities/Attributes/JobAttribute.cs index 1766a82d3..21aede8de 100644 --- a/src/modules/Elsa.Jobs.Activities/Attributes/JobAttribute.cs +++ b/src/modules/Elsa.Jobs.Activities/Attributes/JobAttribute.cs @@ -3,6 +3,7 @@ namespace Elsa.Jobs.Activities.Attributes; [AttributeUsage(AttributeTargets.Class)] public class JobAttribute : Attribute { + /// public JobAttribute(string @namespace, string? category, string? description = default) { Namespace = @namespace; @@ -10,6 +11,7 @@ public class JobAttribute : Attribute Category = category; } + /// public JobAttribute(string @namespace, string? description = default) { Namespace = @namespace; @@ -17,6 +19,7 @@ public class JobAttribute : Attribute Category = @namespace; } + /// public JobAttribute(string @namespace, string? activityType, string? description = default, string? category = default) { Namespace = @namespace; diff --git a/src/modules/Elsa.Jobs.Activities/Implementations/JobActivityProvider.cs b/src/modules/Elsa.Jobs.Activities/Implementations/JobActivityProvider.cs index 9598d0ba0..fa6807448 100644 --- a/src/modules/Elsa.Jobs.Activities/Implementations/JobActivityProvider.cs +++ b/src/modules/Elsa.Jobs.Activities/Implementations/JobActivityProvider.cs @@ -30,6 +30,7 @@ public class JobActivityProvider : IActivityProvider _jobRegistry = jobRegistry; } + /// public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) { var jobTypes = _jobRegistry.List(); diff --git a/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs new file mode 100644 index 000000000..b1c6a6d0c --- /dev/null +++ b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs @@ -0,0 +1,58 @@ +using System.ComponentModel; +using System.Text.Json.Serialization; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.MassTransit.Implementations; +using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Models; + +namespace Elsa.MassTransit.Activities; + +/// +/// A generic activity that waits for a message of a given type to be received. Used by the . +/// +[Browsable(false)] +public class MessageReceived : Trigger +{ + internal const string InputKey = "Message"; + + /// + [JsonConstructor] + public MessageReceived() + { + } + + /// + /// The message type to receive. + /// + public Type MessageType { get; set; } = default!; + + /// + protected override object GetTriggerPayload(TriggerIndexingContext context) => GetBookmarkPayload(context.ExpressionExecutionContext); + + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark. + if (!context.TryGetInput(InputKey, out var message)) + { + // Create bookmarks for when we receive the expected HTTP request. + context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext)); + return; + } + + // Provide the received message as output. + context.Set(Result, message); + + // Complete. + await context.CompleteActivityAsync(); + } + + private object GetBookmarkPayload(ExpressionExecutionContext context) + { + // Generate bookmark data for message type. + return new MessageReceivedBookmarkPayload(MessageType); + } +} + +internal record MessageReceivedBookmarkPayload(Type MessageType); \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Activities/PublishMessage.cs b/src/modules/Elsa.MassTransit/Activities/PublishMessage.cs new file mode 100644 index 000000000..d549d996d --- /dev/null +++ b/src/modules/Elsa.MassTransit/Activities/PublishMessage.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using System.Text.Json.Serialization; +using Elsa.Expressions.Helpers; +using Elsa.Extensions; +using Elsa.MassTransit.Implementations; +using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Models; +using MassTransit; +using MassTransit.Middleware; + +namespace Elsa.MassTransit.Activities; + +/// +/// A generic activity that publishes a message of a given type. Used by the . +/// +[Browsable(false)] +public class PublishMessage : Activity +{ + /// + [JsonConstructor] + public PublishMessage() + { + } + + /// + /// The message type to publish. + /// + public Type MessageType { get; set; } = default!; + + /// + /// The message to send. Must be a concrete implementation of the configured . + /// + [Input(Description = "The message to send. Must be a concrete implementation of the configured message type.")] + public Input Message { get; set; } = default!; + + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var bus = context.GetRequiredService(); + var message = Message.Get(context).ConvertTo(MessageType)!; + await bus.Publish(message, context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Consumers/WorkflowMessageConsumer.cs b/src/modules/Elsa.MassTransit/Consumers/WorkflowMessageConsumer.cs new file mode 100644 index 000000000..f4d56d75c --- /dev/null +++ b/src/modules/Elsa.MassTransit/Consumers/WorkflowMessageConsumer.cs @@ -0,0 +1,37 @@ +using Elsa.MassTransit.Activities; +using Elsa.Workflows.Core.Helpers; +using Elsa.Workflows.Runtime.Models; +using Elsa.Workflows.Runtime.Services; +using MassTransit; + +namespace Elsa.MassTransit.Consumers; + +/// +/// A consumer of various dispatch message types to asynchronously execute workflows. +/// +public class WorkflowMessageConsumer : IConsumer where T : class +{ + private readonly IWorkflowDispatcher _workflowRuntime; + + /// + /// Constructor. + /// + public WorkflowMessageConsumer(IWorkflowDispatcher workflowRuntime) + { + _workflowRuntime = workflowRuntime; + } + + /// + public async Task Consume(ConsumeContext context) + { + var cancellationToken = context.CancellationToken; + var messageType = typeof(T); + var message = context.Message; + var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(messageType); + var bookmark = new MessageReceivedBookmarkPayload(messageType); + var correlationId = context.CorrelationId?.ToString(); + var input = new Dictionary { [MessageReceived.InputKey] = message }; + var request = new DispatchTriggerWorkflowsRequest(activityTypeName, bookmark, correlationId, input); + await _workflowRuntime.DispatchAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Extensions/MassTransitFeatureExtensions.cs b/src/modules/Elsa.MassTransit/Extensions/MassTransitFeatureExtensions.cs index edcf97d0d..d6c681b50 100644 --- a/src/modules/Elsa.MassTransit/Extensions/MassTransitFeatureExtensions.cs +++ b/src/modules/Elsa.MassTransit/Extensions/MassTransitFeatureExtensions.cs @@ -1,5 +1,6 @@ using Elsa.Features.Services; using Elsa.MassTransit.Features; +using Elsa.MassTransit.Implementations; using MassTransit; // ReSharper disable once CheckNamespace @@ -11,6 +12,7 @@ namespace Elsa.Extensions; public static class MassTransitFeatureExtensions { private static readonly object ServiceBusConsumerTypesKey = new(); + private static readonly object MessageTypesKey = new(); /// /// Registers the specified type for MassTransit service bus consumer discovery. @@ -26,9 +28,29 @@ public static class MassTransitFeatureExtensions types.Add(type); return feature; } + + /// + /// Registers a message type which is to be used by the to dynamically provide activities to send and receive these messages. + /// + public static MassTransitFeature AddMessageType(this MassTransitFeature feature) where T : class => feature.AddMessageType(typeof(T)); /// - /// Returns all collected types for discovery of service bus consumers. + /// Registers a message type which is to be used by the to dynamically provide activities to send and receive these messages. + /// + public static MassTransitFeature AddMessageType(this MassTransitFeature feature, Type type) + { + var types = feature.Module.Properties.GetOrAdd(MessageTypesKey, () => new HashSet()); + types.Add(type); + return feature; + } + + /// + /// Returns all collected consumer types. /// internal static IEnumerable GetConsumers(this MassTransitFeature feature) => feature.Module.Properties.GetOrAdd(ServiceBusConsumerTypesKey, () => new HashSet()); + + /// + /// Returns all collected message types. + /// + internal static IEnumerable GetMessages(this MassTransitFeature feature) => feature.Module.Properties.GetOrAdd(MessageTypesKey, () => new HashSet()); } \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs b/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs index 9204cac87..757fc51a6 100644 --- a/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs +++ b/src/modules/Elsa.MassTransit/Features/MassTransitFeature.cs @@ -1,10 +1,20 @@ +using System.ComponentModel; +using System.Reflection; using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; +using Elsa.MassTransit.Consumers; +using Elsa.MassTransit.Implementations; +using Elsa.MassTransit.Options; +using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Helpers; +using Elsa.Workflows.Core.Serialization; using Elsa.Workflows.Core.Serialization.Converters; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Management.Options; +using Humanizer; using MassTransit; using MassTransit.Serialization; -using MassTransit.Serialization.JsonConverters; using Microsoft.Extensions.DependencyInjection; namespace Elsa.MassTransit.Features; @@ -22,7 +32,7 @@ public class MassTransitFeature : FeatureBase /// /// A delegate that can be set to configure MassTransit's . /// - public Action? BusConfigurator { get; set; } + public Action? BusConfigurator { get; set; } /// public override void Configure() @@ -31,12 +41,36 @@ public class MassTransitFeature : FeatureBase { configure.UsingInMemory((context, configurator) => { configurator.ConfigureEndpoints(context); }); }; + } /// public override void Apply() { + var messageTypes = this.GetMessages(); + + Services.AddActivityProvider(); AddMassTransit(BusConfigurator); + + // Add collected message types to options. + Services.Configure(options => options.MessageTypes = new HashSet(messageTypes)); + + // Add collected message types as available variable types. + Services.Configure(options => + { + foreach (var messageType in messageTypes) + { + var activityAttr = messageType.GetCustomAttribute(); + var categoryAttr = messageType.GetCustomAttribute(); + var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit"; + var descriptionAttr = messageType.GetCustomAttribute(); + var description = descriptionAttr?.Description ?? activityAttr?.Description; + options.VariableDescriptors.Add(new VariableDescriptor(messageType, category, description)); + } + }); + + // Configure message serializer. + SystemTextJsonMessageSerializer.Options.Converters.Add(new TypeJsonConverter(new WellKnownTypeRegistry())); } /// @@ -44,7 +78,12 @@ public class MassTransitFeature : FeatureBase /// private void AddMassTransit(Action? config) { - var consumerTypes = this.GetConsumers().ToArray(); + // For each message type, create a concrete WorkflowMessageConsumer. + var workflowMessageConsumerType = typeof(WorkflowMessageConsumer<>); + var workflowMessageConsumers = this.GetMessages().Select(x => workflowMessageConsumerType.MakeGenericType(x)); + + // Concatenate the manually registered consumers with the workflow message consumers. + var consumerTypes = this.GetConsumers().Concat(workflowMessageConsumers).ToArray(); Services.AddMassTransit(bus => { diff --git a/src/modules/Elsa.MassTransit/Implementations/MassTransitActivityTypeProvider.cs b/src/modules/Elsa.MassTransit/Implementations/MassTransitActivityTypeProvider.cs new file mode 100644 index 000000000..e0f61b711 --- /dev/null +++ b/src/modules/Elsa.MassTransit/Implementations/MassTransitActivityTypeProvider.cs @@ -0,0 +1,137 @@ +using System.ComponentModel; +using System.Reflection; +using Elsa.Extensions; +using Elsa.MassTransit.Activities; +using Elsa.MassTransit.Options; +using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Helpers; +using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Management.Services; +using Humanizer; +using Microsoft.Extensions.Options; + +namespace Elsa.MassTransit.Implementations; + +/// +/// Provides activities to the system from the configured MassTransit message types. +/// +public class MassTransitActivityTypeProvider : IActivityProvider +{ + private readonly IActivityFactory _activityFactory; + private readonly MassTransitActivityOptions _options; + + /// + /// Constructor. + /// + public MassTransitActivityTypeProvider(IActivityFactory activityFactory, IOptions options) + { + _activityFactory = activityFactory; + _options = options.Value; + } + + /// + public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var messageTypes = _options.MessageTypes; + var descriptors = CreateDescriptors(messageTypes).ToList(); + return new(descriptors); + } + + private IEnumerable CreateDescriptors(IEnumerable messageTypes) + { + foreach (var messageType in messageTypes) + { + yield return CreateMessageReceivedDescriptor(messageType); + + if(messageType.IsClass) + yield return CreatePublishMessageDescriptor(messageType); + } + } + + private ActivityDescriptor CreateMessageReceivedDescriptor(Type messageType) + { + var activityAttr = messageType.GetCustomAttribute(); + var typeName = activityAttr?.Type ?? messageType.Name; + var fullTypeName = ActivityTypeNameHelper.GenerateTypeName(messageType); + var displayNameAttr = messageType.GetCustomAttribute(); + var displayName = displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title); + var categoryAttr = messageType.GetCustomAttribute(); + var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit"; + var descriptionAttr = messageType.GetCustomAttribute(); + var description = descriptionAttr?.Description ?? activityAttr?.Description; + + return new() + { + TypeName = fullTypeName, + Version = 1, + DisplayName = displayName, + Description = description, + Category = category, + Kind = ActivityKind.Trigger, + IsBrowsable = true, + ActivityType = typeof(MessageReceived), + Outputs = + { + new OutputDescriptor + { + Description = "The received message", + DisplayName = "Received Message", + Name = nameof(MessageReceived.Result), + Type = typeof(object) + } + }, + Constructor = context => + { + var activity = _activityFactory.Create(context); + activity.Type = fullTypeName; + activity.MessageType = messageType; + return activity; + } + }; + } + + private ActivityDescriptor CreatePublishMessageDescriptor(Type messageType) + { + var activityAttr = messageType.GetCustomAttribute(); + var typeName = activityAttr?.Type ?? messageType.Name; + var ns = ActivityTypeNameHelper.GenerateNamespace(messageType); + var fullTypeName = ns + ".Publish" + typeName; + var displayNameAttr = messageType.GetCustomAttribute(); + var displayName = "Publish " + (displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title)); + var categoryAttr = messageType.GetCustomAttribute(); + var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit"; + var descriptionAttr = messageType.GetCustomAttribute(); + var description = descriptionAttr?.Description ?? activityAttr?.Description; + + return new() + { + TypeName = fullTypeName, + Version = 1, + DisplayName = displayName, + Description = description, + Category = category, + Kind = ActivityKind.Action, + IsBrowsable = true, + ActivityType = typeof(PublishMessage), + Inputs = + { + new InputDescriptor + { + Description = "The message to publish.", + UIHint = InputUIHints.MultiLine, + DisplayName = "Message", + Type = typeof(Input), + Name = nameof(PublishMessage.Message) + } + }, + Constructor = context => + { + var activity = _activityFactory.Create(context); + activity.Type = fullTypeName; + activity.MessageType = messageType; + return activity; + } + }; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Options/MassTransitActivityOptions.cs b/src/modules/Elsa.MassTransit/Options/MassTransitActivityOptions.cs new file mode 100644 index 000000000..73f945eb7 --- /dev/null +++ b/src/modules/Elsa.MassTransit/Options/MassTransitActivityOptions.cs @@ -0,0 +1,12 @@ +namespace Elsa.MassTransit.Options; + +/// +/// Provides settings to the RabbitMQ broker for MassTransit. +/// +public class MassTransitActivityOptions +{ + /// + /// A set of message types that can be sent and received in the form of workflow activities. + /// + public ISet MessageTypes { get; set; } = new HashSet(); +} \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Options/RabbitMqOptions.cs b/src/modules/Elsa.MassTransit/Options/RabbitMqOptions.cs index 7e60f012e..2f8c3ca6c 100644 --- a/src/modules/Elsa.MassTransit/Options/RabbitMqOptions.cs +++ b/src/modules/Elsa.MassTransit/Options/RabbitMqOptions.cs @@ -5,7 +5,7 @@ namespace Elsa.MassTransit.Options; /// public class RabbitMqOptions { - public string Host { get; set; } - public string Username { get; set; } - public string Password { get; set; } + public string Host { get; set; } = default!; + public string Username { get; set; } = default!; + public string Password { get; set; } = default!; } \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Handlers/UpdateRunningWorkflowsHandler.cs b/src/modules/Elsa.ProtoActor/Handlers/UpdateRunningWorkflowsHandler.cs index 255961b07..cd17e197e 100644 --- a/src/modules/Elsa.ProtoActor/Handlers/UpdateRunningWorkflowsHandler.cs +++ b/src/modules/Elsa.ProtoActor/Handlers/UpdateRunningWorkflowsHandler.cs @@ -1,5 +1,6 @@ using Elsa.Extensions; using Elsa.Mediator.Services; +using Elsa.ProtoActor.Extensions; using Elsa.ProtoActor.Grains; using Elsa.ProtoActor.Protos; using Elsa.Workflows.Core.Notifications; @@ -32,7 +33,7 @@ internal class UpdateRunningWorkflowsHandler : INotificationHandler(this WorkflowsApiFeature feature) + { + feature.Module.AddFastEndpointsAssembly(); + return feature; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs index 07e7d0c33..3d4edc429 100644 --- a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs +++ b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs @@ -24,7 +24,6 @@ public class WorkflowsApiFeature : FeatureBase /// public override void Configure() { - Module.AddFastEndpointsAssembly(GetType()); } diff --git a/src/modules/Elsa.Workflows.Core/Extensions/TypeExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/TypeExtensions.cs deleted file mode 100644 index fd61a43a8..000000000 --- a/src/modules/Elsa.Workflows.Core/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Concurrent; -using System.Reflection; - -// ReSharper disable once CheckNamespace -namespace Elsa.Extensions; - -public static class TypeExtensions -{ - private static readonly ConcurrentDictionary SimpleAssemblyQualifiedTypeNameCache = new(); - - /// - /// Gets the assembly-qualified name of the type, without any version info etc. - /// E.g. "System.String, System.Private.CoreLib" - /// - public static string GetSimpleAssemblyQualifiedName(this Type type) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - - return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, GetSimpleAssemblyQualifiedNameInternal); - } - - private static string GetSimpleAssemblyQualifiedNameInternal(Type type) => $"{type.FullName}, {Assembly.GetAssembly(type)!.GetName().Name}"; -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index 3c26ec627..dd907b59d 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -111,7 +111,7 @@ public class WorkflowsFeature : FeatureBase .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton(sp => ActivatorUtilities.CreateInstance(sp)) .AddTransient() .AddSingleton(typeof(Func), sp => () => sp.GetRequiredService()) .AddSingleton() diff --git a/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs b/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs index 5300ac4bb..9edfb9fe8 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Elsa.Workflows.Core.Serialization.Converters; using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Implementations; @@ -7,13 +8,15 @@ public class BookmarkPayloadSerializer : IBookmarkPayloadSerializer { private readonly JsonSerializerOptions _settings; - public BookmarkPayloadSerializer() + public BookmarkPayloadSerializer(IWellKnownTypeRegistry wellKnownTypeRegistry) { _settings = new JsonSerializerOptions { // Enables serialization of ValueTuples, which use fields instead of properties. IncludeFields = true }; + + _settings.Converters.Add(new TypeJsonConverter(wellKnownTypeRegistry)); } public T Deserialize(string json) where T : notnull => JsonSerializer.Deserialize(json, _settings)!; diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs index 2459babc1..3c9dad2c5 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicConverter.cs @@ -14,7 +14,7 @@ public class PolymorphicConverter : JsonConverter public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { var typeName = value.GetType().GetSimpleAssemblyQualifiedName(); - var wrappedValue = JsonSerializer.SerializeToNode(value)!; + var wrappedValue = JsonSerializer.SerializeToNode(value, options)!; wrappedValue["$type"] = typeName; wrappedValue.WriteTo(writer); } @@ -25,7 +25,7 @@ public class PolymorphicConverter : JsonConverter var element = JsonElement.ParseValue(ref reader); var typeName = element.GetProperty("$type").GetString()!; var type = Type.GetType(typeName)!; - var value = element.Deserialize(type); + var value = element.Deserialize(type, options); return value!; } diff --git a/src/modules/Elsa.Workflows.Runtime/Implementations/TriggerIndexer.cs b/src/modules/Elsa.Workflows.Runtime/Implementations/TriggerIndexer.cs index c8849038e..9dc5af734 100644 --- a/src/modules/Elsa.Workflows.Runtime/Implementations/TriggerIndexer.cs +++ b/src/modules/Elsa.Workflows.Runtime/Implementations/TriggerIndexer.cs @@ -5,6 +5,7 @@ using Elsa.Expressions.Services; using Elsa.Extensions; using Elsa.Workflows.Core.Helpers; using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Core.Serialization; using Elsa.Workflows.Core.Services; using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Runtime.Comparers; @@ -30,6 +31,7 @@ public class TriggerIndexer : ITriggerIndexer private readonly IServiceProvider _serviceProvider; private readonly IBookmarkHasher _hasher; private readonly ILogger _logger; + private readonly JsonSerializerOptions _serializerOptions; /// /// Constructor. @@ -43,6 +45,7 @@ public class TriggerIndexer : ITriggerIndexer IEventPublisher eventPublisher, IServiceProvider serviceProvider, IBookmarkHasher hasher, + SerializerOptionsProvider serializerOptionsProvider, ILogger logger) { _activityWalker = activityWalker; @@ -54,6 +57,7 @@ public class TriggerIndexer : ITriggerIndexer _hasher = hasher; _logger = logger; _workflowDefinitionService = workflowDefinitionService; + _serializerOptions = serializerOptionsProvider.CreateDefaultOptions(); } /// @@ -152,7 +156,7 @@ public class TriggerIndexer : ITriggerIndexer Name = triggerTypeName, ActivityId = trigger.Id, Hash = _hasher.Hash(triggerTypeName, x), - Data = JsonSerializer.Serialize(x) + Data = JsonSerializer.Serialize(x, _serializerOptions) }); return triggers.ToList(); diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Elsa.Samples.MassTransitActivities.csproj b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Elsa.Samples.MassTransitActivities.csproj new file mode 100644 index 000000000..3681734d2 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Elsa.Samples.MassTransitActivities.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Endpoints/Orders/Create/Endpoint.cs b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Endpoints/Orders/Create/Endpoint.cs new file mode 100644 index 000000000..f207e22e8 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Endpoints/Orders/Create/Endpoint.cs @@ -0,0 +1,33 @@ +using Elsa.Samples.MassTransitActivities.Messages; +using FastEndpoints; +using MassTransit; + +namespace Elsa.Samples.MassTransitActivities.Endpoints.Orders.Create; + +public class Create : EndpointWithoutRequest +{ + private readonly IBus _bus; + + public Create(IBus bus) + { + _bus = bus; + } + + public override void Configure() + { + Post("orders"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken cancellationToken) + { + await _bus.Publish(new + { + Id = Guid.NewGuid().ToString("N"), + CustomerId = "1", + Product = "Pizza", + Quantity = 5, + Total = 62.5 + }, cancellationToken); + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Messages/OrderCompleted.cs b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Messages/OrderCompleted.cs new file mode 100644 index 000000000..81dfc11af --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Messages/OrderCompleted.cs @@ -0,0 +1,4 @@ +namespace Elsa.Samples.MassTransitActivities.Messages; + +// ReSharper disable once InconsistentNaming +public record OrderCompleted(string OrderId); \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Messages/OrderCreated.cs b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Messages/OrderCreated.cs new file mode 100644 index 000000000..3ef16cb7b --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Messages/OrderCreated.cs @@ -0,0 +1,11 @@ +namespace Elsa.Samples.MassTransitActivities.Messages; + +// ReSharper disable once InconsistentNaming +public record OrderCreated( + + string Id, + string CustomerId, + string Product, + int Quantity, + decimal Total +); \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Pages/Index.cshtml b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Pages/Index.cshtml new file mode 100644 index 000000000..d76ef7cc9 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Pages/Index.cshtml @@ -0,0 +1,24 @@ +@page +@using Elsa.Workflows.Designer +@using Microsoft.AspNetCore.Mvc.TagHelpers +@{ + var serverUrl = Url.Content("elsa/api"); +} + + + + + + + Elsa Workflows 3.0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Pages/_ViewImports.cshtml b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Pages/_ViewImports.cshtml new file mode 100644 index 000000000..47975fca7 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Pages/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@namespace Elsa.Samples.MassTransitActivities.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Program.cs b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Program.cs new file mode 100644 index 000000000..a4b8cbe3a --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Program.cs @@ -0,0 +1,66 @@ +using Elsa.EntityFrameworkCore.Extensions; +using Elsa.EntityFrameworkCore.Modules.Management; +using Elsa.EntityFrameworkCore.Modules.Runtime; +using Elsa.Extensions; +using Elsa.Samples.MassTransitActivities.Messages; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; +var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!; + +// Add services to the container. +builder.Services.AddElsa(elsa => +{ + // Configure management feature to use EF Core. + elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite())); + + // Configure runtime feature to use EF Core. + elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite())); + + // Expose API endpoints. + elsa.UseWorkflowsApi(); + + // Add services for HTTP activities and workflow middleware. + elsa.UseHttp(); + + // Use JavaScript and Liquid. + elsa.UseJavaScript(); + elsa.UseLiquid(); + + // Configure identity so that we can create a default admin user. + elsa.UseIdentity(identity => + { + identity.IdentityOptions.CreateDefaultAdmin = builder.Environment.IsDevelopment(); + identity.TokenOptions.SigningKey = "secret-token-signing-key"; + identity.TokenOptions.Lifetime = TimeSpan.FromDays(1); + }); + + // Use default authentication (JWT). + elsa.UseDefaultAuthentication(); + + // Configure MassTransit. + elsa.UseMassTransit(massTransit => + { + massTransit.UseRabbitMq(rabbitMqConnectionString); + massTransit.AddMessageType(); + massTransit.AddMessageType(); + }); +}); + +builder.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin())); + +// Add Razor pages. +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseHttpsRedirection(); +app.UseCors(); +app.UseStaticFiles(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseWorkflowsApi(); +app.UseWorkflows(); +app.MapRazorPages(); +app.Run(); \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Properties/launchSettings.json b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Properties/launchSettings.json new file mode 100644 index 000000000..1753d9988 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38703", + "sslPort": 44342 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5207", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7299;http://localhost:5207", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/samples/aspnet/Elsa.Samples.MassTransitActivities/appsettings.json b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/appsettings.json new file mode 100644 index 000000000..1549c033f --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.MassTransitActivities/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "RabbitMq": "rabbitmq://guest:guest@localhost" + } +} diff --git a/src/samples/aspnet/Elsa.Samples.WorkflowServerAndDesigner/Elsa.Samples.WorkflowServerAndDesigner.csproj b/src/samples/aspnet/Elsa.Samples.WorkflowServerAndDesigner/Elsa.Samples.WorkflowServerAndDesigner.csproj index 23936141d..79d708c0f 100644 --- a/src/samples/aspnet/Elsa.Samples.WorkflowServerAndDesigner/Elsa.Samples.WorkflowServerAndDesigner.csproj +++ b/src/samples/aspnet/Elsa.Samples.WorkflowServerAndDesigner/Elsa.Samples.WorkflowServerAndDesigner.csproj @@ -15,9 +15,4 @@ - - - - -