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