using System.Net.Http.Headers; using System.Text.Json.Serialization; using Elsa.Extensions; using Elsa.Http.ContentWriters; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Contracts; using Elsa.Workflows.Core.Models; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders; namespace Elsa.Http; /// /// Send an HTTP request. /// [Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "Flow HTTP Request", Kind = ActivityKind.Task)] [PublicAPI] public class FlowSendHttpRequest : SendHttpRequestBase { /// /// A list of expected status codes to handle. /// [Input(Description = "A list of expected status codes to handle.", UIHint = InputUIHints.MultiText)] public Input> ExpectedStatusCodes { get; set; } = default!; /// protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response) { var expectedStatusCodes = ExpectedStatusCodes.GetOrDefault(context) ?? new List(0); var statusCode = (int)response.StatusCode; var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode); var outcome = hasMatchingStatusCode ? statusCode.ToString() : "Unmatched status code"; await context.CompleteActivityWithOutcomesAsync(outcome); } } /// /// Send an HTTP request. /// [Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)] [PublicAPI] public class SendHttpRequest : SendHttpRequestBase { /// /// A list of expected status codes to handle and the corresponding activity to execute when the status code matches. /// [Input( Description = "A list of expected status codes to handle and the corresponding activity to execute when the status code matches.", UIHint = "http-status-codes" )] public ICollection ExpectedStatusCodes { get; set; } = new List(); /// /// The activity to execute when the HTTP status code does not match any of the expected status codes. /// [Port] public IActivity? UnmatchedStatusCode { get; set; } /// protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response) { var expectedStatusCodes = ExpectedStatusCodes; var statusCode = (int)response.StatusCode; var matchingCase = expectedStatusCodes.FirstOrDefault(x => x.StatusCode == statusCode); var activity = matchingCase?.Activity ?? UnmatchedStatusCode; await context.ScheduleActivityAsync(activity, OnChildActivityCompletedAsync); } private async ValueTask OnChildActivityCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) { await context.CompleteActivityAsync(); } } /// /// A binding between an HTTP status code and an activity. /// public class HttpStatusCodeCase { /// /// Creates a new instance of the class. /// [JsonConstructor] public HttpStatusCodeCase() { } /// /// Creates a new instance of the class. /// public HttpStatusCodeCase(int statusCode, IActivity activity) { StatusCode = statusCode; Activity = activity; } /// /// The HTTP status code to match. /// public int StatusCode { get; set; } /// /// The activity to execute when the HTTP status code matches. /// public IActivity? Activity { get; set; } } /// /// Base class for activities that send HTTP requests. /// public abstract class SendHttpRequestBase : Activity { /// /// The URL to send the request to. /// [Input] public Input Url { get; set; } = default!; /// /// The HTTP method to use when sending the request. /// [Input( Description = "The HTTP method to use when sending the request.", Options = new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" }, DefaultValue = "GET", UIHint = InputUIHints.Dropdown )] public Input Method { get; set; } = new("GET"); /// /// The content to send with the request. Can be a string, an object, a byte array or a stream. /// [Input(Description = "The content to send with the request. Can be a string, an object, a byte array or a stream.")] public Input Content { get; set; } = default!; /// /// The content type to use when sending the request. /// [Input( Description = "The content type to use when sending the request.", Options = new[] { "", "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" }, UIHint = InputUIHints.Dropdown )] public Input ContentType { get; set; } = default!; /// /// The Authorization header value to send with the request. /// /// Bearer {some-access-token} [Input( Description = "The Authorization header value to send with the request. For example: Bearer {some-access-token}", Category = "Security" )] public Input Authorization { get; set; } = default!; /// /// The headers to send along with the request. /// [Input(Description = "The headers to send along with the request.", Category = "Advanced")] public Input RequestHeaders { get; set; } = new(new HttpRequestHeaders()); /// /// The parsed content, if any. /// [Output(Description = "The parsed content, if any.")] public Output ParsedContent { get; set; } = default!; /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var request = PrepareRequest(context); var httpClientFactory = context.GetRequiredService(); var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase)); var cancellationToken = context.CancellationToken; var response = await httpClient.SendAsync(request, cancellationToken); var parsedContent = await ParseContentAsync(context, response.Content); context.Set(Result, response); context.Set(ParsedContent, parsedContent); await HandleResponseAsync(context, response); } /// /// Handles the response. /// protected abstract ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response); private async Task ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent) { 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!; return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken); } 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); if (!string.IsNullOrWhiteSpace(authorization)) request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization); foreach (var header in headers) request.Headers.Add(header.Key, header.Value.AsEnumerable()); var contentType = ContentType.GetOrDefault(context); var content = Content.GetOrDefault(context); if (contentType != null && content != null) { var contentWriters = context.GetServices(); var contentWriter = SelectContentWriter(contentType, contentWriters); request.Content = contentWriter.CreateHttpContent(content, contentType); } return request; } private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable requestContentWriters) => string.IsNullOrWhiteSpace(contentType) ? new JsonContentFactory() : requestContentWriters.First(w => w.SupportsContentType(contentType)); }