diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs b/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs index 5e161fa1f..d4c8e42cd 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Elsa.Expressions; using Elsa.Models; @@ -7,29 +6,39 @@ namespace Elsa.Activities.Http.Activities { public class HttpRequestAction : Activity { + public HttpRequestAction() + { + SupportedStatusCodes = new HashSet{ 200 }; + } + /// /// The URL to invoke. /// - public Uri Url { get; set; } + public WorkflowExpression Url { get; set; } /// /// The HTTP method to use. /// - public string Method { get; set; } + public string Method { get; set; } = "GET"; /// /// The body to send along with the request. /// public WorkflowExpression Body { get; set; } - + + /// + /// The Content Type header to send along with the request body. + /// + public WorkflowExpression ContentType { get; set; } + /// /// The headers to send along with the request. /// - public IDictionary RequestHeaders { get; set; } + public WorkflowExpression RequestHeaders { get; set; } /// - /// A list of HTTP status codes this activity cam handle. + /// A list of HTTP status codes this activity can handle. /// - public ICollection SupportedStatusCodes { get; set; } + public HashSet SupportedStatusCodes { get; set; } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs b/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs index af5294e2f..f909bc1cd 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs @@ -15,7 +15,7 @@ namespace Elsa.Activities.Http.Activities /// The body to send along with the response /// public WorkflowExpression Body { get; set; } - + /// /// The Content-Type header to send along with the response. /// diff --git a/src/activities/Elsa.Activities.Http/Drivers/HttpRequestActionDriver.cs b/src/activities/Elsa.Activities.Http/Drivers/HttpRequestActionDriver.cs index 5bb9b1431..9481b73cb 100644 --- a/src/activities/Elsa.Activities.Http/Drivers/HttpRequestActionDriver.cs +++ b/src/activities/Elsa.Activities.Http/Drivers/HttpRequestActionDriver.cs @@ -1,24 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Elsa.Activities.Http.Activities; +using Elsa.Activities.Http.Models; +using Elsa.Expressions; using Elsa.Handlers; using Elsa.Models; using Elsa.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; namespace Elsa.Activities.Http.Drivers { public class HttpRequestActionDriver : ActivityDriver { private readonly IWorkflowExpressionEvaluator expressionEvaluator; + private readonly HttpClient httpClient; + private readonly IEnumerable contentFormatters; - public HttpRequestActionDriver(IWorkflowExpressionEvaluator expressionEvaluator) + public HttpRequestActionDriver( + IWorkflowExpressionEvaluator expressionEvaluator, + IHttpClientFactory httpClientFactory, + IEnumerable contentFormatters) { this.expressionEvaluator = expressionEvaluator; + httpClient = httpClientFactory.CreateClient(nameof(HttpRequestActionDriver)); + this.contentFormatters = contentFormatters; } protected override async Task OnExecuteAsync(HttpRequestAction activity, WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) { - return Endpoint("Done"); + var request = await CreateRequestAsync(activity, workflowContext, cancellationToken); + var response = await httpClient.SendAsync(request, cancellationToken); + var content = response.Content != null ? await response.Content.ReadAsStringAsync() : default(string); + var contentType = response.Content?.Headers.ContentType.MediaType; + var formatter = SelectContentFormatter(contentType); + + var responseModel = new HttpResponseModel + { + StatusCode = response.StatusCode, + Headers = new HeaderDictionary(response.Headers.ToDictionary(x => x.Key, x => new StringValues(x.Value.ToArray()))), + Content = content, + FormattedContent = await formatter.FormatAsync(content, contentType) + }; + + workflowContext.SetLastResult(responseModel); + var statusEndpoint = ((int) response.StatusCode).ToString(); + + return Endpoints(new[] { "Done", statusEndpoint }); + } + + private IContentFormatter SelectContentFormatter(string contentType) + { + var formatters = contentFormatters.OrderByDescending(x => x.Priority).ToList(); + return formatters.FirstOrDefault(x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase)) ?? formatters.Last(); + } + + private async Task CreateRequestAsync(HttpRequestAction activity, WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var methodSupportsBody = GetMethodSupportsBody(activity.Method); + var uri = await expressionEvaluator.EvaluateAsync(activity.Url, workflowContext, cancellationToken); + var request = new HttpRequestMessage(new HttpMethod(activity.Method), uri); + var requestHeaders = await ParseRequestHeadersAsync(activity, workflowContext, cancellationToken); + + if (methodSupportsBody) + { + var body = await expressionEvaluator.EvaluateAsync(activity.Body, workflowContext, cancellationToken); + var contentType = await expressionEvaluator.EvaluateAsync(activity.ContentType, workflowContext, cancellationToken); + + if (!string.IsNullOrWhiteSpace(body)) + { + request.Content = new StringContent(body, Encoding.UTF8, contentType); + } + } + + foreach (var header in requestHeaders) + { + request.Headers.Add(header.Key, header.Value.AsEnumerable()); + } + + return request; + } + + private async Task ParseRequestHeadersAsync(HttpRequestAction activity, WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var headersText = await expressionEvaluator.EvaluateAsync(activity.RequestHeaders, workflowContext, cancellationToken); + var headers = new HeaderDictionary(); + + if (headersText != null) + { + var headersQuery = + from line in Regex.Split(headersText, "\\n", RegexOptions.Multiline) + let pair = line.Split(':', '=') + select new KeyValuePair(pair[0], pair[1]); + + foreach (var header in headersQuery) + { + var headerValueExpression = new WorkflowExpression(activity.RequestHeaders.Syntax, header.Value); + var headerValue = await expressionEvaluator.EvaluateAsync(headerValueExpression, workflowContext, cancellationToken); + headers.Add(header.Key, headerValue); + } + } + + return headers; + } + + private bool GetMethodSupportsBody(string method) + { + var methods = new[] { "POST", "PUT", "PATCH" }; + return methods.Contains(method, StringComparer.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj index 0c5ec15ae..180b21265 100644 --- a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj +++ b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj @@ -23,6 +23,7 @@ + diff --git a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs index 87bc758fa..eeb68150c 100644 --- a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs +++ b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Elsa.Activities.Http.Drivers; +using Elsa.Activities.Http.Formatters; using Elsa.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -18,10 +19,16 @@ namespace Elsa.Activities.Http.Extensions services.AddHttpWorkflowDescriptors(); services.AddAsyncInitialization(); services.TryAddSingleton(); + services.AddHttpClient(nameof(HttpRequestActionDriver)); services .AddActivityDriver() - .AddActivityDriver(); + .AddActivityDriver() + .AddActivityDriver(); + + services + .AddSingleton() + .AddSingleton(); return services; } diff --git a/src/activities/Elsa.Activities.Http/Formatters/JsonContentFormatter.cs b/src/activities/Elsa.Activities.Http/Formatters/JsonContentFormatter.cs new file mode 100644 index 000000000..60eef10ef --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Formatters/JsonContentFormatter.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Elsa.Activities.Http.Formatters +{ + public class JsonContentFormatter : IContentFormatter + { + public int Priority => 0; + public IEnumerable SupportedContentTypes => new[] { "application/json", "text/json" }; + + public Task FormatAsync(string content, string contentType) + { + return Task.FromResult(JToken.Parse(content)); + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Formatters/NullContentFormatter.cs b/src/activities/Elsa.Activities.Http/Formatters/NullContentFormatter.cs new file mode 100644 index 000000000..b17a42c09 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Formatters/NullContentFormatter.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Elsa.Activities.Http.Formatters +{ + public class NullContentFormatter : IContentFormatter + { + public int Priority => -1; + public IEnumerable SupportedContentTypes => new[] { "", default(string) }; + + public Task FormatAsync(string content, string contentType) + { + return null; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/IContentFormatter.cs b/src/activities/Elsa.Activities.Http/IContentFormatter.cs new file mode 100644 index 000000000..ff38d7c49 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/IContentFormatter.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Elsa.Activities.Http +{ + public interface IContentFormatter + { + int Priority { get; } + IEnumerable SupportedContentTypes { get; } + Task FormatAsync(string content, string contentType); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Models/HttpResponseModel.cs b/src/activities/Elsa.Activities.Http/Models/HttpResponseModel.cs new file mode 100644 index 000000000..b259a854f --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Models/HttpResponseModel.cs @@ -0,0 +1,17 @@ +using System.Net; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Activities.Http.Models +{ + public class HttpResponseModel + { + public HttpResponseModel() + { + } + + public HttpStatusCode StatusCode { get; set; } + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + public string Content { get; set; } + public object FormattedContent { get; set; } + } +} \ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/Display/HttpRequestActionDisplay.cs b/src/web/modules/Elsa.Web.Activities.Http/Display/HttpRequestActionDisplay.cs index 9b9cc6bff..2dfef7b47 100644 --- a/src/web/modules/Elsa.Web.Activities.Http/Display/HttpRequestActionDisplay.cs +++ b/src/web/modules/Elsa.Web.Activities.Http/Display/HttpRequestActionDisplay.cs @@ -1,5 +1,7 @@ -using Elsa.Activities.Http.Activities; -using Elsa.Expressions; +using System; +using System.Collections.Generic; +using System.Linq; +using Elsa.Activities.Http.Activities; using Elsa.Web.Activities.Http.ViewModels; using Elsa.Web.Components.ViewModels; using Elsa.Web.Drivers; @@ -12,14 +14,24 @@ namespace Elsa.Web.Activities.Http.Display { model.Method = activity.Method; model.Body = new ExpressionViewModel(activity.Body); - model.Url = activity.Url; + model.Url = new ExpressionViewModel(activity.Url); + model.RequestHeaders = new ExpressionViewModel(activity.RequestHeaders); + model.ContentType = new ExpressionViewModel(activity.ContentType); + model.SupportedStatusCodes = string.Join(", ", activity.SupportedStatusCodes); } protected override void UpdateActivity(HttpRequestActionViewModel model, HttpRequestAction activity) { activity.Method = model.Method; activity.Body = model.Body.ToWorkflowExpression(); - activity.Url = model.Url; + activity.Url = model.Url.ToWorkflowExpression(); + activity.RequestHeaders = model.RequestHeaders.ToWorkflowExpression(); + activity.ContentType = model.ContentType.ToWorkflowExpression(); + activity.SupportedStatusCodes = new HashSet(model.SupportedStatusCodes + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(int.Parse)); } } } \ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/ViewModels/HttpRequestActionViewModel.cs b/src/web/modules/Elsa.Web.Activities.Http/ViewModels/HttpRequestActionViewModel.cs index 8845751f9..ae1d08e54 100644 --- a/src/web/modules/Elsa.Web.Activities.Http/ViewModels/HttpRequestActionViewModel.cs +++ b/src/web/modules/Elsa.Web.Activities.Http/ViewModels/HttpRequestActionViewModel.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Elsa.Expressions; using Elsa.Web.Components.ViewModels; using Microsoft.AspNetCore.Mvc.Rendering; @@ -10,14 +8,19 @@ namespace Elsa.Web.Activities.Http.ViewModels { public class HttpRequestActionViewModel { - [Required] - public Uri Url { get; set; } + public ExpressionViewModel Url { get; set; } [Required] public string Method { get; set; } public ExpressionViewModel Body { get; set; } + public ExpressionViewModel ContentType { get; set; } + public ExpressionViewModel RequestHeaders { get; set; } + + [Required] + public string SupportedStatusCodes { get; set; } + public ICollection GetAvailableHttpMethods() { var availableHttpMethods = new[] { "GET", "POST", "PUT", "DELETE", "OPTIONS" }; diff --git a/src/web/modules/Elsa.Web.Activities.Http/Views/HttpRequestAction.Edit.cshtml b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpRequestAction.Edit.cshtml index 52a2736d0..dab3f3e62 100644 --- a/src/web/modules/Elsa.Web.Activities.Http/Views/HttpRequestAction.Edit.cshtml +++ b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpRequestAction.Edit.cshtml @@ -1,8 +1,7 @@ -@using OrchardCore.Mvc.Core.Utilities @model HttpRequestActionViewModel
- + @T["The relative path that will trigger this activity. Example: '/webhooks/my'"]
@@ -17,4 +16,22 @@ @T["The HTTP request body to send along with the request."] + +
+ + + + @T["The Content-Type header send along with the request."] +
+
+ + + + @T["Any additional headers to send along with the request, one per line. For example: my-header-one=foo\r\nmy-header-two=bar"] +
+
+ + + + @T["A comma-separated list of HTTP status codes to handle. These will be used to emit the activity's endpoints."]
\ No newline at end of file