Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Reflection;

namespace Elsa.Http.OpenApi.Contracts;

/// <summary>
/// Service for retrieving Elsa package version information.
/// </summary>
public interface IElsaVersionProvider
{
/// <summary>
/// Gets the Elsa package version.
/// </summary>
/// <returns>The package version string.</returns>
string GetVersion();
}
16 changes: 16 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/Contracts/IOpenApiGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Elsa.Http.OpenApi.Models;

namespace Elsa.Http.OpenApi.Contracts;

/// <summary>
/// Contract for generating OpenAPI JSON documentation.
/// </summary>
public interface IOpenApiGenerator
{
/// <summary>
/// Generates OpenAPI JSON documentation from a list of endpoint definitions.
/// </summary>
/// <param name="endpoints">The list of endpoint definitions.</param>
/// <returns>OpenAPI JSON string.</returns>
string GenerateOpenApiJson(List<EndpointDefinition> endpoints);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Elsa.Http.OpenApi.Models;

namespace Elsa.Http.OpenApi.Contracts;

/// <summary>
/// Contract for extracting workflow HTTP endpoints for OpenAPI documentation.
/// </summary>
public interface IWorkflowEndpointExtractor
{
/// <summary>
/// Extracts all HTTP endpoints from workflows.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of endpoint definitions.</returns>
Task<List<EndpointDefinition>> ExtractEndpointsAsync(CancellationToken cancellationToken = default);
}
37 changes: 37 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/Elsa.Http.OpenApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>
Provides OpenAPI functionality for HTTP triggers in Elsa workflows.
</Description>
<PackageTags>elsa extension module, http, openapi</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>

<ItemGroup Label="Elsa" Condition="'$(UseProjectReferences)' != 'true'">
<PackageReference Include="Elsa.Workflows.Runtime" />
<PackageReference Include="Elsa.Workflows.Management" />
<PackageReference Include="Elsa.Http" />
<PackageReference Include="Elsa.Common" />
<PackageReference Include="Elsa.Scheduling" />
<PackageReference Include="Elsa.Api.Common" />
</ItemGroup>

<ItemGroup Label="Elsa" Condition="'$(UseProjectReferences)' == 'true'">
<ProjectReference Include="..\..\..\..\..\elsa-core\src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
<ProjectReference Include="..\..\..\..\..\elsa-core\src\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
<ProjectReference Include="..\..\..\..\..\elsa-core\src\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\..\..\..\elsa-core\src\common\Elsa.Common\Elsa.Common.csproj" />
<ProjectReference Include="..\..\..\..\..\elsa-core\src\modules\Elsa.Scheduling\Elsa.Scheduling.csproj" />
<ProjectReference Include="..\..\..\..\..\elsa-core\src\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
</ItemGroup>

<ItemGroup>
<None Remove="README.md" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Elsa.Http.OpenApi.Contracts;
using Elsa.Http.OpenApi.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

namespace Elsa.Http.OpenApi.Extensions;

/// <summary>
/// Extension methods for mapping OpenAPI endpoints.
/// </summary>
public static class EndpointRouteBuilderExtensions
{
/// <summary>
/// Maps OpenAPI endpoints for workflow documentation.
/// </summary>
/// <param name="app">The endpoint route builder.</param>
/// <returns>The endpoint route builder for chaining.</returns>
public static IEndpointRouteBuilder MapWorkflowOpenApi(this IEndpointRouteBuilder app)
{
// OpenAPI JSON endpoint
app.MapGet("/openapi.json", async ([FromServices] IWorkflowEndpointExtractor extractor, [FromServices] IOpenApiGenerator generator) =>
{
var endpoints = await extractor.ExtractEndpointsAsync();
var openApiJson = generator.GenerateOpenApiJson(endpoints);
return Results.Content(openApiJson, "application/json");
});

// ReDoc UI endpoint
app.MapGet("/documentation", () =>
{
var html = @"
<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
</head>
<body>
<redoc spec-url=""/openapi.json""></redoc>
<script src=""https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js""> </script>
</body>
</html>";
return Results.Content(html, "text/html");
});

return app;
}
}
22 changes: 22 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/Extensions/ModuleExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Elsa.Extensions;
using Elsa.Features.Services;
using Elsa.Http.Features;
using Elsa.Http.OpenApi.Features;

// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;

/// <summary>
/// Provides extensions to install the HTTP OpenAPI feature.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Install the <see cref="HttpOpenApiFeature"/> feature as part of the HTTP feature configuration.
/// </summary>
public static HttpFeature UseOpenApi(this HttpFeature feature, Action<HttpOpenApiFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Elsa.Http.OpenApi.Contracts;
using Elsa.Http.OpenApi.Services;
using Microsoft.Extensions.DependencyInjection;

namespace Elsa.Http.OpenApi.Extensions;

/// <summary>
/// Extension methods for configuring HTTP OpenAPI services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds HTTP OpenAPI services to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddElsaHttpOpenApi(this IServiceCollection services)
{
services.AddScoped<IWorkflowEndpointExtractor, WorkflowEndpointExtractor>();
services.AddSingleton<IElsaVersionProvider, ElsaVersionProvider>();
services.AddSingleton<IOpenApiGenerator, OpenApiGenerator>();

return services;
}
}
25 changes: 25 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/Features/HttpOpenApiFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Http.Features;
using Elsa.Http.OpenApi.Extensions;

namespace Elsa.Http.OpenApi.Features;

/// <summary>
/// Feature for HTTP OpenAPI functionality.
/// </summary>
[DependsOn(typeof(HttpFeature))]
public class HttpOpenApiFeature : FeatureBase
{
/// <inheritdoc />
public HttpOpenApiFeature(IModule module) : base(module)
{
}

/// <inheritdoc />
public override void Apply()
{
Services.AddElsaHttpOpenApi();
}
}
3 changes: 3 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/FodyWeavers.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ο»Ώ<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
18 changes: 18 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/Models/EndpointDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Elsa.Http.OpenApi.Models;

/// <summary>
/// Represents an HTTP endpoint definition extracted from a workflow.
/// </summary>
/// <param name="Path">The HTTP path of the endpoint.</param>
/// <param name="Method">The HTTP method (GET, POST, etc.).</param>
/// <param name="Summary">Optional summary description of the endpoint.</param>
/// <param name="WorkflowDefinitionId">The workflow definition ID that contains this endpoint.</param>
/// <param name="WorkflowDefinitionName">The name of the workflow definition.</param>
/// <param name="WorkflowVersion">The version of the workflow definition.</param>
public record EndpointDefinition(
string Path,
string Method,
string? Summary = null,
string? WorkflowDefinitionId = null,
string? WorkflowDefinitionName = null,
int? WorkflowVersion = null);
Empty file.
44 changes: 44 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/Services/ElsaVersionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Linq;
using System.Reflection;
using Elsa.Http.OpenApi.Contracts;

namespace Elsa.Http.OpenApi.Services;

/// <summary>
/// Service for retrieving Elsa package version information by examining loaded assemblies.
/// </summary>
public class ElsaVersionProvider : IElsaVersionProvider
{
/// <summary>
/// Gets the Elsa package version by examining loaded assemblies.
/// </summary>
/// <returns>The package version string.</returns>
public string GetVersion()
{
// Try to get version from Elsa.Workflows.Core assembly first
var elsaCoreAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "Elsa.Workflows.Core");

if (elsaCoreAssembly != null)
{
var versionAttribute = elsaCoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
return RemoveAutoGeneratedPostfix(versionAttribute);
}

// Fallback to any Elsa assembly
var elsaAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name?.StartsWith("Elsa.") == true);

if (elsaAssembly != null)
{
var versionAttribute = elsaAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
return RemoveAutoGeneratedPostfix(versionAttribute);
}

// Final fallback
return "1.0.0";
}

private static string RemoveAutoGeneratedPostfix(AssemblyInformationalVersionAttribute? versionAttribute) =>
versionAttribute?.InformationalVersion.Split("+")[0] ?? "1.0.0";
}
85 changes: 85 additions & 0 deletions src/modules/http/Elsa.Http.OpenApi/Services/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Elsa.Http.OpenApi.Contracts;
using Elsa.Http.OpenApi.Models;
using System.Text.Json;

namespace Elsa.Http.OpenApi.Services;

/// <summary>
/// Service for generating OpenAPI JSON documentation from workflow endpoints.
/// </summary>
public class OpenApiGenerator : IOpenApiGenerator
{
private readonly IElsaVersionProvider _versionProvider;

public OpenApiGenerator(IElsaVersionProvider versionProvider)
{
_versionProvider = versionProvider;
}

/// <summary>
/// Generates OpenAPI JSON documentation from a list of endpoint definitions.
/// </summary>
/// <param name="endpoints">The list of endpoint definitions.</param>
/// <returns>OpenAPI JSON string.</returns>
public string GenerateOpenApiJson(List<EndpointDefinition> endpoints)
{
var openApiDoc = new
{
openapi = "3.0.0",
info = new
{
title = "Elsa Workflow HTTP Endpoints",
version = _versionProvider.GetVersion(),
description = "HTTP endpoints exposed by Elsa workflows"
},
paths = GeneratePaths(endpoints)
};

return JsonSerializer.Serialize(openApiDoc, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}

private object GeneratePaths(List<EndpointDefinition> endpoints)
{
var paths = new Dictionary<string, object>();

foreach (var endpoint in endpoints)
{
if (!paths.ContainsKey(endpoint.Path))
{
paths[endpoint.Path] = new Dictionary<string, object>();
}

var pathItem = (Dictionary<string, object>)paths[endpoint.Path];

// Create description with workflow definition ID if available
var description = endpoint.WorkflowDefinitionId != null
? $"Workflow endpoint from '{endpoint.WorkflowDefinitionName}' (ID: {endpoint.WorkflowDefinitionId})"
: $"Workflow endpoint for {endpoint.Method.ToUpperInvariant()} {endpoint.Path}";

// Use workflow name as tag, fallback to "Workflows"
var tags = endpoint.WorkflowDefinitionName != null
? new[] { endpoint.WorkflowDefinitionName }
: new[] { "Workflows" };

pathItem[endpoint.Method.ToLowerInvariant()] = new
{
summary = $"{endpoint.Method.ToUpperInvariant()} {endpoint.Path}",
description = description,
responses = new
{
@default = new
{
description = "Response from workflow execution"
}
},
tags = tags
};
}

return paths;
}
}
Loading
Loading