Merge remote-tracking branch 'origin/enh/5601'
This commit is contained in:
commit
72c3d2ca84
|
|
@ -6,7 +6,7 @@
|
|||
<ItemGroup>
|
||||
<PackageVersion Include="Antlr4.Runtime.Standard" Version="4.13.1" />
|
||||
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.11.3" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.11.4" />
|
||||
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.17.5" />
|
||||
<PackageVersion Include="Azure.ResourceManager" Version="1.12.0" />
|
||||
<PackageVersion Include="Azure.ResourceManager.AppContainers" Version="1.1.1" />
|
||||
|
|
@ -23,9 +23,9 @@
|
|||
<PackageVersion Include="DistributedLock.Postgres" Version="1.1.0" />
|
||||
<PackageVersion Include="DistributedLock.Redis" Version="1.0.3" />
|
||||
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="8.14.0" />
|
||||
<PackageVersion Include="Elsa.Studio" Version="3.2.0-preview.310" />
|
||||
<PackageVersion Include="Elsa.Studio.Core.BlazorWasm" Version="3.2.0-preview.310" />
|
||||
<PackageVersion Include="Elsa.Studio.Login.BlazorWasm" Version="3.2.0-preview.310" />
|
||||
<PackageVersion Include="Elsa.Studio" Version="3.2.0-rc2.351" />
|
||||
<PackageVersion Include="Elsa.Studio.Core.BlazorWasm" Version="3.2.0-rc2.351" />
|
||||
<PackageVersion Include="Elsa.Studio.Login.BlazorWasm" Version="3.2.0-rc2.351" />
|
||||
<PackageVersion Include="FastEndpoints" Version="5.26.0" />
|
||||
<PackageVersion Include="FastEndpoints.Security" Version="5.26.0" />
|
||||
<PackageVersion Include="FastEndpoints.Swagger" Version="5.26.0" />
|
||||
|
|
|
|||
293
src/modules/Elsa.Http/Activities/DownloadHttpFile.cs
Normal file
293
src/modules/Elsa.Http/Activities/DownloadHttpFile.cs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.Models;
|
||||
using Elsa.Http.UIHints;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Attributes;
|
||||
using Elsa.Workflows.Contracts;
|
||||
using Elsa.Workflows.UIHints;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using HttpHeaders = Elsa.Http.Models.HttpHeaders;
|
||||
|
||||
namespace Elsa.Http;
|
||||
|
||||
/// <summary>
|
||||
/// An activity that downloads a file from a given URL.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "HTTP", "Downloads a file from a given URL.", DisplayName = "Download File", Kind = ActivityKind.Task)]
|
||||
[Output(IsSerializable = false)]
|
||||
public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValueProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public DownloadHttpFile([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The URL to download the file from.
|
||||
/// </summary>
|
||||
[Input(DisplayName = "URL", Description = "The URL to download the file from.")]
|
||||
public Input<Uri?> Url { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP method to use when sending the request.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "The HTTP method to use when sending the request.",
|
||||
Options = new[]
|
||||
{
|
||||
"GET", "POST", "PUT"
|
||||
},
|
||||
DefaultValue = "GET",
|
||||
UIHint = InputUIHints.DropDown
|
||||
)]
|
||||
public Input<string> Method { get; set; } = new("GET");
|
||||
|
||||
/// <summary>
|
||||
/// A list of expected status codes to handle.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "A list of expected status codes to handle.",
|
||||
UIHint = InputUIHints.MultiText,
|
||||
DefaultValueProvider = typeof(FlowSendHttpRequest)
|
||||
)]
|
||||
public Input<ICollection<int>> ExpectedStatusCodes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The content to send with the request. Can be a string, an object, a byte array or a stream.
|
||||
/// </summary>
|
||||
[Input(Name = "Content", Description = "The content to send with the request. Can be a string, an object, a byte array or a stream.")]
|
||||
public Input<object?> RequestContent { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The content type to use when sending the request.
|
||||
/// </summary>
|
||||
[Input(
|
||||
DisplayName = "Content Type",
|
||||
Description = "The content type to use when sending the request.",
|
||||
UIHandler = typeof(HttpContentTypeOptionsProvider),
|
||||
UIHint = InputUIHints.DropDown
|
||||
)]
|
||||
public Input<string?> RequestContentType { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The Authorization header value to send with the request.
|
||||
/// </summary>
|
||||
/// <example>Bearer {some-access-token}</example>
|
||||
[Input(Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}", Category = "Security")]
|
||||
public Input<string?> Authorization { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// A value that allows to add the Authorization header without validation.
|
||||
/// </summary>
|
||||
[Input(Description = "A value that allows to add the Authorization header without validation.", Category = "Security")]
|
||||
public Input<bool> DisableAuthorizationHeaderValidation { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The headers to send along with the request.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "The headers to send along with the request.",
|
||||
UIHint = InputUIHints.JsonEditor,
|
||||
Category = "Advanced"
|
||||
)]
|
||||
public Input<HttpHeaders?> RequestHeaders { get; set; } = new(new HttpHeaders());
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP response.
|
||||
/// </summary>
|
||||
[Output(IsSerializable = false)]
|
||||
public Output<HttpResponseMessage> Response { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP response status code
|
||||
/// </summary>
|
||||
[Output(Description = "The HTTP response status code")]
|
||||
public Output<int> StatusCode { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The downloaded content stream, if any.
|
||||
/// </summary>
|
||||
[Output(Description = "The downloaded content stream, if any.", IsSerializable = false)]
|
||||
public Output<Stream?> ResponseContentStream { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The downloaded content bytes, if any.
|
||||
/// </summary>
|
||||
[Output(Description = "The downloaded content bytes, if any.", IsSerializable = false)]
|
||||
public Output<byte[]?> ResponseContentBytes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The response headers that were received.
|
||||
/// </summary>
|
||||
[Output(Description = "The response headers that were received.")]
|
||||
public Output<HttpHeaders?> ResponseHeaders { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The response content headers that were received.
|
||||
/// </summary>
|
||||
[Output(DisplayName = "Content Headers", Description = "The response content headers that were received.")]
|
||||
public Output<HttpHeaders?> ResponseContentHeaders { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
await TrySendAsync(context);
|
||||
}
|
||||
|
||||
private async Task TrySendAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var request = PrepareRequest(context);
|
||||
var logger = (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType()));
|
||||
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
|
||||
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase));
|
||||
var cancellationToken = context.CancellationToken;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
var file = await GetFileFromResponse(context, response, request);
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var responseHeaders = new HttpHeaders(response.Headers);
|
||||
var responseContentHeaders = new HttpHeaders(response.Content.Headers);
|
||||
|
||||
context.Set(Response, response);
|
||||
context.Set(ResponseContentStream, file?.Stream);
|
||||
context.Set(Result, file);
|
||||
context.Set(StatusCode, statusCode);
|
||||
context.Set(ResponseHeaders, responseHeaders);
|
||||
context.Set(ResponseContentHeaders, responseContentHeaders);
|
||||
if (ResponseContentBytes.HasTarget(context)) context.Set(ResponseContentBytes, file?.GetBytes());
|
||||
|
||||
await HandleResponseAsync(context, response);
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
logger.LogWarning(e, "An error occurred while sending an HTTP request");
|
||||
context.AddExecutionLogEntry("Error", e.Message, payload: new
|
||||
{
|
||||
StackTrace = e.StackTrace
|
||||
});
|
||||
context.JournalData.Add("Error", e.Message);
|
||||
await HandleRequestExceptionAsync(context, e);
|
||||
}
|
||||
catch (TaskCanceledException e)
|
||||
{
|
||||
logger.LogWarning(e, "An error occurred while sending an HTTP request");
|
||||
context.AddExecutionLogEntry("Error", e.Message, payload: new
|
||||
{
|
||||
StackTrace = e.StackTrace
|
||||
});
|
||||
context.JournalData.Add("Cancelled", true);
|
||||
await HandleTaskCanceledExceptionAsync(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the response.
|
||||
/// </summary>
|
||||
private async Task HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
|
||||
{
|
||||
var expectedStatusCodes = ExpectedStatusCodes.GetOrDefault(context) ?? new List<int>(0);
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode);
|
||||
var outcome = expectedStatusCodes.Any() ? hasMatchingStatusCode ? statusCode.ToString() : "Unmatched status code" : default;
|
||||
var outcomes = new List<string>();
|
||||
|
||||
if (outcome != null)
|
||||
outcomes.Add(outcome);
|
||||
|
||||
outcomes.Add("Done");
|
||||
await context.CompleteActivityWithOutcomesAsync(outcomes.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an exception that occurred while sending the request.
|
||||
/// </summary>
|
||||
private async Task HandleRequestExceptionAsync(ActivityExecutionContext context, HttpRequestException exception)
|
||||
{
|
||||
await context.CompleteActivityWithOutcomesAsync("Failed to connect");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="TaskCanceledException"/> that occurred while sending the request.
|
||||
/// </summary>
|
||||
private async Task HandleTaskCanceledExceptionAsync(ActivityExecutionContext context, TaskCanceledException exception)
|
||||
{
|
||||
await context.CompleteActivityWithOutcomesAsync("Timeout");
|
||||
}
|
||||
|
||||
private async Task<HttpFile?> GetFileFromResponse(ActivityExecutionContext context, HttpResponseMessage httpResponse, HttpRequestMessage httpRequestMessage)
|
||||
{
|
||||
var httpContent = httpResponse.Content;
|
||||
if (!HasContent(httpContent))
|
||||
return null;
|
||||
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken);
|
||||
var responseHeaders = httpResponse.Headers;
|
||||
var contentHeaders = httpContent.Headers;
|
||||
var contentType = contentHeaders.ContentType?.MediaType!;
|
||||
var filename = contentHeaders.ContentDisposition?.FileName ?? httpRequestMessage.RequestUri!.Segments.LastOrDefault() ?? "file.dat";
|
||||
var eTag = responseHeaders.ETag?.Tag;
|
||||
|
||||
return new HttpFile(contentStream, filename, contentType, eTag);
|
||||
}
|
||||
|
||||
private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0;
|
||||
|
||||
private HttpRequestMessage PrepareRequest(ActivityExecutionContext context)
|
||||
{
|
||||
var method = Method.GetOrDefault(context) ?? "GET";
|
||||
var url = Url.Get(context);
|
||||
var request = new HttpRequestMessage(new HttpMethod(method), url);
|
||||
var headers = context.GetHeaders(RequestHeaders);
|
||||
var authorization = Authorization.GetOrDefault(context);
|
||||
var addAuthorizationWithoutValidation = DisableAuthorizationHeaderValidation.GetOrDefault(context);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(authorization))
|
||||
if (addAuthorizationWithoutValidation)
|
||||
request.Headers.TryAddWithoutValidation("Authorization", authorization);
|
||||
else
|
||||
request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
|
||||
|
||||
foreach (var header in headers)
|
||||
request.Headers.Add(header.Key, header.Value.AsEnumerable());
|
||||
|
||||
var contentType = RequestContentType.GetOrDefault(context);
|
||||
var content = RequestContent.GetOrDefault(context);
|
||||
|
||||
if (contentType != null && content != null)
|
||||
{
|
||||
var factories = context.GetServices<IHttpContentFactory>();
|
||||
var factory = SelectContentWriter(contentType, factories);
|
||||
request.Content = factory.CreateHttpContent(content, contentType);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable<IHttpContentFactory> factories)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentType))
|
||||
return new JsonContentFactory();
|
||||
|
||||
var parsedContentType = new System.Net.Mime.ContentType(contentType);
|
||||
return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory();
|
||||
}
|
||||
|
||||
object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property)
|
||||
{
|
||||
if (property.Name == nameof(ExpectedStatusCodes))
|
||||
return new List<int>
|
||||
{
|
||||
200
|
||||
};
|
||||
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
|
|
@ -468,8 +468,9 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
var targetType = ParsedContent.GetTargetType(context);
|
||||
var contentStream = httpRequest.Body;
|
||||
var contentType = httpRequest.ContentType!;
|
||||
var headers = httpRequest.Headers.ToDictionary(x => x.Key, x => x.Value.ToArray());
|
||||
|
||||
return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken);
|
||||
return await context.ParseContentAsync(contentStream, contentType, targetType, headers, cancellationToken);
|
||||
}
|
||||
|
||||
private static bool HasContent(HttpRequest httpRequest) => httpRequest.Headers.ContentLength > 0;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
/// </summary>
|
||||
[Input(
|
||||
Description = "The HTTP method to use when sending the request.",
|
||||
Options = new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" },
|
||||
Options = new[]
|
||||
{
|
||||
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"
|
||||
},
|
||||
DefaultValue = "GET",
|
||||
UIHint = InputUIHints.DropDown
|
||||
)]
|
||||
|
|
@ -83,7 +86,7 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
/// </summary>
|
||||
[Output(Description = "The HTTP response status code")]
|
||||
public Output<int> StatusCode { get; set; } = default!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The parsed content, if any.
|
||||
/// </summary>
|
||||
|
|
@ -128,10 +131,10 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
try
|
||||
{
|
||||
var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
var parsedContent = await ParseContentAsync(context, response.Content);
|
||||
var parsedContent = await ParseContentAsync(context, response);
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var responseHeaders = new HttpHeaders(response.Headers);
|
||||
|
||||
|
||||
context.Set(Result, response);
|
||||
context.Set(ParsedContent, parsedContent);
|
||||
context.Set(StatusCode, statusCode);
|
||||
|
|
@ -142,28 +145,37 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
catch (HttpRequestException e)
|
||||
{
|
||||
logger.LogWarning(e, "An error occurred while sending an HTTP request");
|
||||
context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace });
|
||||
context.AddExecutionLogEntry("Error", e.Message, payload: new
|
||||
{
|
||||
StackTrace = e.StackTrace
|
||||
});
|
||||
context.JournalData.Add("Error", e.Message);
|
||||
await HandleRequestExceptionAsync(context, e);
|
||||
}
|
||||
catch (TaskCanceledException e)
|
||||
{
|
||||
logger.LogWarning(e, "An error occurred while sending an HTTP request");
|
||||
context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace });
|
||||
context.AddExecutionLogEntry("Error", e.Message, payload: new
|
||||
{
|
||||
StackTrace = e.StackTrace
|
||||
});
|
||||
context.JournalData.Add("Cancelled", true);
|
||||
await HandleTaskCanceledExceptionAsync(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent)
|
||||
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpResponseMessage httpResponse)
|
||||
{
|
||||
var httpContent = httpResponse.Content;
|
||||
if (!HasContent(httpContent))
|
||||
return null;
|
||||
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var targetType = ParsedContent.GetTargetType(context);
|
||||
var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken);
|
||||
var contentType = httpContent.Headers.ContentType?.MediaType!;
|
||||
var responseHeaders = httpResponse.Headers;
|
||||
var contentHeaders = httpContent.Headers;
|
||||
var contentType = contentHeaders.ContentType?.MediaType!;
|
||||
|
||||
targetType ??= contentType switch
|
||||
{
|
||||
|
|
@ -171,7 +183,10 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
_ => typeof(string)
|
||||
};
|
||||
|
||||
return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken);
|
||||
var contentHeadersDictionary = contentHeaders.ToDictionary(x => x.Key, x => x.Value.Cast<string?>().ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
var responseHeadersDictionary = responseHeaders.ToDictionary(x => x.Key, x => x.Value.Cast<string?>().ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
var headersDictionary = contentHeadersDictionary.Concat(responseHeadersDictionary).ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
|
||||
return await context.ParseContentAsync(contentStream, contentType, targetType, headersDictionary, cancellationToken);
|
||||
}
|
||||
|
||||
private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Http.Contexts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the context in which an HTTP response is being parsed.
|
||||
/// </summary>
|
||||
public record HttpResponseParserContext(Stream Content, string ContentType, Type? ReturnType, IDictionary<string, string[]> Headers, CancellationToken CancellationToken);
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using Elsa.Http.Contexts;
|
||||
|
||||
namespace Elsa.Http.Contracts;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -14,10 +16,10 @@ public interface IHttpContentParser
|
|||
/// <summary>
|
||||
/// Returns a value indicating whether this reader supports the specified content type.
|
||||
/// </summary>
|
||||
bool GetSupportsContentType(string contentType);
|
||||
bool GetSupportsContentType(HttpResponseParserContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the specified <c>stream</c> and returns a parsed object of the specified type. If no type is specified, a string is returned.
|
||||
/// </summary>
|
||||
Task<object> ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken);
|
||||
Task<object> ReadAsync(HttpResponseParserContext context);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using Elsa.Http.Abstractions;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.DownloadableContentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handles content that represents a downloadable stream.
|
||||
/// </summary>
|
||||
public class HttpFileDownloadableContentHandler : DownloadableContentHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => content is HttpFile;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Downloadable GetDownloadable(DownloadableContext context)
|
||||
{
|
||||
var file = (HttpFile)context.Content;
|
||||
var stream = file.Stream;
|
||||
var fileName = file.Filename;
|
||||
var contentType = file.ContentType;
|
||||
return new Downloadable(stream, fileName, contentType);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ namespace Elsa.Http.DownloadableContentHandlers;
|
|||
public class MultiDownloadableContentHandler : DownloadableContentHandlerBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool GetSupportsContent(object content) => content is IEnumerable enumerable and not string;
|
||||
public override bool GetSupportsContent(object content) => content is IEnumerable and not string and not byte[];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IEnumerable<Func<ValueTask<Downloadable>>> GetDownloadablesAsync(DownloadableContext context)
|
||||
|
|
|
|||
30
src/modules/Elsa.Http/Extensions/HeadersExtensions.cs
Normal file
30
src/modules/Elsa.Http/Extensions/HeadersExtensions.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for HTTP headers.
|
||||
/// </summary>
|
||||
public static class HeadersExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the filename from the Content-Disposition header.
|
||||
/// </summary>
|
||||
public static string? GetFilename(this HttpResponseMessage response)
|
||||
{
|
||||
var dictionary = response.Headers.ToDictionary(x => x.Key, x => x.Value.ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
return dictionary.GetFilename();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filename from the Content-Disposition header.
|
||||
/// </summary>
|
||||
public static string? GetFilename(this IDictionary<string, string[]> headers)
|
||||
{
|
||||
if (!headers.TryGetValue("Content-Disposition", out var values))
|
||||
return null;
|
||||
|
||||
var contentDispositionString = string.Join("", values);
|
||||
var contentDisposition = new System.Net.Mime.ContentDisposition(contentDispositionString);
|
||||
return contentDisposition.FileName;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Models;
|
||||
|
|
@ -7,15 +8,16 @@ namespace Elsa.Extensions;
|
|||
|
||||
internal static class HttpActivityExecutionContextExtensions
|
||||
{
|
||||
public static async Task<object?> ParseContentAsync(this ActivityExecutionContext context, Stream content, string contentType, Type? returnType, CancellationToken cancellationToken)
|
||||
public static async Task<object?> ParseContentAsync(this ActivityExecutionContext context, Stream content, string contentType, Type? returnType, Dictionary<string, string?[]> headers, CancellationToken cancellationToken)
|
||||
{
|
||||
var parsers = context.GetServices<IHttpContentParser>().OrderByDescending(x => x.Priority).ToList();
|
||||
var contentParser = parsers.FirstOrDefault(x => x.GetSupportsContentType(contentType));
|
||||
var httpResponseParserContext = new HttpResponseParserContext(content, contentType, returnType, headers, cancellationToken);
|
||||
var contentParser = parsers.FirstOrDefault(x => x.GetSupportsContentType(httpResponseParserContext));
|
||||
|
||||
if (contentParser == null)
|
||||
return null;
|
||||
|
||||
return await contentParser.ReadAsync(content, returnType, cancellationToken);
|
||||
return await contentParser.ReadAsync(httpResponseParserContext);
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<string, string[]>> GetHeaders(this ActivityExecutionContext context, Input input)
|
||||
|
|
|
|||
|
|
@ -116,7 +116,9 @@ public class HttpFeature : FeatureBase
|
|||
typeof(HttpResponse),
|
||||
typeof(HttpResponseMessage),
|
||||
typeof(HttpHeaders),
|
||||
typeof(IFormFile)
|
||||
typeof(IFormFile),
|
||||
typeof(HttpFile),
|
||||
typeof(Downloadable)
|
||||
}, "HTTP");
|
||||
|
||||
management.AddActivitiesFrom<HttpFeature>();
|
||||
|
|
@ -162,8 +164,9 @@ public class HttpFeature : FeatureBase
|
|||
// Content parsers.
|
||||
.AddSingleton<IHttpContentParser, JsonHttpContentParser>()
|
||||
.AddSingleton<IHttpContentParser, XmlHttpContentParser>()
|
||||
.AddSingleton<IHttpContentParser, PlainTextHttpContentParser>()
|
||||
.AddSingleton<IHttpContentParser, PlainTextHttpContentParser>()
|
||||
.AddSingleton<IHttpContentParser, TextHtmlHttpContentParser>()
|
||||
.AddSingleton<IHttpContentParser, FileHttpContentParser>()
|
||||
|
||||
// HTTP content factories.
|
||||
.AddScoped<IHttpContentFactory, TextContentFactory>()
|
||||
|
|
@ -193,6 +196,7 @@ public class HttpFeature : FeatureBase
|
|||
.AddScoped<IDownloadableContentHandler, DownloadableDownloadableContentHandler>()
|
||||
.AddScoped<IDownloadableContentHandler, UrlDownloadableContentHandler>()
|
||||
.AddScoped<IDownloadableContentHandler, StringDownloadableContentHandler>()
|
||||
.AddScoped<IDownloadableContentHandler, HttpFileDownloadableContentHandler>()
|
||||
|
||||
// File caches.
|
||||
.AddScoped(FileCache)
|
||||
|
|
@ -220,6 +224,10 @@ public class HttpFeature : FeatureBase
|
|||
{
|
||||
options.AddTypeAlias<IFormFile>("FormFile");
|
||||
options.AddTypeAlias<IFormFile[]>("FormFile[]");
|
||||
options.AddTypeAlias<HttpFile>("HttpFile");
|
||||
options.AddTypeAlias<HttpFile[]>("HttpFile[]");
|
||||
options.AddTypeAlias<Downloadable>("Downloadable");
|
||||
options.AddTypeAlias<Downloadable[]>("Downloadable[]");
|
||||
});
|
||||
}
|
||||
}
|
||||
63
src/modules/Elsa.Http/Models/HttpFile.cs
Normal file
63
src/modules/Elsa.Http/Models/HttpFile.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Http.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a downloadable object.
|
||||
/// </summary>
|
||||
public class HttpFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Downloadable"/> class.
|
||||
/// </summary>
|
||||
[JsonConstructor]
|
||||
public HttpFile()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpFile"/> 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>
|
||||
/// <param name="eTag">The ETag to use when downloading the stream.</param>
|
||||
public HttpFile(Stream stream, string? filename = default, string? contentType = default, string? eTag = default)
|
||||
{
|
||||
Stream = stream;
|
||||
Filename = filename;
|
||||
ContentType = contentType;
|
||||
ETag = eTag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The file stream.
|
||||
/// </summary>
|
||||
public Stream Stream { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The filename.
|
||||
/// </summary>
|
||||
public string? Filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content type.
|
||||
/// </summary>
|
||||
public string? ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ETag.
|
||||
/// </summary>
|
||||
public string? ETag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file bytes.
|
||||
/// </summary>
|
||||
public byte[] GetBytes()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
if (Stream.CanSeek) Stream.Seek(0, SeekOrigin.Begin);
|
||||
Stream.CopyTo(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,13 @@ public class HttpHeaders : Dictionary<string, string[]>
|
|||
Add(item.Key, item.Value.ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpHeaders(HttpContentHeaders source)
|
||||
{
|
||||
foreach (var item in source)
|
||||
Add(item.Key, item.Value.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content type.
|
||||
/// </summary>
|
||||
|
|
|
|||
34
src/modules/Elsa.Http/Parsers/FileHttpContentParser.cs
Normal file
34
src/modules/Elsa.Http/Parsers/FileHttpContentParser.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Models;
|
||||
|
||||
namespace Elsa.Http.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Reads received file from the HTTP response, if any.
|
||||
/// </summary>
|
||||
public class FileHttpContentParser : IHttpContentParser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
// Lower priority than other parsers, so that they can be tried first.
|
||||
// If none of them can parse the content, this parser will be tried and interpret the content as a file.
|
||||
public int Priority => -100;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool GetSupportsContentType(HttpResponseParserContext context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<object> ReadAsync(HttpResponseParserContext context)
|
||||
{
|
||||
var stream = context.Content;
|
||||
var filename = context.Headers.GetFilename() ?? "file.dat";
|
||||
var contentType = context.ContentType;
|
||||
var eTag = context.Headers["ETag"].FirstOrDefault();
|
||||
var file = new HttpFile(stream, filename, contentType, eTag);
|
||||
return Task.FromResult<object>(file);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using System.Dynamic;
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
|
||||
|
|
@ -16,10 +17,10 @@ public class JsonHttpContentParser : IHttpContentParser
|
|||
public int Priority => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool GetSupportsContentType(string contentType) => contentType.Contains("json", StringComparison.InvariantCultureIgnoreCase);
|
||||
public bool GetSupportsContentType(HttpResponseParserContext context) => context.ContentType.Contains("json", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object> ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken)
|
||||
public async Task<object> ReadAsync(HttpResponseParserContext context)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
|
|
@ -28,8 +29,10 @@ public class JsonHttpContentParser : IHttpContentParser
|
|||
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
var content = context.Content;
|
||||
using var reader = new StreamReader(content, leaveOpen: true);
|
||||
var json = await reader.ReadToEndAsync();
|
||||
var returnType = context.ReturnType;
|
||||
|
||||
if(returnType == typeof(string))
|
||||
return json;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
|
||||
namespace Elsa.Http.Parsers;
|
||||
|
|
@ -11,11 +12,12 @@ public class PlainTextHttpContentParser : IHttpContentParser
|
|||
public int Priority => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool GetSupportsContentType(string contentType) => contentType.Contains("text/plain", StringComparison.InvariantCultureIgnoreCase);
|
||||
public bool GetSupportsContentType(HttpResponseParserContext context) => context.ContentType.Contains("text/plain", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object> ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken)
|
||||
public async Task<object> ReadAsync(HttpResponseParserContext context)
|
||||
{
|
||||
var content = context.Content;
|
||||
using var reader = new StreamReader(content, leaveOpen: true);
|
||||
var stringContent = await reader.ReadToEndAsync();
|
||||
return stringContent;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
using Elsa.Http.Contracts;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
|
||||
namespace Elsa.Http.Parsers;
|
||||
|
||||
namespace Elsa.Http.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Reads text/html content type streams.
|
||||
/// </summary>
|
||||
// TODO: found a library to use a Html Content Parser and use a complexe object Type, until this, this class allow to accept request send using text/html content-type
|
||||
/// </summary>
|
||||
// TODO: found a library to use a Html Content Parser and use a complexe object Type, until this, this class allow to accept request send using text/html content-type
|
||||
public class TextHtmlHttpContentParser : IHttpContentParser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int Priority => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool GetSupportsContentType(string contentType) => contentType.Contains("text/html", StringComparison.InvariantCultureIgnoreCase);
|
||||
public bool GetSupportsContentType(HttpResponseParserContext context) => context.ContentType.Contains("text/html", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object> ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken)
|
||||
{
|
||||
public async Task<object> ReadAsync(HttpResponseParserContext context)
|
||||
{
|
||||
var content = context.Content;
|
||||
using var reader = new StreamReader(content, leaveOpen: true);
|
||||
var stringContent = await reader.ReadToEndAsync();
|
||||
return stringContent;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Xml.Serialization;
|
||||
using Elsa.Http.Contexts;
|
||||
using Elsa.Http.Contracts;
|
||||
|
||||
namespace Elsa.Http.Parsers;
|
||||
|
|
@ -12,17 +13,19 @@ public class XmlHttpContentParser : IHttpContentParser
|
|||
public int Priority => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool GetSupportsContentType(string contentType) => contentType.Contains("xml", StringComparison.InvariantCultureIgnoreCase);
|
||||
public bool GetSupportsContentType(HttpResponseParserContext context) => context.ContentType.Contains("xml", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object> ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken)
|
||||
public async Task<object> ReadAsync(HttpResponseParserContext context)
|
||||
{
|
||||
var content = context.Content;
|
||||
using var reader = new StreamReader(content, leaveOpen: true);
|
||||
var xml = await reader.ReadToEndAsync();
|
||||
var returnType = context.ReturnType;
|
||||
|
||||
if (returnType == null || returnType == typeof(string))
|
||||
return xml;
|
||||
|
||||
|
||||
var serializer = new XmlSerializer(returnType);
|
||||
return serializer.Deserialize(reader)!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class DefaultDownloadableManager : IDownloadableManager
|
|||
var provider = _providers.FirstOrDefault(x => x.GetSupportsContent(content));
|
||||
|
||||
if (provider == null)
|
||||
return Enumerable.Empty<Func<ValueTask<Downloadable>>>();
|
||||
return [];
|
||||
|
||||
options ??= new();
|
||||
var context = new DownloadableContext(this, content, options, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Runtime.Interop;
|
||||
|
||||
namespace Elsa.JavaScript.ObjectConverters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte array to a <see cref="JsValue"/> instance representing a Uint8Array.
|
||||
/// </summary>
|
||||
internal class ByteArrayConverter : IObjectConverter
|
||||
{
|
||||
public bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result)
|
||||
{
|
||||
if (value is byte[] bytes)
|
||||
{
|
||||
// TODO: Temporary: Uint8Array creates a copy of the byte array. Instead, we want to create a view or a buffer referencing the byte array.
|
||||
// See also: https://github.com/sebastienros/jint/pull/1590
|
||||
result = engine.Intrinsics.Uint8Array.Construct(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = JsValue.Null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,13 @@ using Elsa.Extensions;
|
|||
using Elsa.JavaScript.Contracts;
|
||||
using Elsa.JavaScript.Helpers;
|
||||
using Elsa.JavaScript.Notifications;
|
||||
using Elsa.JavaScript.ObjectConverters;
|
||||
using Elsa.JavaScript.Options;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Humanizer;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Native.TypedArray;
|
||||
using Jint.Runtime.Interop;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
|
@ -27,23 +30,11 @@ namespace Elsa.JavaScript.Services;
|
|||
/// <summary>
|
||||
/// Provides a JavaScript evaluator using Jint.
|
||||
/// </summary>
|
||||
public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
||||
public class JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
|
||||
: IJavaScriptEvaluator
|
||||
{
|
||||
private readonly INotificationSender _mediator;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly JintOptions _jintOptions;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_memoryCache = memoryCache;
|
||||
_jintOptions = scriptOptions.Value;
|
||||
_configuration = configuration;
|
||||
}
|
||||
private readonly JintOptions _jintOptions = scriptOptions.Value;
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions();
|
||||
|
||||
/// <inheritdoc />
|
||||
[RequiresUnreferencedCode("The Jint library uses reflection and can't be statically analyzed.")]
|
||||
|
|
@ -79,6 +70,8 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
|
||||
return instance;
|
||||
});
|
||||
|
||||
engineOptions.Interop.ObjectConverters.Add(new ByteArrayConverter());
|
||||
|
||||
await _mediator.SendAsync(new CreatingJavaScriptEngine(engineOptions, context), cancellationToken);
|
||||
var engine = new Engine(engineOptions);
|
||||
|
|
@ -126,7 +119,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
|
||||
// Create configuration value accessor
|
||||
if (_jintOptions.AllowConfigurationAccess)
|
||||
engine.SetValue("getConfig", (Func<string, object?>)(name => _configuration.GetSection(name).Value));
|
||||
engine.SetValue("getConfig", (Func<string, object?>)(name => configuration.GetSection(name).Value));
|
||||
|
||||
// Add common .NET types.
|
||||
engine.RegisterType<DateTime>();
|
||||
|
|
@ -138,7 +131,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
_jintOptions.ConfigureEngineCallback(engine, context);
|
||||
|
||||
// Allow listeners invoked by the mediator to configure the engine.
|
||||
await _mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
|
||||
await mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
|
@ -162,6 +155,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
}
|
||||
}
|
||||
|
||||
[RequiresUnreferencedCode("Calls Jint.Engine.SetValue<T>(String, T)")]
|
||||
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
|
||||
{
|
||||
var variableNames = context.GetVariableNamesInScope().ToList();
|
||||
|
|
@ -187,7 +181,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
{
|
||||
var cacheKey = "jint:script:" + Hash(expression);
|
||||
|
||||
var parsedScript = _memoryCache.GetOrCreate(cacheKey, entry =>
|
||||
var parsedScript = memoryCache.GetOrCreate(cacheKey, entry =>
|
||||
{
|
||||
if (_jintOptions.ScriptCacheTimeout.HasValue)
|
||||
entry.SetAbsoluteExpiration(_jintOptions.ScriptCacheTimeout.Value);
|
||||
|
|
@ -207,15 +201,19 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
|
|||
}
|
||||
|
||||
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
|
||||
private static string Serialize(object value)
|
||||
private string Serialize(object value)
|
||||
{
|
||||
return JsonSerializer.Serialize(value, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateJsonSerializerOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
|
||||
};
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
return JsonSerializer.Serialize(value, options);
|
||||
return options;
|
||||
}
|
||||
|
||||
private string Hash(string input)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,14 @@ public class TypeAliasRegistry : ITypeAliasRegistry
|
|||
this.RegisterType<decimal>("Decimal");
|
||||
this.RegisterType<float>("Single");
|
||||
this.RegisterType<double>("Double");
|
||||
this.RegisterType<byte[]>("Buffer");
|
||||
this.RegisterType<Stream>("Stream");
|
||||
this.RegisterType<Guid>("Guid");
|
||||
this.RegisterType<DateTime>("Date");
|
||||
this.RegisterType<DateTimeOffset>("Date");
|
||||
this.RegisterType<DateOnly>("Date");
|
||||
this.RegisterType<TimeOnly>("Date");
|
||||
this.RegisterType<IDictionary<string, object>>("ObjectDictionary");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -53,4 +53,13 @@ public static class OutputExtensions
|
|||
var parsedContentVariableType = (memoryBlock.Metadata as VariableBlockMetadata)?.Variable.GetType();
|
||||
return parsedContentVariableType?.GenericTypeArguments.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the output has a target.
|
||||
/// </summary>
|
||||
public static bool HasTarget(this Output? output, ActivityExecutionContext context)
|
||||
{
|
||||
var memoryBlockReference = output?.MemoryBlockReference();
|
||||
return memoryBlockReference is not null && context.ExpressionExecutionContext.TryGetBlock(memoryBlockReference, out _);
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
|
|||
|
||||
var consumingWorkflows = new List<WorkflowDefinition>();
|
||||
|
||||
if (definition.Options.UsableAsActivity == true)
|
||||
if (definition.Options is { UsableAsActivity: true, AutoUpdateConsumingWorkflows: true })
|
||||
{
|
||||
consumingWorkflows.AddRange(await UpdateReferencesInConsumingWorkflows(definition, cancellationToken));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue