HTTP Response Activity (#16)

* Updated Readme with a few more bits
* Implemented basic version of HTTP Response activity
This commit is contained in:
Sipke Schoorstra 2019-01-20 18:16:57 +01:00 committed by GitHub
parent bf7a70e7c4
commit 009bceff6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 248 additions and 18 deletions

View file

@ -1,10 +1,13 @@
![Web-based workflow designer](/doc/elsa-cover.png)
## Elsa Workflows
Elsa Workflows is a set of lean & mean workflow components that you can use in your .NET Core applications.
With Elsa, you can invoke and trigger workflows from your own application. Workflows can be expressed as JSON, YAML, XML or in code.
You can manually handcraft workflows or use the web-based workflow designer.
![Web-based workflow designer](/doc/workflow-sample-1.png)
![Web-based workflow designer](/doc/workflow-sample-1.png)
## Why Elsa Workflows?
@ -112,3 +115,4 @@ In order to run Elsa on your local machine, follow these steps:
- Implement more activities (fork, join, script, HTTP request, loops, etc.)
- Implement integration with Orchard Core (separate repo)
- Detailed documentation

BIN
doc/elsa-cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 KiB

View file

@ -19,13 +19,18 @@ namespace Elsa.Activities.Http.Activities
public string Method { get; set; }
/// <summary>
/// The body to send along with the request
/// The body to send along with the request.
/// </summary>
public WorkflowExpression<string> Body { get; set; }
/// <summary>
/// The body to send along with the request
/// The headers to send along with the request.
/// </summary>
public IDictionary<string, string> RequestHeaders { get; set; }
/// <summary>
/// A list of HTTP status codes this activity cam handle.
/// </summary>
public ICollection<int> SupportedStatusCodes { get; set; }
}
}

View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Net;
using Elsa.Expressions;
using Elsa.Models;
namespace Elsa.Activities.Http.Activities
{
public class HttpResponseAction : Activity
{
/// <summary>
/// The HTTP status code to return.
/// </summary>
public HttpStatusCode StatusCode { get; set; }
/// <summary>
/// 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>
public WorkflowExpression<string> ContentType { get; set; }
/// <summary>
/// The headers to send along with the response, one header: value pair per line.
/// </summary>
public WorkflowExpression<string> ResponseHeaders { get; set; }
}
}

View file

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Activities.Http.Activities;
using Elsa.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Localization;
namespace Elsa.Activities.Http
@ -23,10 +25,18 @@ namespace Elsa.Activities.Http
T["Triggers when an incoming HTTP request is received."],
T["Done"]);
yield return ActivityDescriptor.ForAction<HttpRequestAction>(
yield return ActivityDescriptor.For<HttpRequestAction>(
Category,
T["HTTP Request"],
T["Execute a HTTP request."],
true,
true,
a => a.SupportedStatusCodes.Select(x => T[x.ToString()]));
yield return ActivityDescriptor.ForAction<HttpResponseAction>(
Category,
T["HTTP Response"],
T["Write a HTTP response."],
T["Done"]);
}
}

View file

@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Activities;
using Elsa.Expressions;
using Elsa.Handlers;
using Elsa.Models;
using Elsa.Results;
using Microsoft.AspNetCore.Http;
namespace Elsa.Activities.Http.Drivers
{
public class HttpResponseActionDriver : ActivityDriver<HttpResponseAction>
{
private readonly IWorkflowExpressionEvaluator expressionEvaluator;
private readonly IHttpContextAccessor httpContextAccessor;
public HttpResponseActionDriver(IWorkflowExpressionEvaluator expressionEvaluator, IHttpContextAccessor httpContextAccessor)
{
this.expressionEvaluator = expressionEvaluator;
this.httpContextAccessor = httpContextAccessor;
}
protected override async Task<ActivityExecutionResult> OnExecuteAsync(HttpResponseAction activity, WorkflowExecutionContext workflowContext, CancellationToken cancellationToken)
{
var response = httpContextAccessor.HttpContext.Response;
response.StatusCode = (int) activity.StatusCode;
response.ContentType = await expressionEvaluator.EvaluateAsync(activity.ContentType, workflowContext, cancellationToken);
var headersText = await expressionEvaluator.EvaluateAsync(activity.ResponseHeaders, workflowContext, cancellationToken);
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.ResponseHeaders.Syntax, header.Value);
response.Headers[header.Key] = await expressionEvaluator.EvaluateAsync(headerValueExpression, workflowContext, cancellationToken);
}
}
var bodyText = await expressionEvaluator.EvaluateAsync(activity.Body, workflowContext, cancellationToken);
if (!string.IsNullOrWhiteSpace(bodyText))
{
await response.WriteAsync(bodyText, cancellationToken);
}
return TriggerEndpoint("Done");
}
}
}

View file

@ -20,12 +20,15 @@ namespace Elsa.Activities.Http.Extensions
public static IServiceCollection AddHttpWorkflowDrivers(this IServiceCollection services)
{
services.AddHttpWorkflowDescriptors();
services.TryAddSingleton<IHttpWorkflowCache, DefaultHttpWorkflowCache>();
services.AddAsyncInitialization();
services.TryAddSingleton<IHttpWorkflowCache, DefaultHttpWorkflowCache>();
services.TryAddTransient<IAsyncInitializer, HttpWorkflowCacheInitializer>();
services.AddActivityDriver<HttpRequestTriggerDriver>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddActivityDriver<HttpRequestTriggerDriver>()
.AddActivityDriver<HttpResponseActionDriver>();
return services;
}
}

View file

@ -34,6 +34,15 @@ namespace Elsa.Models
params LocalizedString[] endpoints) =>
new ActivityDescriptor(typeof(T), category, displayText, description, isTrigger, isBrowsable, endpoints);
public static ActivityDescriptor For<T>(
LocalizedString category,
LocalizedString displayText,
LocalizedString description,
bool isTrigger,
bool isBrowsable,
Func<T, IEnumerable<LocalizedString>> getEndpoints) where T : IActivity =>
new ActivityDescriptor(typeof(T), category, displayText, description, isTrigger, isBrowsable, a => getEndpoints((T) a));
public ActivityDescriptor()
{
}
@ -46,15 +55,34 @@ namespace Elsa.Models
bool isTrigger,
bool isBrowsable,
params LocalizedString[] endpoints)
: this(
activityType,
category,
displayText,
description,
isTrigger,
isBrowsable,
a => endpoints)
{
}
public ActivityDescriptor(
Type activityType,
LocalizedString category,
LocalizedString displayText,
LocalizedString description,
bool isTrigger,
bool isBrowsable,
Func<IActivity, IEnumerable<LocalizedString>> getEndpoints)
{
ActivityType = activityType;
Name = activityType.Name;
Category = category;
DisplayText = displayText;
Description = description;
GetEndpoints = a => endpoints;
IsTrigger = isTrigger;
IsBrowsable = isBrowsable;
GetEndpoints = getEndpoints;
}
public bool IsBrowsable { get; set; } = true;

View file

@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1457" />
<PackageReference Include="Jint" Version="3.0.0-beta-1466" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />

View file

@ -1,4 +1,4 @@
@model Elsa.Web.ViewModels.ActivityShapeModel<Elsa.Activities.Console.Activities.ReadLine>
@model ActivityShapeModel<ReadLine>
@{
var activityDescriptor = Model.Activity.Descriptor;
}

View file

@ -1,4 +1,4 @@
@model Elsa.Web.ViewModels.ActivityShapeModel<Elsa.Activities.Console.Activities.ReadLine>
@model ActivityShapeModel<ReadLine>
<header>
<h4><i class="fa fa-glasses"></i>@Model.DisplayText</h4>

View file

@ -1,4 +1,4 @@
@model Elsa.Web.ViewModels.ActivityShapeModel<Elsa.Activities.Console.Activities.WriteLine>
@model ActivityShapeModel<WriteLine>
@{
var activityDescriptor = Model.Activity.Descriptor;
}

View file

@ -1,4 +1,4 @@
@model Elsa.Web.ViewModels.ActivityShapeModel<Elsa.Activities.Console.Activities.WriteLine>
@model ActivityShapeModel<WriteLine>
<header>
<h4><i class="fa fa-pen"></i>@Model.DisplayText</h4>

View file

@ -1,4 +1,4 @@
@model Elsa.Web.Activities.Console.ViewModels.WriteLineViewModel
@model WriteLineViewModel
<div class="form-group">
<label asp-for="TextExpression">@T["Text Expression"]</label>
<input asp-for="TextExpression" class="form-control"/>

View file

@ -1,2 +1,5 @@
@using Elsa.Activities.Console.Activities
@using Elsa.Web.Activities.Console.ViewModels
@using Elsa.Web.ViewModels
@inherits OrchardCore.DisplayManagement.Razor.RazorPage<TModel>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View file

@ -0,0 +1,27 @@
using System.Net;
using Elsa.Activities.Http.Activities;
using Elsa.Web.Activities.Http.ViewModels;
using Elsa.Web.Components.ViewModels;
using Elsa.Web.Drivers;
namespace Elsa.Web.Activities.Http.Display
{
public class HttpResponseActionDisplay : ActivityDisplayDriver<HttpResponseAction, HttpResponseActionViewModel>
{
protected override void EditActivity(HttpResponseAction activity, HttpResponseActionViewModel model)
{
model.StatusCode = (int)activity.StatusCode;
model.Body = new ExpressionViewModel(activity.Body);
model.ResponseHeaders = new ExpressionViewModel(activity.ResponseHeaders);
model.ContentType = new ExpressionViewModel(activity.ContentType);
}
protected override void UpdateActivity(HttpResponseActionViewModel model, HttpResponseAction activity)
{
activity.StatusCode = (HttpStatusCode)model.StatusCode;
activity.Body = model.Body.ToWorkflowExpression<string>();
activity.ResponseHeaders = model.ResponseHeaders.ToWorkflowExpression<string>();
activity.ContentType = model.ContentType.ToWorkflowExpression<string>();
}
}
}

View file

@ -13,7 +13,8 @@ namespace Elsa.Web.Activities.Http
services
.AddHttpWorkflowDescriptors()
.AddActivityDisplay<HttpRequestTriggerDisplay>()
.AddActivityDisplay<HttpRequestActionDisplay>();
.AddActivityDisplay<HttpRequestActionDisplay>()
.AddActivityDisplay<HttpResponseActionDisplay>();
}
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Expressions;
using Elsa.Web.Components.ViewModels;
namespace Elsa.Web.Activities.Http.ViewModels
{
public class HttpResponseActionViewModel
{
public HttpResponseActionViewModel()
{
StatusCode = 200;
ContentType = new ExpressionViewModel("application/json", PlainTextEvaluator.SyntaxName);
}
public int StatusCode { get; set; }
public ExpressionViewModel Body { get; set; }
public ExpressionViewModel ContentType { get; set; }
public ExpressionViewModel ResponseHeaders { get; set; }
}
}

View file

@ -0,0 +1,6 @@
@model ActivityShapeModel<HttpResponseAction>
@{
var activityDescriptor = Model.Activity.Descriptor;
}
<h4 class="card-title"><i class="fas fa-globe"></i>@activityDescriptor.DisplayText</h4>
<p>@activityDescriptor.Description</p>

View file

@ -0,0 +1,5 @@
@model ActivityShapeModel<HttpResponseAction>
<header>
<h4><i class="fas fa-globe"></i>@Model.DisplayText</h4>
</header>

View file

@ -0,0 +1,25 @@
@model HttpResponseActionViewModel
<div class="form-group">
<label asp-for="StatusCode">@T["HTTP Status Code"]</label>
<input asp-for="StatusCode" class="form-control" type="number" min="0" max="999"/>
<span asp-validation-for="StatusCode" class="text-danger"></span>
<small class="form-text text-muted">@T["The HTTP status code to return."]</small>
</div>
<div class="form-group">
<label asp-for="Body">@T["Body"]</label>
<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 response."]</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 value to return."]</small>
</div>
<div class="form-group">
<label asp-for="ResponseHeaders">@T["Response Headers"]</label>
<vc:expression-editor model="@Model.ResponseHeaders" prefix="@Html.NameFor(m => m.ResponseHeaders)"></vc:expression-editor>
<span asp-validation-for="ResponseHeaders" class="text-danger"></span>
<small class="form-text text-muted">@T["Any additional header to return, one per line. For example: my-header-one=foo\r\nmy-header-two=bar"]</small>
</div>

View file

@ -7,11 +7,16 @@ namespace Elsa.Web.Components.ViewModels
public ExpressionViewModel()
{
}
public ExpressionViewModel(WorkflowExpression workflowExpression)
public ExpressionViewModel(string expression, string syntax)
{
Expression = expression;
Syntax = syntax;
}
public ExpressionViewModel(WorkflowExpression workflowExpression)
: this(workflowExpression?.Expression, workflowExpression?.Syntax)
{
Expression = workflowExpression?.Expression;
Syntax = workflowExpression?.Syntax;
}
public string Expression { get; set; }