Initial scaffold: Elsa 3.5 custom API server with sample Shout activity
- src/Elsa.CustomServer: minimal ASP.NET Core 8 host referencing Elsa 3.5 NuGets - Activities auto-registered via AddActivitiesFrom<Program>() — drop new files under Activities/ - Sqlite persistence for workflow definitions + runtime - Multi-stage Dockerfile targeting linux/amd64, ASP.NET 8 runtime, port 8080 - Forwarded-headers configured so URLs come out https:// behind Traefik - CORS wide-open so the standalone Studio (different origin) can talk to us
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/App_Data
|
||||||
|
**/*.db
|
||||||
|
**/*.db-*
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
deploy.json
|
||||||
|
README.md
|
||||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## .NET
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
## Rider / VS Code
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
## Runtime / build output
|
||||||
|
publish/
|
||||||
|
out/
|
||||||
|
TestResults/
|
||||||
|
|
||||||
|
## macOS / Linux
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
## Elsa runtime state
|
||||||
|
App_Data/
|
||||||
|
*.db
|
||||||
|
*.db-*
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-journal
|
||||||
|
|
||||||
|
## Coolify config carries the app UUID and env values, keep it local
|
||||||
|
deploy.json
|
||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Elsa Custom Server — Docker image
|
||||||
|
# Multi-stage build so the final image only carries the ASP.NET runtime.
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /source
|
||||||
|
|
||||||
|
# Restore first for better layer caching.
|
||||||
|
COPY src/Elsa.CustomServer/Elsa.CustomServer.csproj src/Elsa.CustomServer/
|
||||||
|
RUN dotnet restore src/Elsa.CustomServer/Elsa.CustomServer.csproj
|
||||||
|
|
||||||
|
# Then copy the rest and publish.
|
||||||
|
COPY src/ src/
|
||||||
|
RUN dotnet publish src/Elsa.CustomServer/Elsa.CustomServer.csproj \
|
||||||
|
-c Release -o /app/publish --no-restore /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish ./
|
||||||
|
|
||||||
|
# Ensure the SQLite DB and any workflow blobs land on a writable path we can volume-mount.
|
||||||
|
RUN mkdir -p /app/App_Data && chmod 777 /app/App_Data
|
||||||
|
VOLUME ["/app/App_Data"]
|
||||||
|
|
||||||
|
# Kestrel binds to whatever HTTP_PORTS says; keep the container port fixed so Coolify
|
||||||
|
# can wire Traefik consistently.
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
ENV HTTP_PORTS=8080
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["dotnet", "Elsa.CustomServer.dll"]
|
||||||
41
README.md
Normal file
41
README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Elsa Custom Server
|
||||||
|
|
||||||
|
Custom Elsa Workflows API server. Extends stock Elsa 3.x with our own activities.
|
||||||
|
|
||||||
|
## Where activities live
|
||||||
|
|
||||||
|
`src/Elsa.CustomServer/Activities/*.cs` — anything marked `[Activity(...)]` under this
|
||||||
|
folder is auto-registered via `AddActivitiesFrom<Program>()` in `Program.cs`.
|
||||||
|
|
||||||
|
To add a new node type in the Studio designer:
|
||||||
|
|
||||||
|
1. Drop a new file under `Activities/`.
|
||||||
|
2. Inherit from `CodeActivity`, `CodeActivity<T>`, or `Activity` for triggerable ones.
|
||||||
|
3. Rebuild and redeploy — Studio picks it up automatically the next time it fetches
|
||||||
|
the activity descriptors.
|
||||||
|
|
||||||
|
Example — see `Activities/Shout.cs`.
|
||||||
|
|
||||||
|
## Local run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotnet run --project src/Elsa.CustomServer
|
||||||
|
```
|
||||||
|
|
||||||
|
Listens on http://localhost:8080; API root is `/elsa/api`.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build -t elsa-custom-server .
|
||||||
|
docker run --rm -p 8080:8080 \
|
||||||
|
-e Identity__Tokens__SigningKey="$(openssl rand -base64 64)" \
|
||||||
|
elsa-custom-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Coolify builds this image from the Dockerfile at repo root when the app is created
|
||||||
|
with `build_pack: "dockerfile"`. The Studio (separate container running the stock
|
||||||
|
`elsaworkflows/elsa-studio-v3` image) needs its `BACKEND__URL` env var pointed at
|
||||||
|
this server's `/elsa/api`.
|
||||||
21
src/Elsa.CustomServer/Activities/Shout.cs
Normal file
21
src/Elsa.CustomServer/Activities/Shout.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Elsa.Extensions;
|
||||||
|
using Elsa.Workflows;
|
||||||
|
using Elsa.Workflows.Attributes;
|
||||||
|
using Elsa.Workflows.Models;
|
||||||
|
|
||||||
|
namespace Elsa.CustomServer.Activities;
|
||||||
|
|
||||||
|
/// Sample custom activity — appears in the Studio toolbox under the "Custom" category.
|
||||||
|
[Activity("Custom", "Shout", "Uppercases a string and returns it.", Kind = ActivityKind.Task)]
|
||||||
|
public class Shout : CodeActivity<string>
|
||||||
|
{
|
||||||
|
[Input(Description = "The text to shout.")]
|
||||||
|
public Input<string> Text { get; set; } = default!;
|
||||||
|
|
||||||
|
protected override void Execute(ActivityExecutionContext context)
|
||||||
|
{
|
||||||
|
var input = context.Get(Text) ?? string.Empty;
|
||||||
|
var result = input.ToUpperInvariant() + "!";
|
||||||
|
context.SetResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/Elsa.CustomServer/Elsa.CustomServer.csproj
Normal file
29
src/Elsa.CustomServer/Elsa.CustomServer.csproj
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>Elsa.CustomServer</RootNamespace>
|
||||||
|
<AssemblyName>Elsa.CustomServer</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Elsa 3.x meta packages. Kept on the latest 3.x line so the Studio (elsa-studio-v3) can talk to us. -->
|
||||||
|
<PackageReference Include="Elsa" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.Http" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.Identity" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.Scheduling" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.Workflows.Api" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.EntityFrameworkCore" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.EntityFrameworkCore.Sqlite" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.CSharp" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.JavaScript" Version="3.5.0" />
|
||||||
|
<PackageReference Include="Elsa.Liquid" Version="3.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="App_Data\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
67
src/Elsa.CustomServer/Program.cs
Normal file
67
src/Elsa.CustomServer/Program.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Elsa.EntityFrameworkCore.Extensions;
|
||||||
|
using Elsa.EntityFrameworkCore.Modules.Management;
|
||||||
|
using Elsa.EntityFrameworkCore.Modules.Runtime;
|
||||||
|
using Elsa.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
var configuration = builder.Configuration;
|
||||||
|
var identitySection = configuration.GetSection("Identity");
|
||||||
|
var identityTokenSection = identitySection.GetSection("Tokens");
|
||||||
|
|
||||||
|
builder.Services.AddElsa(elsa =>
|
||||||
|
{
|
||||||
|
elsa
|
||||||
|
// Discovers everything in this assembly decorated with [Activity] — including anything you drop under Activities/.
|
||||||
|
.AddActivitiesFrom<Program>()
|
||||||
|
.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));
|
||||||
|
})
|
||||||
|
.UseDefaultAuthentication()
|
||||||
|
.UseWorkflowManagement(m => m.UseEntityFrameworkCore(ef => ef.UseSqlite()))
|
||||||
|
.UseWorkflowRuntime(r => r.UseEntityFrameworkCore(ef => ef.UseSqlite()))
|
||||||
|
.UseScheduling()
|
||||||
|
.UseWorkflowsApi()
|
||||||
|
.UseHttp(http => http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options))
|
||||||
|
.UseCSharp()
|
||||||
|
.UseJavaScript()
|
||||||
|
.UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Studio (whichever host) is on a different origin, so allow it in.
|
||||||
|
builder.Services.AddCors(cors => cors.AddDefaultPolicy(p => p
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyOrigin()
|
||||||
|
.WithExposedHeaders("x-elsa-workflow-instance-id")));
|
||||||
|
|
||||||
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
|
// Traefik terminates TLS in front of us; without this, self-generated URLs come out as http://
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(o =>
|
||||||
|
{
|
||||||
|
o.ForwardedHeaders = ForwardedHeaders.All;
|
||||||
|
o.KnownNetworks.Clear();
|
||||||
|
o.KnownProxies.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseForwardedHeaders();
|
||||||
|
app.UseCors();
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.UseWorkflowsApi();
|
||||||
|
app.UseWorkflows();
|
||||||
|
app.UseJsonSerializationErrorHandler();
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
app.MapGet("/", () => Results.Ok(new { name = "Elsa Custom Server", status = "ok" }));
|
||||||
|
|
||||||
|
app.Run();
|
||||||
8
src/Elsa.CustomServer/appsettings.Development.json
Normal file
8
src/Elsa.CustomServer/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"Microsoft.AspNetCore": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/Elsa.CustomServer/appsettings.json
Normal file
42
src/Elsa.CustomServer/appsettings.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;"
|
||||||
|
},
|
||||||
|
"Http": {
|
||||||
|
"BaseUrl": "https://elsa-api.tes.gd",
|
||||||
|
"BasePath": "/workflows"
|
||||||
|
},
|
||||||
|
"Identity": {
|
||||||
|
"Tokens": {
|
||||||
|
"SigningKey": "CHANGE_ME_VIA_ENV_Identity__Tokens__SigningKey",
|
||||||
|
"AccessTokenLifetime": "1:00:00",
|
||||||
|
"RefreshTokenLifetime": "1.00:00:00"
|
||||||
|
},
|
||||||
|
"Roles": [
|
||||||
|
{
|
||||||
|
"Id": "admin",
|
||||||
|
"Name": "Administrator",
|
||||||
|
"Permissions": [ "*" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Users": [
|
||||||
|
{
|
||||||
|
"Id": "admin",
|
||||||
|
"Name": "admin",
|
||||||
|
"Email": "admin@elsa.local",
|
||||||
|
"HashedPassword": "",
|
||||||
|
"HashedPasswordSalt": "",
|
||||||
|
"Roles": [ "admin" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Applications": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user