From a397b6799e10582cc27de7644e3bc1518d53088d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 4 Nov 2024 11:59:47 +0100 Subject: [PATCH] Add new sample project for ASP.NET Hangfire Integration Introduced a new sample project to demonstrate ASP.NET integration with Hangfire, including setup configurations in `appsettings.json`, program initialization, and a background job activity. This will serve as a reference implementation for integrating Elsa with Hangfire in ASP.NET applications. --- Elsa.sln | 9 +++ .../Activities/ExecuteSomeJob.cs | 28 ++++++++ ....Samples.AspNet.HangfireIntegration.csproj | 15 ++++ .../Jobs/SomeJob.cs | 34 +++++++++ .../Models/SomeJobResult.cs | 6 ++ .../Program.cs | 69 +++++++++++++++++++ .../Properties/launchSettings.json | 37 ++++++++++ .../README.md | 6 ++ .../Stimuli/SomeJobStimulus.cs | 3 + .../appsettings.json | 44 ++++++++++++ 10 files changed, 251 insertions(+) create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/Activities/ExecuteSomeJob.cs create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/Elsa.Samples.AspNet.HangfireIntegration.csproj create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/Jobs/SomeJob.cs create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/Models/SomeJobResult.cs create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/Program.cs create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/Properties/launchSettings.json create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/README.md create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/Stimuli/SomeJobStimulus.cs create mode 100644 samples/Elsa.Samples.AspNet.HangfireIntegration/appsettings.json diff --git a/Elsa.sln b/Elsa.sln index b683f573c..0d20a07dc 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -383,6 +383,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Tenants.AspNetCore", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql\Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql.csproj", "{2B939AC9-03A4-479E-AA0D-CB58F4A7F480}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{0350351C-E347-41D8-8E9C-ECF90653418B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.HangfireIntegration", "samples\Elsa.Samples.AspNet.HangfireIntegration\Elsa.Samples.AspNet.HangfireIntegration.csproj", "{FAE7B132-8AD3-44C0-B45B-1B2BA64B1DB4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -815,6 +819,10 @@ Global {2B939AC9-03A4-479E-AA0D-CB58F4A7F480}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B939AC9-03A4-479E-AA0D-CB58F4A7F480}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B939AC9-03A4-479E-AA0D-CB58F4A7F480}.Release|Any CPU.Build.0 = Release|Any CPU + {FAE7B132-8AD3-44C0-B45B-1B2BA64B1DB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAE7B132-8AD3-44C0-B45B-1B2BA64B1DB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAE7B132-8AD3-44C0-B45B-1B2BA64B1DB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAE7B132-8AD3-44C0-B45B-1B2BA64B1DB4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -959,6 +967,7 @@ Global {D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912} {2B939AC9-03A4-479E-AA0D-CB58F4A7F480} = {50470834-4CD8-479A-8B58-0A1869BA5D37} {2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {FAE7B132-8AD3-44C0-B45B-1B2BA64B1DB4} = {0350351C-E347-41D8-8E9C-ECF90653418B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/Activities/ExecuteSomeJob.cs b/samples/Elsa.Samples.AspNet.HangfireIntegration/Activities/ExecuteSomeJob.cs new file mode 100644 index 000000000..8119b624a --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/Activities/ExecuteSomeJob.cs @@ -0,0 +1,28 @@ +using Elsa.Extensions; +using Elsa.Samples.AspNet.HangfireIntegration.Jobs; +using Elsa.Samples.AspNet.HangfireIntegration.Models; +using Elsa.Samples.AspNet.HangfireIntegration.Stimuli; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Hangfire; + +namespace Elsa.Samples.AspNet.HangfireIntegration.Activities; + +[Activity("Samples", "Samples", "Enqueues a background job and resumes the workflow when the job is done.")] +public class ExecuteSomeJob : Activity +{ + protected override void Execute(ActivityExecutionContext context) + { + var stimulus = new SomeJobStimulus(); + var bookmark = context.CreateBookmark(stimulus, OnResume); + var backgroundJobClient = context.GetRequiredService(); + backgroundJobClient.Enqueue(x => x.RunAsync(bookmark.Id, default)); + } + + private async ValueTask OnResume(ActivityExecutionContext context) + { + var jobResult = context.GetWorkflowInput("JobResult"); + Result.Set(context, jobResult); + await context.CompleteActivityAsync(); + } +} \ No newline at end of file diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/Elsa.Samples.AspNet.HangfireIntegration.csproj b/samples/Elsa.Samples.AspNet.HangfireIntegration/Elsa.Samples.AspNet.HangfireIntegration.csproj new file mode 100644 index 000000000..834d0e79c --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/Elsa.Samples.AspNet.HangfireIntegration.csproj @@ -0,0 +1,15 @@ + + + net8.0 + latest + enable + enable + + + + + + + + + \ No newline at end of file diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/Jobs/SomeJob.cs b/samples/Elsa.Samples.AspNet.HangfireIntegration/Jobs/SomeJob.cs new file mode 100644 index 000000000..ef2ad7b99 --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/Jobs/SomeJob.cs @@ -0,0 +1,34 @@ +using Elsa.Samples.AspNet.HangfireIntegration.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.Runtime.Options; + +namespace Elsa.Samples.AspNet.HangfireIntegration.Jobs; + +public class SomeJob(IBookmarkResumer bookmarkResumer) +{ + public async Task RunAsync(string bookmarkId, CancellationToken cancellationToken) + { + Console.Write("Executing some job..."); + await Task.Delay(5000, cancellationToken); + Console.WriteLine("Done!"); + + await ResumeBookmarkAsync(bookmarkId, cancellationToken); + } + + private async Task ResumeBookmarkAsync(string bookmarkId, CancellationToken cancellationToken) + { + var resumeOptions = new ResumeBookmarkOptions + { + Input = new Dictionary + { + ["JobResult"] = new SomeJobResult { Message = "Hello from SomeJob!" } + } + }; + var filter = new BookmarkFilter + { + BookmarkId = bookmarkId + }; + await bookmarkResumer.ResumeAsync(filter, resumeOptions, cancellationToken); + } +} \ No newline at end of file diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/Models/SomeJobResult.cs b/samples/Elsa.Samples.AspNet.HangfireIntegration/Models/SomeJobResult.cs new file mode 100644 index 000000000..309f42927 --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/Models/SomeJobResult.cs @@ -0,0 +1,6 @@ +namespace Elsa.Samples.AspNet.HangfireIntegration.Models; + +public class SomeJobResult +{ + public string Message { get; set; } +} \ No newline at end of file diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/Program.cs b/samples/Elsa.Samples.AspNet.HangfireIntegration/Program.cs new file mode 100644 index 000000000..00ef210d7 --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/Program.cs @@ -0,0 +1,69 @@ +using Elsa.EntityFrameworkCore.Modules.Management; +using Elsa.EntityFrameworkCore.Modules.Runtime; +using Elsa.Extensions; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; +var identitySection = configuration.GetSection("Identity"); +var identityTokenSection = identitySection.GetSection("Tokens"); + +// Add Elsa to the container. +builder.Services.AddElsa(elsa => +{ + // Configure management feature to use EF Core. + elsa.UseWorkflowManagement(management => + { + management + .AddActivitiesFrom() + .UseEntityFrameworkCore(); + }); + + elsa.UseWorkflowRuntime(runtime => + { + runtime.UseEntityFrameworkCore(); + + // Use Hangfire to schedule background activities. + runtime.UseHangfireBackgroundActivityScheduler(); + }); + + // Expose API endpoints. + elsa.UseWorkflowsApi(); + + // Use Hangfire. + elsa.UseHangfire(hangfire => + { + hangfire.UseSqliteStorage(sqlite => sqlite.NameOrConnectionString = "elsa.sqlite.db"); + }); + + // Use hangfire for scheduling timer events. + elsa.UseScheduling(scheduling => scheduling.UseHangfireScheduler()); + + // Configure identity. + elsa.UseIdentity(identity => + { + identity.TokenOptions = options => identityTokenSection.Bind(options); + identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options)); + identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options)); + identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options)); + }); + + // Use default authentication (JWT). + elsa.UseDefaultAuthentication(); +}); + +// Configure CORS to allow designer app hosted on a different origin to invoke the APIs. +builder.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())); + +// Build the web app. +var app = builder.Build(); + +// Configure the web app's request pipeline. +app.UseHttpsRedirection(); +app.UseCors(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseWorkflowsApi(); +app.UseWorkflows(); + +// Run the web app. +app.Run(); \ No newline at end of file diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/Properties/launchSettings.json b/samples/Elsa.Samples.AspNet.HangfireIntegration/Properties/launchSettings.json new file mode 100644 index 000000000..3513ccba1 --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3978", + "sslPort": 44367 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5090", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7020;http://localhost:5090", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/README.md b/samples/Elsa.Samples.AspNet.HangfireIntegration/README.md new file mode 100644 index 000000000..94f9fae21 --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/README.md @@ -0,0 +1,6 @@ +## Secrets +The following are the secrets stored in hashed form in appsettings.json: + +**API key**: `4E753976726458745954355043687772-e54d5a2c-33a3-4c05-a216-b09569062aed` +**Admin user**: `admin` +**Admin password**: `password` \ No newline at end of file diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/Stimuli/SomeJobStimulus.cs b/samples/Elsa.Samples.AspNet.HangfireIntegration/Stimuli/SomeJobStimulus.cs new file mode 100644 index 000000000..5bc571554 --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/Stimuli/SomeJobStimulus.cs @@ -0,0 +1,3 @@ +namespace Elsa.Samples.AspNet.HangfireIntegration.Stimuli; + +public record SomeJobStimulus; \ No newline at end of file diff --git a/samples/Elsa.Samples.AspNet.HangfireIntegration/appsettings.json b/samples/Elsa.Samples.AspNet.HangfireIntegration/appsettings.json new file mode 100644 index 000000000..ac4f10dd2 --- /dev/null +++ b/samples/Elsa.Samples.AspNet.HangfireIntegration/appsettings.json @@ -0,0 +1,44 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting": "Information", + "Hangfire": "Warning" + } + }, + "AllowedHosts": "*", + "Identity": { + "Tokens": { + "SigningKey": "secret-signing-key-of-at-least-256-bits", + "AccessTokenLifetime": "1:00:00:00", + "RefreshTokenLifetime": "1:00:10:00" + }, + "Roles": [{ + "Id": "admin", + "Name": "Administrator", + "Permissions": ["*"] + }], + "Users": [ + { + "Id": "a2323f46-42db-4e15-af8b-94238717d817", + "Name": "admin", + "HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=", + "HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=", + "Roles": ["admin"] + } + ], + "Applications": [{ + "id": "529572c2df854b13807b8bf23f1784cd", + "name": "Postman", + "roles": [ + "admin" + ], + "clientId": "Nu9vrdXtYT5PChwr", + "clientSecret": "011pp2C$|j01-qrMZpC9VC0F00XCJq(5", + "hashedApiKey": "d0rDld3A+ugKmdctGtMzOLTYjQFkOlUWN+kt0VyW9D0=", + "hashedApiKeySalt": "EnutGOyy5MuJWV0fF5jCQiciK7a8PU/DRF+fr6nekSY=", + "hashedClientSecret": "ERia2zBcCSWb/9dvB0grQ9yf7fWgFrClNeR8A5RMTzk=", + "hashedClientSecretSalt": "z3z8KmzHt+xkAj/zYTXcB8I7y0xAkLm95v4Er/oNqiY=" + }] + } +}