Implemented HTTP Request Action activity (#24)
This commit is contained in:
parent
a167799cd6
commit
3ec5c5ae84
|
|
@ -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<int>{ 200 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The URL to invoke.
|
||||
/// </summary>
|
||||
public Uri Url { get; set; }
|
||||
public WorkflowExpression<string> Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP method to use.
|
||||
/// </summary>
|
||||
public string Method { get; set; }
|
||||
public string Method { get; set; } = "GET";
|
||||
|
||||
/// <summary>
|
||||
/// The body to send along with the request.
|
||||
/// </summary>
|
||||
public WorkflowExpression<string> Body { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The Content Type header to send along with the request body.
|
||||
/// </summary>
|
||||
public WorkflowExpression<string> ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers to send along with the request.
|
||||
/// </summary>
|
||||
public IDictionary<string, string> RequestHeaders { get; set; }
|
||||
public WorkflowExpression<string> RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of HTTP status codes this activity cam handle.
|
||||
/// A list of HTTP status codes this activity can handle.
|
||||
/// </summary>
|
||||
public ICollection<int> SupportedStatusCodes { get; set; }
|
||||
public HashSet<int> SupportedStatusCodes { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ namespace Elsa.Activities.Http.Activities
|
|||
/// The body to send along with the response
|
||||
/// </summary>
|
||||
public WorkflowExpression<string> Body { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The Content-Type header to send along with the response.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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<HttpRequestAction>
|
||||
{
|
||||
private readonly IWorkflowExpressionEvaluator expressionEvaluator;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly IEnumerable<IContentFormatter> contentFormatters;
|
||||
|
||||
public HttpRequestActionDriver(IWorkflowExpressionEvaluator expressionEvaluator)
|
||||
public HttpRequestActionDriver(
|
||||
IWorkflowExpressionEvaluator expressionEvaluator,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IEnumerable<IContentFormatter> contentFormatters)
|
||||
{
|
||||
this.expressionEvaluator = expressionEvaluator;
|
||||
httpClient = httpClientFactory.CreateClient(nameof(HttpRequestActionDriver));
|
||||
this.contentFormatters = contentFormatters;
|
||||
}
|
||||
|
||||
protected override async Task<ActivityExecutionResult> 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<HttpRequestMessage> 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<IHeaderDictionary> 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<string, string>(pair[0], pair[1]);
|
||||
|
||||
foreach (var header in headersQuery)
|
||||
{
|
||||
var headerValueExpression = new WorkflowExpression<string>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddHttpClient(nameof(HttpRequestActionDriver));
|
||||
|
||||
services
|
||||
.AddActivityDriver<HttpRequestTriggerDriver>()
|
||||
.AddActivityDriver<HttpResponseActionDriver>();
|
||||
.AddActivityDriver<HttpResponseActionDriver>()
|
||||
.AddActivityDriver<HttpRequestActionDriver>();
|
||||
|
||||
services
|
||||
.AddSingleton<IContentFormatter, NullContentFormatter>()
|
||||
.AddSingleton<IContentFormatter, JsonContentFormatter>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string> SupportedContentTypes => new[] { "application/json", "text/json" };
|
||||
|
||||
public Task<object> FormatAsync(string content, string contentType)
|
||||
{
|
||||
return Task.FromResult<object>(JToken.Parse(content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> SupportedContentTypes => new[] { "", default(string) };
|
||||
|
||||
public Task<object> FormatAsync(string content, string contentType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/activities/Elsa.Activities.Http/IContentFormatter.cs
Normal file
13
src/activities/Elsa.Activities.Http/IContentFormatter.cs
Normal file
|
|
@ -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<string> SupportedContentTypes { get; }
|
||||
Task<object> FormatAsync(string content, string contentType);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string>();
|
||||
activity.Url = model.Url;
|
||||
activity.Url = model.Url.ToWorkflowExpression<string>();
|
||||
activity.RequestHeaders = model.RequestHeaders.ToWorkflowExpression<string>();
|
||||
activity.ContentType = model.ContentType.ToWorkflowExpression<string>();
|
||||
activity.SupportedStatusCodes = new HashSet<int>(model.SupportedStatusCodes
|
||||
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(int.Parse));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SelectListItem> GetAvailableHttpMethods()
|
||||
{
|
||||
var availableHttpMethods = new[] { "GET", "POST", "PUT", "DELETE", "OPTIONS" };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
@using OrchardCore.Mvc.Core.Utilities
|
||||
@model HttpRequestActionViewModel
|
||||
<div class="form-group">
|
||||
<label asp-for="Url">@T["Url"]</label>
|
||||
<input asp-for="Url" class="form-control"/>
|
||||
<vc:expression-editor model="@Model.Url" prefix="@Html.NameFor(m => m.Url)"></vc:expression-editor>
|
||||
<span asp-validation-for="Url" class="text-danger"></span>
|
||||
<small class="form-text text-muted">@T["The relative path that will trigger this activity. Example: '/webhooks/my'"]</small>
|
||||
</div>
|
||||
|
|
@ -17,4 +16,22 @@
|
|||
<vc:expression-editor model="@Model.Body" prefix="@Html.NameFor(m => m.Body)"></vc:expression-editor>
|
||||
<span asp-validation-for="Body" class="text-danger"></span>
|
||||
<small class="form-text text-muted">@T["The HTTP request body to send along with the request."]</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ContentType">@T["Content Type"]</label>
|
||||
<vc:expression-editor model="@Model.ContentType" prefix="@Html.NameFor(m => m.ContentType)"></vc:expression-editor>
|
||||
<span asp-validation-for="ContentType" class="text-danger"></span>
|
||||
<small class="form-text text-muted">@T["The Content-Type header send along with the request."]</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RequestHeaders">@T["Request Headers"]</label>
|
||||
<vc:expression-editor model="@Model.RequestHeaders" prefix="@Html.NameFor(m => m.RequestHeaders)"></vc:expression-editor>
|
||||
<span asp-validation-for="RequestHeaders" class="text-danger"></span>
|
||||
<small class="form-text text-muted">@T["Any additional headers to send along with the request, one per line. For example: my-header-one=foo\r\nmy-header-two=bar"]</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SupportedStatusCodes">@T["Handle Status Codes"]</label>
|
||||
<input asp-for="SupportedStatusCodes" class="form-control"/>
|
||||
<span asp-validation-for="SupportedStatusCodes" class="text-danger"></span>
|
||||
<small class="form-text text-muted">@T["A comma-separated list of HTTP status codes to handle. These will be used to emit the activity's endpoints."]</small>
|
||||
</div>
|
||||
Loading…
Reference in a new issue