Add sample project for WorkflowContext module

This commit is contained in:
Sipke Schoorstra 2023-04-18 00:01:24 +02:00
parent 90e4b4680c
commit be911646e6
15 changed files with 459 additions and 0 deletions

View file

@ -174,6 +174,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.HangfireIntegr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.ProtoActorRuntime", "src\samples\aspnet\Elsa.Samples.ProtoActorRuntime\Elsa.Samples.ProtoActorRuntime.csproj", "{A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.WorkflowContexts", "src\samples\aspnet\Elsa.Samples.WorkflowContexts\Elsa.Samples.WorkflowContexts.csproj", "{F88AB1C2-100E-459F-B2BE-5F2ACF050A0A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -432,6 +434,10 @@ Global
{A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}.Release|Any CPU.Build.0 = Release|Any CPU
{F88AB1C2-100E-459F-B2BE-5F2ACF050A0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F88AB1C2-100E-459F-B2BE-5F2ACF050A0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F88AB1C2-100E-459F-B2BE-5F2ACF050A0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F88AB1C2-100E-459F-B2BE-5F2ACF050A0A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@ -508,5 +514,6 @@ Global
{B0312D9E-FA30-43E9-B666-40A8782D6E1C} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{D2614FC7-102F-4F78-BB06-7C87304A10BA} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{F88AB1C2-100E-459F-B2BE-5F2ACF050A0A} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,10 @@
namespace Elsa.Samples.WorkflowContexts.Contracts;
/// <summary>
/// A sample repository of customers.
/// </summary>
public interface ICustomerStore
{
Task<Customer?> GetAsync(string id, CancellationToken cancellationToken = default);
Task SaveAsync(Customer customer, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,20 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["src/samples/aspnet/Elsa.Samples.WorkflowContexts/Elsa.Samples.WorkflowContexts.csproj", "src/samples/aspnet/Elsa.Samples.WorkflowContexts/"]
RUN dotnet restore "src/samples/aspnet/Elsa.Samples.WorkflowContexts/Elsa.Samples.WorkflowContexts.csproj"
COPY . .
WORKDIR "/src/src/samples/aspnet/Elsa.Samples.WorkflowContexts"
RUN dotnet build "Elsa.Samples.WorkflowContexts.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Elsa.Samples.WorkflowContexts.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Elsa.Samples.WorkflowContexts.dll"]

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Email\Elsa.Email.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.JavaScript\Elsa.JavaScript.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Scheduling\Elsa.Scheduling.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.WorkflowContexts\Elsa.WorkflowContexts.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,10 @@
namespace Elsa.Samples.WorkflowContexts.Contracts;
public class Customer
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Website { get; set; }
}

View file

@ -0,0 +1,11 @@
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Samples.WorkflowContexts.Contracts;
using Elsa.Samples.WorkflowContexts.Providers;
namespace Elsa.Samples.WorkflowContexts.Extensions;
public static class ExpressionExecutionContextExtensions
{
public static Customer GetCustomer(this ExpressionExecutionContext context) => context.GetWorkflowContext<CustomerWorkflowContextProvider, Customer>();
}

View file

@ -0,0 +1,10 @@
using Elsa.Workflows.Core.Models;
namespace Elsa.Samples.WorkflowContexts.Extensions;
public static class WorkflowExecutionContextExtensions
{
private const string CustomerIdKey = "CustomerId";
public static string? GetCustomerId(this WorkflowExecutionContext context) => context.GetProperty<string>(CustomerIdKey);
public static void SetCustomerId(this WorkflowExecutionContext context, string? customerId) => context.SetProperty(CustomerIdKey, customerId);
}

View file

@ -0,0 +1,106 @@
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Samples.WorkflowContexts.Contracts;
using Elsa.Samples.WorkflowContexts.Providers;
using Elsa.Samples.WorkflowContexts.Services;
using Elsa.Samples.WorkflowContexts.Workflows;
using Elsa.Workflows.Core.Middleware.Workflows;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var identitySection = configuration.GetSection("Identity");
var identityTokenSection = identitySection.GetSection("Tokens");
// Add Elsa services.
services
.AddElsa(elsa => elsa
.AddActivitiesFrom<Program>()
.AddWorkflow<CustomerCommunicationsWorkflow>()
.UseIdentity(identity =>
{
identity.IdentityOptions = options => identitySection.Bind(options);
identity.TokenOptions = options => identityTokenSection.Bind(options);
identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseWorkflows(workflows => workflows.WithWorkflowExecutionPipeline(pipeline => pipeline
.Reset()
.UsePersistentVariables()
.UseBookmarkPersistence()
.UseWorkflowExecutionLogPersistence()
.UseWorkflowStatePersistence()
.UseWorkflowContexts()
.UseDefaultActivityScheduler()
))
.UseWorkflowManagement(management =>
{
// Use EF core for workflow definitions and instances.
management.UseWorkflowInstances(m => m.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)));
management.UseEntityFrameworkCore(m => m.UseSqlite(sqliteConnectionString));
})
.UseWorkflowRuntime(runtime =>
{
// Use EF core for triggers and bookmarks.
runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
// Use EF core for execution log records.
runtime.UseExecutionLogRecords(log => log.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)));
// Use the default workflow runtime with EF core.
runtime.UseDefaultRuntime(defaultRuntime => defaultRuntime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)));
// Install a workflow state exporter to capture workflow states and store them in IWorkflowInstanceStore.
runtime.UseAsyncWorkflowStateExporter();
})
.UseScheduling()
.UseWorkflowsApi(api => api.AddFastEndpointsAssembly<Program>())
.UseJavaScript()
.UseLiquid()
.UseHttp()
.UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options))
);
// Add health checks.
services.AddHealthChecks();
// Add CORS.
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
// Add domain services.
services.AddSingleton<ICustomerStore, MemoryCustomerStore>();
// Add workflow context providers.
services.AddWorkflowContextProvider<CustomerWorkflowContextProvider>();
// Configure middleware pipeline.
var app = builder.Build();
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
// CORS.
app.UseCors();
// Health checks.
app.MapHealthChecks("/");
app.UseAuthentication();
app.UseAuthorization();
// Elsa API endpoints for designer.
app.UseWorkflowsApi();
// Captures unhandled exceptions and returns a JSON response.
app.UseJsonSerializationErrorHandler();
// Elsa HTTP Endpoint activities
app.UseWorkflows();
// Run.
app.Run();

View file

@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:32709",
"sslPort": 44321
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5286",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7019;http://localhost:5286",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,31 @@
using Elsa.Samples.WorkflowContexts.Contracts;
using Elsa.Samples.WorkflowContexts.Extensions;
using Elsa.WorkflowContexts.Abstractions;
using Elsa.Workflows.Core.Models;
namespace Elsa.Samples.WorkflowContexts.Providers;
public class CustomerWorkflowContextProvider : WorkflowContextProvider<Customer>
{
private readonly ICustomerStore _customerStore;
public CustomerWorkflowContextProvider(ICustomerStore customerStore)
{
_customerStore = customerStore;
}
protected override async ValueTask<Customer?> LoadAsync(WorkflowExecutionContext workflowExecutionContext)
{
var customerId = workflowExecutionContext.GetCustomerId();
return customerId != null ? await _customerStore.GetAsync(customerId) : null;
}
protected override async ValueTask SaveAsync(WorkflowExecutionContext workflowExecutionContext, Customer? context)
{
if (context != null)
{
await _customerStore.SaveAsync(context);
workflowExecutionContext.SetCustomerId(context.Id);
}
}
}

View file

@ -0,0 +1,10 @@
# Server
This project represents an Elsa application that hosts workflows and exposes API endpoints to manage & execute workflows.
## 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`

View file

@ -0,0 +1,53 @@
using Elsa.Samples.WorkflowContexts.Contracts;
namespace Elsa.Samples.WorkflowContexts.Services;
/// <summary>
/// An in-memory implementation of <see cref="ICustomerStore"/>.
/// </summary>
public class MemoryCustomerStore : ICustomerStore
{
private readonly IDictionary<string, Customer> _customers = CreateCustomersDatabase();
public Task<Customer?> GetAsync(string id, CancellationToken cancellationToken = default)
{
return Task.FromResult(_customers.TryGetValue(id, out var customer) ? customer : null);
}
public Task SaveAsync(Customer customer, CancellationToken cancellationToken = default)
{
_customers[customer.Id] = customer;
return Task.CompletedTask;
}
private static IDictionary<string, Customer> CreateCustomersDatabase()
{
return new Dictionary<string, Customer>
{
["1"] = new()
{
Id = "1",
Name = "John Doe",
Email = "john.doe@acme.com",
Phone = "+1 555 123 4567",
Website = "https://acme.com"
},
["2"] = new()
{
Id = "2",
Name = "Alice Smith",
Email = "alice.smith@example.com",
Phone = "+1 555 123 4567",
Website = "https://example.com"
},
["3"] = new()
{
Id = "3",
Name = "Bob Jones",
Email = "bob.jones@supplier.com",
Phone = "+1 555 123 4567",
Website = "https://supplier.com"
},
};
}
}

View file

@ -0,0 +1,57 @@
using Elsa.Email.Activities;
using Elsa.Extensions;
using Elsa.Samples.WorkflowContexts.Extensions;
using Elsa.Samples.WorkflowContexts.Providers;
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Samples.WorkflowContexts.Workflows;
/// <summary>
/// A workflow that sends annoying emails to customers.
/// </summary>
public class CustomerCommunicationsWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.AddWorkflowContextProvider<CustomerWorkflowContextProvider>();
builder.Root = new Sequence
{
Activities =
{
Inline.From(context => context.WorkflowExecutionContext.SetCustomerId(context.GetInput<string>("CustomerId"))),
Delay.FromSeconds(5),
new SendEmail
{
Subject = new(context => $"Welcome to our family, {context.GetCustomer().Name}!"),
Body = new("Welcome aboard!"),
To = new(context => new[] { context.GetCustomer().Email })
},
Delay.FromSeconds(5),
new SendEmail
{
Subject = new(context => $"{context.GetCustomer().Name}, we got great deals for you!"),
Body = new("Get your creditcard ready!"),
To = new(context => new[] { context.GetCustomer().Email })
},
Delay.FromSeconds(5),
new SendEmail
{
Subject = new(context => $"{context.GetCustomer().Name}, you're missing out!"),
Body = new("Sale ends in 2 hours!"),
To = new(context => new[] { context.GetCustomer().Email })
},
Delay.FromSeconds(5),
new SendEmail
{
Subject = new(context => $"{context.GetCustomer().Name}, the clock is ticking!"),
Body = new("Tick tik tick!"),
To = new(context => new[] { context.GetCustomer().Email })
},
}
};
}
}

View file

@ -0,0 +1,57 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Elsa.Mediator": "Warning",
"MassTransit": "Warning",
"Microsoft.Extensions.Http": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore": "Warning",
"System.Net.Http": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;"
},
"Identity": {
"Tokens": {
"SigningKey": "secret-signing-key",
"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="
}]
},
"Smtp": {
"Host": "localhost",
"Port": 2525,
"DefaultSender": "noreply@crmservices.com"
}
}

View file

@ -0,0 +1,12 @@
version: '3.9'
services:
smtp4dev:
image: rnwood/smtp4dev:3.1.3-ci20211206101
ports:
- "3000:80"
- "2525:25"
volumes:
mssql-azuresql-edge-data: