WriteHttpFileResponse Activity (#4445)
* Incremental work on WriteFileHttpResponse activity * BREAKING Schema change - Increase bookmark flexibility * Implement zipped file response
This commit is contained in:
parent
12f9e0844b
commit
bb2fa55d0e
|
|
@ -3,6 +3,7 @@
|
|||
<s:Boolean x:Key="/Default/UserDictionary/Words/=abcdefghijklmnopqrstuvwxyz/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cronos/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Deconstructor/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=downloadables/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=granularly/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Initializable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=materializers/@EntryIndexedValue">True</s:Boolean>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ using Proto.Persistence.SqlServer;
|
|||
const bool useMongoDb = false;
|
||||
const bool useSqlServer = false;
|
||||
const bool useDapper = false;
|
||||
const bool useProtoActor = false;
|
||||
const bool useProtoActor = true;
|
||||
const bool useHangfire = false;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ public record Bookmark(
|
|||
string ActivityNodeId,
|
||||
string ActivityInstanceId,
|
||||
bool AutoBurn = true,
|
||||
string? CallbackMethodName = default
|
||||
string? CallbackMethodName = default,
|
||||
IDictionary<string, string>? Metadata = default
|
||||
)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public class Initial : Migration
|
|||
.WithColumn("ActivityInstanceId").AsString().NotNullable()
|
||||
.WithColumn("Hash").AsString().Nullable()
|
||||
.WithColumn("SerializedPayload").AsString().Nullable()
|
||||
.WithColumn("SerializedMetadata").AsString().Nullable()
|
||||
.WithColumn("CreatedAt").AsDateTimeOffset().NotNullable();
|
||||
|
||||
IfDatabase("Sqlite")
|
||||
|
|
@ -43,6 +44,7 @@ public class Initial : Migration
|
|||
.WithColumn("ActivityInstanceId").AsString().NotNullable()
|
||||
.WithColumn("Hash").AsString().Nullable()
|
||||
.WithColumn("SerializedPayload").AsString().Nullable()
|
||||
.WithColumn("SerializedMetadata").AsString().Nullable()
|
||||
.WithColumn("CreatedAt").AsDateTime2().NotNullable();
|
||||
|
||||
IfDatabase("SqlServer", "Oracle", "MySql", "Postgres")
|
||||
|
|
|
|||
|
|
@ -9,5 +9,6 @@ internal class StoredBookmarkRecord
|
|||
public string? CorrelationId { get; set; }
|
||||
public string? ActivityInstanceId { get; set; }
|
||||
public string? SerializedPayload { get; set; }
|
||||
public string? SerializedMetadata { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ public class DapperBookmarkStore : IBookmarkStore
|
|||
{
|
||||
return await _store.DeleteAsync(q => ApplyFilter(q, filter), cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
private void ApplyFilter(ParameterizedQuery query, BookmarkFilter filter)
|
||||
{
|
||||
query
|
||||
|
|
@ -62,7 +62,7 @@ public class DapperBookmarkStore : IBookmarkStore
|
|||
.Is(nameof(StoredBookmarkRecord.ActivityInstanceId), filter.ActivityInstanceId)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
private IEnumerable<StoredBookmark> Map(IEnumerable<StoredBookmarkRecord> source) => source.Select(Map);
|
||||
|
||||
private StoredBookmarkRecord Map(StoredBookmark source)
|
||||
|
|
@ -76,10 +76,11 @@ public class DapperBookmarkStore : IBookmarkStore
|
|||
ActivityTypeName = source.ActivityTypeName,
|
||||
Hash = source.Hash,
|
||||
SerializedPayload = source.Payload != null ? _payloadSerializer.Serialize(source.Payload) : default,
|
||||
SerializedMetadata = source.Metadata != null ? _payloadSerializer.Serialize(source.Metadata) : default,
|
||||
CreatedAt = source.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private StoredBookmark Map(StoredBookmarkRecord source)
|
||||
{
|
||||
return new StoredBookmark
|
||||
|
|
@ -91,6 +92,7 @@ public class DapperBookmarkStore : IBookmarkStore
|
|||
ActivityTypeName = source.ActivityTypeName,
|
||||
Hash = source.Hash,
|
||||
Payload = source.SerializedPayload != null ? _payloadSerializer.Deserialize(source.SerializedPayload) : default,
|
||||
Metadata = source.SerializedMetadata != null ? _payloadSerializer.Deserialize<Dictionary<string, string>>(source.SerializedMetadata) : default,
|
||||
CreatedAt = source.CreatedAt
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20230911202213_Initial")]
|
||||
[Migration("20230915185743_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -127,6 +127,9 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
|
|
@ -70,6 +70,8 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
|||
CorrelationId = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
|
||||
SerializedMetadata = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SerializedPayload = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
|
|
@ -124,6 +124,9 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||
namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20230911202223_Initial")]
|
||||
[Migration("20230915185758_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -130,6 +130,9 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
@ -51,6 +51,7 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
|||
ActivityInstanceId = table.Column<string>(type: "text", nullable: true),
|
||||
CorrelationId = table.Column<string>(type: "text", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
SerializedMetadata = table.Column<string>(type: "text", nullable: true),
|
||||
SerializedPayload = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
|
|
@ -127,6 +127,9 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20230911202217_Initial")]
|
||||
[Migration("20230915185751_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -130,6 +130,9 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
@ -51,6 +51,7 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
|||
ActivityInstanceId = table.Column<string>(type: "nvarchar(450)", nullable: true),
|
||||
CorrelationId = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
SerializedMetadata = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
SerializedPayload = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
|
|
@ -127,6 +127,9 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20230911202220_Initial")]
|
||||
[Migration("20230915185754_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -125,6 +125,9 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
@ -45,6 +45,7 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
|||
ActivityInstanceId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CorrelationId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SerializedMetadata = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SerializedPayload = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
|
|
@ -122,6 +122,9 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
|||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SerializedMetadata")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SerializedPayload")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,11 @@ public class EFCoreBookmarkStore : IBookmarkStore
|
|||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<long> DeleteAsync(BookmarkFilter filter, CancellationToken cancellationToken = default) => await _store.DeleteWhereAsync(filter.Apply, cancellationToken);
|
||||
|
||||
|
||||
private ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, StoredBookmark entity, CancellationToken cancellationToken)
|
||||
{
|
||||
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? _serializer.Serialize(entity.Payload) : default;
|
||||
dbContext.Entry(entity).Property("SerializedMetadata").CurrentValue = entity.Metadata != null ? _serializer.Serialize(entity.Metadata) : default;
|
||||
return default;
|
||||
}
|
||||
|
||||
|
|
@ -43,9 +44,11 @@ public class EFCoreBookmarkStore : IBookmarkStore
|
|||
if (entity is null)
|
||||
return default;
|
||||
|
||||
var json = dbContext.Entry(entity).Property<string>("SerializedPayload").CurrentValue;
|
||||
entity.Payload = !string.IsNullOrEmpty(json) ? _serializer.Deserialize(json) : null;
|
||||
|
||||
var payloadJson = dbContext.Entry(entity).Property<string>("SerializedPayload").CurrentValue;
|
||||
var metadataJson = dbContext.Entry(entity).Property<string>("SerializedMetadata").CurrentValue;
|
||||
entity.Payload = !string.IsNullOrEmpty(payloadJson) ? _serializer.Deserialize(payloadJson) : null;
|
||||
entity.Metadata = !string.IsNullOrEmpty(metadataJson) ? _serializer.Deserialize<Dictionary<string, string>>(metadataJson) : null;
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,9 @@ public class Configurations :
|
|||
public void Configure(EntityTypeBuilder<StoredBookmark> builder)
|
||||
{
|
||||
builder.Ignore(x => x.Payload);
|
||||
builder.Ignore(x => x.Metadata);
|
||||
builder.Property<string>("SerializedPayload");
|
||||
builder.Property<string>("SerializedMetadata");
|
||||
builder.HasKey(x => x.BookmarkId);
|
||||
builder.HasIndex(x => x.ActivityTypeName, $"IX_{nameof(StoredBookmark)}_{nameof(StoredBookmark.ActivityTypeName)}");
|
||||
builder.HasIndex(x => x.Hash, $"IX_{nameof(StoredBookmark)}_{nameof(StoredBookmark.Hash)}");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base class for <see cref="IDownloadableProvider"/> implementations.
|
||||
/// </summary>
|
||||
public abstract class DownloadableProviderBase : IDownloadableProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual float Priority => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool GetSupportsContent(object content);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of downloadables from the specified content.
|
||||
/// </summary>
|
||||
protected virtual async ValueTask<IEnumerable<Downloadable>> GetDownloadablesAsync(DownloadableContext context)
|
||||
{
|
||||
var downloadable = await GetDownloadableAsync(context);
|
||||
return new[]{ downloadable };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a downloadable from the specified content.
|
||||
/// </summary>
|
||||
protected virtual ValueTask<Downloadable> GetDownloadableAsync(DownloadableContext context)
|
||||
{
|
||||
var downloadable = GetDownloadable(context);
|
||||
return new (downloadable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a downloadable from the specified content.
|
||||
/// </summary>
|
||||
/// <exception cref="NotImplementedException">This method is not implemented. The derived class must implement at least one of the GetDownloadable methods.</exception>
|
||||
protected virtual Downloadable GetDownloadable(DownloadableContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
async ValueTask<IEnumerable<Downloadable>> IDownloadableProvider.GetDownloadablesAsync(DownloadableContext context)
|
||||
{
|
||||
return await GetDownloadablesAsync(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.Bookmarks;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.Workflows.Core;
|
||||
|
|
@ -103,7 +104,7 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
{
|
||||
// We're executing in a non-HTTP context (e.g. in a virtual actor).
|
||||
// Create a bookmark to allow the invoker to export the state and resume execution from there.
|
||||
context.CreateBookmark(OnResumeAsync);
|
||||
context.CreateBookmark(OnResumeAsync, BookmarkMetadata.HttpCrossBoundary);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
165
src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs
Normal file
165
src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
using Elsa.Workflows.Core.Exceptions;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace Elsa.Http;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a file to the HTTP response.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "HTTP", "Send one ore more files (zipped) to the HTTP response.", DisplayName = "HTTP File Response")]
|
||||
public class WriteFileHttpResponse : Activity
|
||||
{
|
||||
/// <summary>
|
||||
/// The MIME type of the file to serve.
|
||||
/// </summary>
|
||||
[Input(Description = "The content type of the file to serve. Leave empty to let the system determine the content type.")]
|
||||
public Input<string?> ContentType { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the file to serve.
|
||||
/// </summary>
|
||||
[Input(Description = "The name of the file to serve. Leave empty to let the system determine the file name.")]
|
||||
public Input<string?> FileName { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The file content to serve. Supports byte array, streams, string, Uri and an array of the aforementioned types.
|
||||
/// </summary>
|
||||
[Input(Description = "The file content to serve. Supports various types, such as byte array, stream, string, Uri, Downloadable and a (mixed) array of the aforementioned types.")]
|
||||
public Input<object> Content { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var httpContextAccessor = context.GetRequiredService<IHttpContextAccessor>();
|
||||
var httpContext = httpContextAccessor.HttpContext;
|
||||
|
||||
if (httpContext == null)
|
||||
{
|
||||
// We're executing in a non-HTTP context (e.g. in a virtual actor).
|
||||
// Create a bookmark to allow the invoker to export the state and resume execution from there.
|
||||
|
||||
context.CreateBookmark(OnResumeAsync, BookmarkMetadata.HttpCrossBoundary);
|
||||
return;
|
||||
}
|
||||
|
||||
await WriteResponseAsync(context, httpContext.Response);
|
||||
}
|
||||
|
||||
private async Task WriteResponseAsync(ActivityExecutionContext context, HttpResponse response)
|
||||
{
|
||||
// Set status code.
|
||||
var statusCode = HttpStatusCode.OK;
|
||||
response.StatusCode = (int)statusCode;
|
||||
|
||||
// Get content and content type.
|
||||
var content = context.Get(Content);
|
||||
|
||||
if (content == null)
|
||||
return;
|
||||
|
||||
// Write content.
|
||||
var downloadables = await GetDownloadablesAsync(context, content);
|
||||
await SendDownloadablesAsync(context, response, downloadables);
|
||||
|
||||
// Complete activity.
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
|
||||
private async Task SendDownloadablesAsync(ActivityExecutionContext context, HttpResponse response, IEnumerable<Downloadable> downloadables)
|
||||
{
|
||||
var downloadableList = downloadables.ToList();
|
||||
|
||||
switch (downloadableList.Count)
|
||||
{
|
||||
case 0:
|
||||
return;
|
||||
case 1:
|
||||
{
|
||||
var downloadable = downloadableList[0];
|
||||
await SendSingleFileAsync(context, response, downloadable);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
await SendMultipleFilesAsync(context, response, downloadableList);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendSingleFileAsync(ActivityExecutionContext context, HttpResponse response, Downloadable downloadable)
|
||||
{
|
||||
var contentType = ContentType.GetOrDefault(context);
|
||||
var filename = FileName.GetOrDefault(context);
|
||||
filename = !string.IsNullOrWhiteSpace(filename) ? filename : !string.IsNullOrWhiteSpace(downloadable.Filename) ? downloadable.Filename : "file.bin";
|
||||
contentType = !string.IsNullOrWhiteSpace(contentType) ? contentType : !string.IsNullOrWhiteSpace(downloadable.ContentType) ? downloadable.ContentType : GetContentType(context, filename);
|
||||
response.ContentType = contentType;
|
||||
response.Headers.Add("Content-Disposition", $"attachment; filename=\"{filename}\"");
|
||||
await downloadable.Stream.CopyToAsync(response.Body);
|
||||
}
|
||||
|
||||
private async Task SendMultipleFilesAsync(ActivityExecutionContext context, HttpResponse response, ICollection<Downloadable> downloadables)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
var currentFileIndex = 0;
|
||||
|
||||
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
foreach (var downloadable in downloadables)
|
||||
{
|
||||
var entryName = !string.IsNullOrWhiteSpace(downloadable.Filename) ? downloadable.Filename : $"file-{currentFileIndex}.bin";
|
||||
var entry = archive.CreateEntry(entryName);
|
||||
var fileStream = downloadable.Stream;
|
||||
await using var entryStream = entry.Open();
|
||||
await fileStream.CopyToAsync(entryStream);
|
||||
}
|
||||
}
|
||||
|
||||
var contentType = ContentType.GetOrDefault(context);
|
||||
var filename = FileName.GetOrDefault(context);
|
||||
|
||||
contentType = !string.IsNullOrWhiteSpace(contentType) ? contentType : System.Net.Mime.MediaTypeNames.Application.Zip;
|
||||
filename = !string.IsNullOrWhiteSpace(filename) ? filename : "download.zip";
|
||||
|
||||
memoryStream.Position = 0;
|
||||
response.ContentType = contentType;
|
||||
response.Headers.Add("Content-Disposition", $"attachment; filename=\"{filename}\"");
|
||||
await memoryStream.CopyToAsync(response.Body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leverages the <see cref="IDownloadableManager"/> to get a list of <see cref="Downloadable"/> instances from the <paramref name="content"/>.
|
||||
/// </summary>
|
||||
private async Task<IEnumerable<Downloadable>> GetDownloadablesAsync(ActivityExecutionContext context, object content)
|
||||
{
|
||||
var manager = context.GetRequiredService<IDownloadableManager>();
|
||||
return await manager.GetDownloadablesAsync(content, context.CancellationToken);
|
||||
}
|
||||
|
||||
private string GetContentType(ActivityExecutionContext context, string filename)
|
||||
{
|
||||
var provider = context.GetService<IContentTypeProvider>() ?? new FileExtensionContentTypeProvider();
|
||||
return provider.TryGetContentType(filename, out var contentType) ? contentType : "application/octet-stream";
|
||||
}
|
||||
|
||||
private async ValueTask OnResumeAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var httpContextAccessor = context.GetRequiredService<IHttpContextAccessor>();
|
||||
var httpContext = httpContextAccessor.HttpContext;
|
||||
|
||||
if (httpContext == null)
|
||||
{
|
||||
// We're not in an HTTP context, so let's fail.
|
||||
throw new FaultException("Cannot execute in a non-HTTP context");
|
||||
}
|
||||
|
||||
await WriteResponseAsync(context, httpContext.Response);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.Bookmarks;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.Http.Providers;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
using Elsa.Workflows.Core.Exceptions;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ public class WriteHttpResponse : Activity
|
|||
// We're executing in a non-HTTP context (e.g. in a virtual actor).
|
||||
// Create a bookmark to allow the invoker to export the state and resume execution from there.
|
||||
|
||||
context.CreateBookmark(OnResumeAsync);
|
||||
context.CreateBookmark(OnResumeAsync, BookmarkMetadata.HttpCrossBoundary);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +78,7 @@ public class WriteHttpResponse : Activity
|
|||
if (httpContext == null)
|
||||
{
|
||||
// We're not in an HTTP context, so let's fail.
|
||||
throw new Exception("Cannot execute in a non-HTTP context");
|
||||
throw new FaultException("Cannot execute in a non-HTTP context");
|
||||
}
|
||||
|
||||
await WriteResponseAsync(context, httpContext.Response);
|
||||
|
|
|
|||
17
src/modules/Elsa.Http/BookmarkMetadata.cs
Normal file
17
src/modules/Elsa.Http/BookmarkMetadata.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace Elsa.Http;
|
||||
|
||||
/// <summary>
|
||||
/// Provides metadata for HTTP bookmarks.
|
||||
/// </summary>
|
||||
public static class BookmarkMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The metadata key for cross-HTTP boundary activity execution.
|
||||
/// </summary>
|
||||
public const string HttpCrossBoundaryMetadataKey = "X-HttpCrossBoundary";
|
||||
|
||||
/// <summary>
|
||||
/// Provides metadata for cross-HTTP boundary activity execution.
|
||||
/// </summary>
|
||||
public static readonly IDictionary<string, string> HttpCrossBoundary = new Dictionary<string, string> { [HttpCrossBoundaryMetadataKey] = "true" };
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
|
||||
namespace Elsa.Http.Bookmarks;
|
||||
|
||||
/// <summary>
|
||||
/// A bookmark used by the <see cref="HttpEndpoint"/> activity.
|
||||
/// </summary>
|
||||
public record HttpEndpointBookmarkPayload
|
||||
{
|
||||
private readonly string _path = default!;
|
||||
private readonly string _method = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpEndpointBookmarkPayload"/> class.
|
||||
/// </summary>
|
||||
[JsonConstructor]
|
||||
public HttpEndpointBookmarkPayload()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpEndpointBookmarkPayload"/> class.
|
||||
/// </summary>
|
||||
public HttpEndpointBookmarkPayload(string path, string method, bool? authorize = default, string? policy = default)
|
||||
{
|
||||
Path = path;
|
||||
Method = method;
|
||||
Authorize = authorize;
|
||||
Policy = policy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path of the HTTP endpoint.
|
||||
/// </summary>
|
||||
public string Path
|
||||
{
|
||||
get => _path;
|
||||
init => _path = value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP method of the endpoint.
|
||||
/// </summary>
|
||||
public string Method
|
||||
{
|
||||
get => _method;
|
||||
init => _method = value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the policy to use for authorization.
|
||||
/// </summary>
|
||||
[ExcludeFromHash] public string? Policy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the endpoint requires authorization.
|
||||
/// </summary>
|
||||
[ExcludeFromHash] public bool? Authorize { get; set; }
|
||||
}
|
||||
11
src/modules/Elsa.Http/Contexts/DownloadableContext.cs
Normal file
11
src/modules/Elsa.Http/Contexts/DownloadableContext.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Elsa.Http.Contracts;
|
||||
|
||||
namespace Elsa.Http.Contexts;
|
||||
|
||||
/// <summary>
|
||||
/// Provides context for downloadable providers.
|
||||
/// </summary>
|
||||
/// <param name="Manager">The manager.</param>
|
||||
/// <param name="Content">The content to get downloadables from.</param>
|
||||
/// <param name="CancellationToken">The cancellation token.</param>
|
||||
public record DownloadableContext(IDownloadableManager Manager, object Content, CancellationToken CancellationToken);
|
||||
14
src/modules/Elsa.Http/Contracts/IDownloadableManager.cs
Normal file
14
src/modules/Elsa.Http/Contracts/IDownloadableManager.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Provides downloadables from the specified content, if supported.
|
||||
/// </summary>
|
||||
public interface IDownloadableManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of downloadables from the specified content.
|
||||
/// </summary>
|
||||
ValueTask<IEnumerable<Downloadable>> GetDownloadablesAsync(object content, CancellationToken cancellationToken = default);
|
||||
}
|
||||
26
src/modules/Elsa.Http/Contracts/IDownloadableProvider.cs
Normal file
26
src/modules/Elsa.Http/Contracts/IDownloadableProvider.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Provides downloadables from the specified content, if supported.
|
||||
/// </summary>
|
||||
public interface IDownloadableProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The priority of this provider. Providers with lower priority are tried first.
|
||||
/// </summary>
|
||||
float Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this provider supports the specified content.
|
||||
/// </summary>
|
||||
/// <param name="content">The content to check.</param>
|
||||
bool GetSupportsContent(object content);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of downloadables from the specified content.
|
||||
/// </summary>
|
||||
ValueTask<IEnumerable<Downloadable>> GetDownloadablesAsync(DownloadableContext context);
|
||||
}
|
||||
12
src/modules/Elsa.Http/Contracts/IFileDownloader.cs
Normal file
12
src/modules/Elsa.Http/Contracts/IFileDownloader.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Elsa.Http.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// A general-purpose downloader of files from a given URL.
|
||||
/// </summary>
|
||||
public interface IFileDownloader
|
||||
{
|
||||
/// <summary>
|
||||
/// Downloads a file from the specified URL.
|
||||
/// </summary>
|
||||
Task<HttpResponseMessage> DownloadAsync(Uri url, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using Elsa.Http.Abstractions;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.DownloadableProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles content that represents a downloadable binary file.
|
||||
/// </summary>
|
||||
public class BinaryDownloadableProvider : DownloadableProviderBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => content is byte[];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Downloadable GetDownloadable(DownloadableContext context)
|
||||
{
|
||||
var bytes = (byte[]) context.Content;
|
||||
var stream = new MemoryStream(bytes);
|
||||
var fileName = "file.bin";
|
||||
var contentType = "application/octet-stream";
|
||||
return new Downloadable(stream, fileName, contentType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Elsa.Http.Abstractions;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.DownloadableProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles content that represents a downloadable.
|
||||
/// </summary>
|
||||
public class DownloadableDownloadableProvider : DownloadableProviderBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => content is Downloadable;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Downloadable GetDownloadable(DownloadableContext context)
|
||||
{
|
||||
return (Downloadable) context.Content;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using System.Collections;
|
||||
using Elsa.Http.Abstractions;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.DownloadableProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles content that represents a list of downloadable objects.
|
||||
/// </summary>
|
||||
public class MultiDownloadableProvider : DownloadableProviderBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => content is IEnumerable enumerable and not string;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<IEnumerable<Downloadable>> GetDownloadablesAsync(DownloadableContext context)
|
||||
{
|
||||
var collectedDownloadables = new List<Downloadable>();
|
||||
var content = context.Content;
|
||||
var enumerable = (IEnumerable) content;
|
||||
var manager = context.Manager;
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
var downloadables = await manager.GetDownloadablesAsync(item, context.CancellationToken);
|
||||
collectedDownloadables.AddRange(downloadables);
|
||||
}
|
||||
|
||||
return collectedDownloadables;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Elsa.Http.Abstractions;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.DownloadableProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles content that represents a downloadable stream.
|
||||
/// </summary>
|
||||
public class StreamDownloadableProvider : DownloadableProviderBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => content is Stream;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Downloadable GetDownloadable(DownloadableContext context)
|
||||
{
|
||||
var stream = (Stream) context.Content;
|
||||
var fileName = "file.bin";
|
||||
var contentType = "application/octet-stream";
|
||||
return new Downloadable(stream, fileName, contentType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using Elsa.Http.Abstractions;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.DownloadableProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Handles content that represents a downloadable URL.
|
||||
/// </summary>
|
||||
public class UrlDownloadableProvider : DownloadableProviderBase
|
||||
{
|
||||
private readonly IFileDownloader _fileDownloader;
|
||||
|
||||
/// <inheritdoc />
|
||||
public UrlDownloadableProvider(IFileDownloader fileDownloader)
|
||||
{
|
||||
_fileDownloader = fileDownloader;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => (content is string url && url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) || content is Uri;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<Downloadable> GetDownloadableAsync(DownloadableContext context)
|
||||
{
|
||||
var url = context.Content is string s ? new Uri(s) : (Uri)context.Content;
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var response = await _fileDownloader.DownloadAsync(url, cancellationToken);
|
||||
var fileName = GetFilename(response) ?? url.Segments.Last();
|
||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream";
|
||||
|
||||
return new Downloadable(stream, fileName, contentType);
|
||||
}
|
||||
|
||||
private static string? GetFilename(HttpResponseMessage response)
|
||||
{
|
||||
if (!response.Content.Headers.TryGetValues("Content-Disposition", out var values))
|
||||
return null;
|
||||
|
||||
var contentDisposition = string.Join("", values);
|
||||
var match = Regex.Match(contentDisposition, """filename="?(?<filename>[^";]*)"?""");
|
||||
|
||||
return match.Success ? match.Groups["filename"].Value : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,8 +27,11 @@
|
|||
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
|
||||
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Http;
|
||||
using Elsa.Http.Bookmarks;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.Workflows.Core.Helpers;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Elsa.Features.Attributes;
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.DownloadableProviders;
|
||||
using Elsa.Http.Handlers;
|
||||
using Elsa.Http.HostedServices;
|
||||
using Elsa.Http.Models;
|
||||
|
|
@ -147,12 +148,20 @@ public class HttpFeature : FeatureBase
|
|||
// Port resolvers.
|
||||
.AddSingleton<IActivityPortResolver, SendHttpRequestActivityPortResolver>()
|
||||
|
||||
// Add HTTP endpoint handlers.
|
||||
// HTTP endpoint handlers.
|
||||
.AddSingleton<AuthenticationBasedHttpEndpointAuthorizationHandler>()
|
||||
.AddSingleton<AllowAnonymousHttpEndpointAuthorizationHandler>()
|
||||
.AddSingleton<DefaultHttpEndpointWorkflowFaultHandler>()
|
||||
.AddSingleton(HttpEndpointWorkflowFaultHandler)
|
||||
.AddSingleton(HttpEndpointAuthorizationHandler)
|
||||
|
||||
// File related services.
|
||||
.AddSingleton<IDownloadableManager, DefaultDownloadableManager>()
|
||||
.AddSingleton<IDownloadableProvider, BinaryDownloadableProvider>()
|
||||
.AddSingleton<IDownloadableProvider, DownloadableDownloadableProvider>()
|
||||
.AddSingleton<IDownloadableProvider, MultiDownloadableProvider>()
|
||||
.AddSingleton<IDownloadableProvider, StreamDownloadableProvider>()
|
||||
.AddSingleton<IDownloadableProvider, UrlDownloadableProvider>()
|
||||
|
||||
// Add mediator handlers.
|
||||
.AddNotificationHandlersFrom<HttpFeature>()
|
||||
|
|
@ -160,6 +169,9 @@ public class HttpFeature : FeatureBase
|
|||
// AuthenticationBasedHttpEndpointAuthorizationHandler requires Authorization services.
|
||||
// We could consider creating a separate module for installing authorization services.
|
||||
.AddAuthorization();
|
||||
|
||||
// HTTP clients.
|
||||
Services.AddHttpClient<IFileDownloader, HttpClientFileDownloader>();
|
||||
|
||||
// Add selectors.
|
||||
foreach (var httpCorrelationIdSelectorType in HttpCorrelationIdSelectorTypes)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Http.Bookmarks;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows.Core.Helpers;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using System.Net;
|
|||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.Bookmarks;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.State;
|
||||
using Elsa.Workflows.Runtime.Filters;
|
||||
|
|
|
|||
45
src/modules/Elsa.Http/Models/Downloadable.cs
Normal file
45
src/modules/Elsa.Http/Models/Downloadable.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Http.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a downloadable object.
|
||||
/// </summary>
|
||||
public class Downloadable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Downloadable"/> class.
|
||||
/// </summary>
|
||||
[JsonConstructor]
|
||||
public Downloadable()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Downloadable"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to download.</param>
|
||||
/// <param name="filename">The filename to use when downloading the stream.</param>
|
||||
/// <param name="contentType">The content type to use when downloading the stream.</param>
|
||||
public Downloadable(Stream stream, string? filename = default, string? contentType = default)
|
||||
{
|
||||
Stream = stream;
|
||||
Filename = filename;
|
||||
ContentType = contentType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The stream to download.
|
||||
/// </summary>
|
||||
public Stream Stream { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The filename to use when downloading the stream.
|
||||
/// </summary>
|
||||
public string? Filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content type to use when downloading the stream.
|
||||
/// </summary>
|
||||
public string? ContentType { get; set; }
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
using Elsa.Workflows.Core.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Http.Models;
|
||||
|
||||
public record HttpEndpointBookmarkPayload
|
||||
{
|
||||
private readonly string _path = default!;
|
||||
private readonly string _method = default!;
|
||||
|
||||
[JsonConstructor]
|
||||
public HttpEndpointBookmarkPayload()
|
||||
{
|
||||
}
|
||||
|
||||
public HttpEndpointBookmarkPayload(string path, string method,
|
||||
bool? authorize = default, string? policy = default)
|
||||
{
|
||||
Path = path;
|
||||
Method = method;
|
||||
Authorize = authorize;
|
||||
Policy = policy;
|
||||
}
|
||||
|
||||
public string Path
|
||||
{
|
||||
get => _path;
|
||||
init => _path = value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string Method
|
||||
{
|
||||
get => _method;
|
||||
init => _method = value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
[ExcludeFromHash]
|
||||
public string? Policy { get; set; }
|
||||
|
||||
[ExcludeFromHash]
|
||||
public bool? Authorize { get; set; }
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ internal class WriteHttpResponseContentTypeOptionsProvider : IActivityPropertyOp
|
|||
{
|
||||
var options = new Dictionary<string, object>
|
||||
{
|
||||
["AvailableContentTypes"] = new[] { "" }.Concat(_options.AvailableContentTypes)
|
||||
["items"] = new[] { "" }.Concat(_options.AvailableContentTypes)
|
||||
};
|
||||
|
||||
return new(options);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,48 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.JavaScript.Notifications;
|
||||
using Elsa.JavaScript.TypeDefinitions.Contracts;
|
||||
using Elsa.JavaScript.TypeDefinitions.Models;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Http.Scripting.JavaScript;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the JavaScript engine with workflow input getters.
|
||||
/// Configures the JavaScript engine with additional .NET types that can be instantiated.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public class HttpJavaScriptHandler : INotificationHandler<EvaluatingJavaScript>
|
||||
public class HttpJavaScriptHandler : INotificationHandler<EvaluatingJavaScript>, ITypeDefinitionProvider
|
||||
{
|
||||
private readonly ITypeDescriber _typeDescriber;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpJavaScriptHandler"/> class.
|
||||
/// </summary>
|
||||
public HttpJavaScriptHandler(ITypeDescriber typeDescriber)
|
||||
{
|
||||
_typeDescriber = typeDescriber;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var engine = notification.Engine;
|
||||
engine.RegisterType<HttpRequestHeaders>();
|
||||
engine.RegisterType<Downloadable>();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IEnumerable<TypeDefinition>> GetTypeDefinitionsAsync(TypeDefinitionContext context)
|
||||
{
|
||||
var definitions = GetTypeDefinitions(context);
|
||||
return new(definitions);
|
||||
}
|
||||
|
||||
private IEnumerable<TypeDefinition> GetTypeDefinitions(TypeDefinitionContext context)
|
||||
{
|
||||
yield return _typeDescriber.DescribeType(typeof(HttpRequestHeaders));
|
||||
yield return _typeDescriber.DescribeType(typeof(Downloadable));
|
||||
}
|
||||
}
|
||||
33
src/modules/Elsa.Http/Services/DefaultDownloadableManager.cs
Normal file
33
src/modules/Elsa.Http/Services/DefaultDownloadableManager.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class DefaultDownloadableManager : IDownloadableManager
|
||||
{
|
||||
private readonly IEnumerable<IDownloadableProvider> _providers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultDownloadableManager"/> class.
|
||||
/// </summary>
|
||||
public DefaultDownloadableManager(IEnumerable<IDownloadableProvider> providers)
|
||||
{
|
||||
_providers = providers.OrderBy(x => x.Priority).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IEnumerable<Downloadable>> GetDownloadablesAsync(object content, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var provider = _providers.FirstOrDefault(x => x.GetSupportsContent(content));
|
||||
|
||||
if (provider == null)
|
||||
return Enumerable.Empty<Downloadable>();
|
||||
|
||||
var context = new DownloadableContext(this, content, cancellationToken);
|
||||
var downloadables = await provider.GetDownloadablesAsync(context);
|
||||
|
||||
return downloadables;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Common.Models;
|
||||
using Elsa.Http.Bookmarks;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.Workflows.Core.Helpers;
|
||||
|
|
@ -39,7 +40,7 @@ public class HttpBookmarkProcessor : IHttpBookmarkProcessor
|
|||
_workflowInstanceManager = workflowInstanceManager;
|
||||
_workflowStateMapper = workflowStateMapper;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<WorkflowState>> ProcessBookmarks(
|
||||
IEnumerable<WorkflowExecutionResult> executionResults,
|
||||
|
|
@ -53,18 +54,13 @@ public class HttpBookmarkProcessor : IHttpBookmarkProcessor
|
|||
throw new Exception("Invalid use of this method, because there is no HTTP context");
|
||||
|
||||
// We must assume that the workflow executed in a different process (when e.g. using Proto.Actor)
|
||||
// and check if we received any `HttpEndpoint` or `WriteHttpResponse` activity bookmarks.
|
||||
// and check if we received any HTTP-related activity bookmarks.
|
||||
// If we did, acquire a lock on the workflow instance and resume it from here within an actual HTTP context so that the activity can complete its HTTP response.
|
||||
var httpEndpointTypeName = ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>();
|
||||
var writeHttpResponseTypeName = ActivityTypeNameHelper.GenerateTypeName<WriteHttpResponse>();
|
||||
|
||||
var query =
|
||||
from executionResult in executionResults
|
||||
from bookmark in executionResult.Bookmarks
|
||||
where bookmark.Name == writeHttpResponseTypeName
|
||||
|| (bookmark.Name == httpEndpointTypeName
|
||||
// If a "regular" HttpEndpointBookmarkPayload bookmark was created, we do not want to resume that - it's a bookmark that was created to block the workflow until the desired HTTP request comes in.
|
||||
&& bookmark.Payload is not HttpEndpointBookmarkPayload)
|
||||
where bookmark.Metadata?.ContainsKey(BookmarkMetadata.HttpCrossBoundaryMetadataKey) == true
|
||||
select (InstanceId: executionResult.WorkflowInstanceId, bookmark.Id);
|
||||
|
||||
var workflowExecutionResults = new Stack<(string InstanceId, string BookmarkId)>(query);
|
||||
|
|
@ -97,11 +93,11 @@ public class HttpBookmarkProcessor : IHttpBookmarkProcessor
|
|||
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(
|
||||
workflowDefinition,
|
||||
cancellationToken);
|
||||
|
||||
|
||||
var workflowHost = await _workflowHostFactory.CreateAsync(workflow, workflowState, cancellationToken);
|
||||
var options = new ResumeWorkflowHostOptions(correlationId, result.BookmarkId, Input: input);
|
||||
await workflowHost.ResumeWorkflowAsync(options, cancellationToken);
|
||||
|
||||
|
||||
// Import the updated workflow state into the runtime.
|
||||
workflowState = workflowHost.WorkflowState;
|
||||
await _workflowRuntime.ImportWorkflowStateAsync(workflowState, cancellationToken);
|
||||
|
|
@ -109,7 +105,7 @@ public class HttpBookmarkProcessor : IHttpBookmarkProcessor
|
|||
}
|
||||
|
||||
// Save the updated workflow states.
|
||||
foreach (var workflowInstance in workflowStates.Select(workflowState => _workflowStateMapper.Map(workflowState)!))
|
||||
foreach (var workflowInstance in workflowStates.Select(workflowState => _workflowStateMapper.Map(workflowState)!))
|
||||
await _workflowInstanceManager.SaveAsync(workflowInstance, cancellationToken);
|
||||
|
||||
return workflowStates;
|
||||
|
|
|
|||
25
src/modules/Elsa.Http/Services/HttpClientFileDownloader.cs
Normal file
25
src/modules/Elsa.Http/Services/HttpClientFileDownloader.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using Elsa.Http.Contracts;
|
||||
|
||||
namespace Elsa.Http.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A general-purpose downloader of files from a given URL that uses <see cref="HttpClient"/>.
|
||||
/// </summary>
|
||||
public class HttpClientFileDownloader : IFileDownloader
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClientFileDownloader"/> class.
|
||||
/// </summary>
|
||||
public HttpClientFileDownloader(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<HttpResponseMessage> DownloadAsync(Uri url, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _httpClient.GetAsync(url, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,35 +19,37 @@ internal class BookmarkMapper
|
|||
{
|
||||
_bookmarkPayloadSerializer = bookmarkPayloadSerializer;
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<ProtoBookmark> Map(IEnumerable<Bookmark> source) =>
|
||||
source.Select(x =>
|
||||
|
||||
|
||||
public IEnumerable<ProtoBookmark> Map(IEnumerable<Bookmark> source) =>
|
||||
source.Select(bookmark =>
|
||||
new ProtoBookmark
|
||||
{
|
||||
Id = x.Id,
|
||||
Name = x.Name,
|
||||
Hash = x.Hash,
|
||||
Payload = x.Payload != null ? _bookmarkPayloadSerializer.Serialize(x.Payload) : string.Empty,
|
||||
ActivityId = x.ActivityId,
|
||||
ActivityNodeId = x.ActivityNodeId,
|
||||
ActivityInstanceId = x.ActivityInstanceId,
|
||||
AutoBurn = x.AutoBurn,
|
||||
CallbackMethodName = x.CallbackMethodName.EmptyIfNull(),
|
||||
CreatedAt = x.CreatedAt.ToString("O")
|
||||
Id = bookmark.Id,
|
||||
Name = bookmark.Name,
|
||||
Hash = bookmark.Hash,
|
||||
Payload = bookmark.Payload != null ? _bookmarkPayloadSerializer.Serialize(bookmark.Payload) : string.Empty,
|
||||
ActivityId = bookmark.ActivityId,
|
||||
ActivityNodeId = bookmark.ActivityNodeId,
|
||||
ActivityInstanceId = bookmark.ActivityInstanceId,
|
||||
AutoBurn = bookmark.AutoBurn,
|
||||
CallbackMethodName = bookmark.CallbackMethodName.EmptyIfNull(),
|
||||
CreatedAt = bookmark.CreatedAt.ToString("O"),
|
||||
Metadata = { bookmark.Metadata ?? new Dictionary<string, string>() }
|
||||
});
|
||||
|
||||
|
||||
public IEnumerable<Bookmark> Map(IEnumerable<ProtoBookmark> source) =>
|
||||
source.Select(x =>
|
||||
source.Select(bookmark =>
|
||||
new Bookmark(
|
||||
x.Id,
|
||||
x.Name,
|
||||
x.Hash,
|
||||
x.Payload.NullIfEmpty(),
|
||||
x.ActivityId,
|
||||
x.ActivityNodeId,
|
||||
x.ActivityInstanceId,
|
||||
DateTimeOffset.Parse(x.CreatedAt),
|
||||
x.AutoBurn,
|
||||
x.CallbackMethodName.NullIfEmpty()));
|
||||
bookmark.Id,
|
||||
bookmark.Name,
|
||||
bookmark.Hash,
|
||||
bookmark.Payload.NullIfEmpty(),
|
||||
bookmark.ActivityId,
|
||||
bookmark.ActivityNodeId,
|
||||
bookmark.ActivityInstanceId,
|
||||
DateTimeOffset.Parse(bookmark.CreatedAt),
|
||||
bookmark.AutoBurn,
|
||||
bookmark.CallbackMethodName.NullIfEmpty(),
|
||||
bookmark.Metadata.ToDictionary(x => x.Key, x => x.Value)));
|
||||
}
|
||||
|
|
@ -93,7 +93,8 @@ message Bookmark {
|
|||
string ActivityId = 5;
|
||||
string ActivityNodeId = 6;
|
||||
string ActivityInstanceId = 7;
|
||||
optional bool AutoBurn = 8;
|
||||
optional string CallbackMethodName = 9;
|
||||
string CreatedAt = 10; // ISO 8601
|
||||
map<string, string> Metadata = 8;
|
||||
optional bool AutoBurn = 9;
|
||||
optional string CallbackMethodName = 10;
|
||||
string CreatedAt = 11; // ISO 8601
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ public class CallAnswered : Activity<CallAnsweredPayload>
|
|||
foreach (var callControlId in callControlIds)
|
||||
{
|
||||
var payload = new CallAnsweredBookmarkPayload(callControlId);
|
||||
context.CreateBookmark(new BookmarkOptions(payload, Resume, Type, IncludeActivityInstanceId: false));
|
||||
context.CreateBookmark(new CreateBookmarkArgs(payload, Resume, Type, IncludeActivityInstanceId: false));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class CallHangup : Activity<CallHangupPayload>
|
|||
foreach (var callControlId in callControlIds)
|
||||
{
|
||||
var payload = new CallHangupBookmarkPayload(callControlId);
|
||||
context.CreateBookmark(new BookmarkOptions(payload, Resume, Type, IncludeActivityInstanceId: false));
|
||||
context.CreateBookmark(new CreateBookmarkArgs(payload, Resume, Type, IncludeActivityInstanceId: false));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class WebhookEvent : Activity<Payload>
|
|||
var eventType = EventType;
|
||||
var payload = new WebhookEventBookmarkPayload(eventType);
|
||||
|
||||
context.CreateBookmark(new BookmarkOptions(payload, Resume, Type, false));
|
||||
context.CreateBookmark(new CreateBookmarkArgs(payload, Resume, Type, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ public class ActivityExecutionContext : IExecutionContext
|
|||
public void CreateBookmarks(IEnumerable<object> payloads, ExecuteActivityDelegate? callback = default, bool includeActivityInstanceId = true)
|
||||
{
|
||||
foreach (var payload in payloads)
|
||||
CreateBookmark(new BookmarkOptions(payload, callback, IncludeActivityInstanceId: includeActivityInstanceId));
|
||||
CreateBookmark(new CreateBookmarkArgs(payload, callback, IncludeActivityInstanceId: includeActivityInstanceId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -295,8 +295,12 @@ public class ActivityExecutionContext : IExecutionContext
|
|||
/// Creates a bookmark so that this activity can be resumed at a later time.
|
||||
/// </summary>
|
||||
/// <param name="callback">An optional callback that is invoked when the bookmark is resumed.</param>
|
||||
/// <param name="metadata">Custom properties to associate with the bookmark.</param>
|
||||
/// <returns>The created bookmark.</returns>
|
||||
public Bookmark CreateBookmark(ExecuteActivityDelegate callback) => CreateBookmark(new BookmarkOptions(default, callback));
|
||||
public Bookmark CreateBookmark(ExecuteActivityDelegate callback, IDictionary<string, string>? metadata = default)
|
||||
{
|
||||
return CreateBookmark(new CreateBookmarkArgs(default, callback, Metadata: metadata));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bookmark so that this activity can be resumed at a later time.
|
||||
|
|
@ -304,21 +308,29 @@ public class ActivityExecutionContext : IExecutionContext
|
|||
/// <param name="payload">The payload to associate with the bookmark.</param>
|
||||
/// <param name="callback">An optional callback that is invoked when the bookmark is resumed.</param>
|
||||
/// <param name="includeActivityInstanceId">Whether or not the activity instance ID should be included in the bookmark payload.</param>
|
||||
/// <param name="customProperties">Custom properties to associate with the bookmark.</param>
|
||||
/// <returns>The created bookmark.</returns>
|
||||
public Bookmark CreateBookmark(object payload, ExecuteActivityDelegate callback, bool includeActivityInstanceId = true) => CreateBookmark(new BookmarkOptions(payload, callback, IncludeActivityInstanceId: includeActivityInstanceId));
|
||||
public Bookmark CreateBookmark(object payload, ExecuteActivityDelegate callback, bool includeActivityInstanceId = true, IDictionary<string, string>? customProperties = default)
|
||||
{
|
||||
return CreateBookmark(new CreateBookmarkArgs(payload, callback, IncludeActivityInstanceId: includeActivityInstanceId, Metadata: customProperties));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bookmark so that this activity can be resumed at a later time.
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload to associate with the bookmark.</param>
|
||||
/// <param name="metadata">Custom properties to associate with the bookmark.</param>
|
||||
/// <returns>The created bookmark.</returns>
|
||||
public Bookmark CreateBookmark(object payload) => CreateBookmark(new BookmarkOptions(payload));
|
||||
public Bookmark CreateBookmark(object payload, IDictionary<string, string>? metadata = default)
|
||||
{
|
||||
return CreateBookmark(new CreateBookmarkArgs(payload, Metadata: metadata));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bookmark so that this activity can be resumed at a later time.
|
||||
/// Creating a bookmark will automatically suspend the workflow after all pending activities have executed.
|
||||
/// </summary>
|
||||
public Bookmark CreateBookmark(BookmarkOptions? options = default)
|
||||
public Bookmark CreateBookmark(CreateBookmarkArgs? options = default)
|
||||
{
|
||||
var payload = options?.Payload;
|
||||
var callback = options?.Callback;
|
||||
|
|
@ -338,7 +350,8 @@ public class ActivityExecutionContext : IExecutionContext
|
|||
Id,
|
||||
_systemClock.UtcNow,
|
||||
options?.AutoBurn ?? true,
|
||||
callback?.Method.Name);
|
||||
callback?.Method.Name,
|
||||
options?.Metadata);
|
||||
|
||||
AddBookmark(bookmark);
|
||||
return bookmark;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace Elsa.Workflows.Core.Models;
|
|||
/// <param name="CreatedAt">The date and time the bookmark was created.</param>
|
||||
/// <param name="AutoBurn">Whether or not the bookmark should be automatically burned.</param>
|
||||
/// <param name="CallbackMethodName">The name of the method on the activity class to invoke when the bookmark is resumed.</param>
|
||||
/// <param name="Metadata">Custom properties associated with the bookmark.</param>
|
||||
public record Bookmark(
|
||||
string Id,
|
||||
string Name,
|
||||
|
|
@ -25,12 +26,12 @@ public record Bookmark(
|
|||
string ActivityInstanceId,
|
||||
DateTimeOffset CreatedAt,
|
||||
bool AutoBurn = true,
|
||||
string? CallbackMethodName = default
|
||||
)
|
||||
string? CallbackMethodName = default,
|
||||
IDictionary<string, string>? Metadata = default)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonConstructor]
|
||||
public Bookmark() : this("", "", "", null, "", "", "", default)
|
||||
public Bookmark() : this("", "", "", null, "", "", "", default, default)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -10,4 +10,10 @@ namespace Elsa.Workflows.Core.Models;
|
|||
/// <param name="BookmarkName">An optional name to associate with the bookmark.</param>
|
||||
/// <param name="AutoBurn">Whether or not the bookmark should be automatically burned when triggered.</param>
|
||||
/// <param name="IncludeActivityInstanceId">Whether or not the activity instance ID should be included in the bookmark payload.</param>
|
||||
public record BookmarkOptions(object? Payload = default, ExecuteActivityDelegate? Callback = default, string? BookmarkName = default, bool AutoBurn = true, bool IncludeActivityInstanceId = true);
|
||||
public record CreateBookmarkArgs(
|
||||
object? Payload = default,
|
||||
ExecuteActivityDelegate? Callback = default,
|
||||
string? BookmarkName = default,
|
||||
bool AutoBurn = true,
|
||||
bool IncludeActivityInstanceId = true,
|
||||
IDictionary<string, string>? Metadata = default);
|
||||
|
|
@ -65,7 +65,7 @@ public class DispatchWorkflow : Activity<object>
|
|||
// If we need to wait for the child workflow to complete, create a bookmark.
|
||||
if (waitForCompletion)
|
||||
{
|
||||
var bookmarkOptions = new BookmarkOptions
|
||||
var bookmarkOptions = new CreateBookmarkArgs
|
||||
{
|
||||
Callback = OnChildWorkflowCompletedAsync,
|
||||
Payload = new DispatchWorkflowBookmark(instanceId),
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ public class Event : Trigger<object?>
|
|||
|
||||
if (!context.IsTriggerOfWorkflow())
|
||||
{
|
||||
var options = new BookmarkOptions
|
||||
var options = new CreateBookmarkArgs
|
||||
{
|
||||
Payload = new EventBookmarkPayload(eventName),
|
||||
IncludeActivityInstanceId = false
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ public class StoredBookmark
|
|||
DateTimeOffset createdAt,
|
||||
string? activityInstanceId = default,
|
||||
string? correlationId = default,
|
||||
object? payload = default)
|
||||
object? payload = default,
|
||||
IDictionary<string, string>? metadata = default)
|
||||
{
|
||||
BookmarkId = bookmarkId;
|
||||
ActivityTypeName = activityTypeName;
|
||||
|
|
@ -28,6 +29,7 @@ public class StoredBookmark
|
|||
ActivityInstanceId = activityInstanceId;
|
||||
CorrelationId = correlationId;
|
||||
Payload = payload;
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -73,6 +75,11 @@ public class StoredBookmark
|
|||
/// </summary>
|
||||
public object? Payload { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom properties associated with the bookmark.
|
||||
/// </summary>
|
||||
public IDictionary<string, string>? Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date and time the bookmark was created.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ public class BackgroundActivityInvokerMiddleware : DefaultActivityInvokerMiddlew
|
|||
var workflowInstanceId = context.WorkflowExecutionContext.Id;
|
||||
var activityNodeId = context.NodeId;
|
||||
var bookmarkPayload = new BackgroundActivityBookmark();
|
||||
var bookmarkOptions = new BookmarkOptions { BookmarkName = BackgroundActivityBookmarkName, Payload = bookmarkPayload };
|
||||
var bookmarkOptions = new CreateBookmarkArgs { BookmarkName = BackgroundActivityBookmarkName, Payload = bookmarkPayload };
|
||||
var bookmark = context.CreateBookmark(bookmarkOptions);
|
||||
scheduledBackgroundActivities.Add(new ScheduledBackgroundActivity(workflowInstanceId, activityNodeId, bookmark.Id));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env zsh
|
||||
|
||||
# Define the modules to update
|
||||
mods=("Management")
|
||||
mods=("Runtime")
|
||||
|
||||
# Define the list of providers
|
||||
providers=("MySql" "SqlServer" "Sqlite" "PostgreSql")
|
||||
|
|
|
|||
Loading…
Reference in a new issue