HTTP Request with status codes as outcomes (#3768)

* Implement configurable expected status outcomes on HTTP Request activity

* Add Catch all outcome

* Implement dynamic port update
This commit is contained in:
Sipke Schoorstra 2023-03-07 13:45:58 +01:00 committed by GitHub
parent ac9ecbb2d4
commit b1564bf646
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 398 additions and 64 deletions

View file

@ -7,7 +7,6 @@ export * from "./flow-node";
export * from "./flowchart";
export * from "./foreach";
export * from "./http-endpoint";
export * from "./http-request";
export * from "./http-response";
export * from "./if";
export * from "./models";

View file

@ -2,12 +2,16 @@ import {Node} from "@antv/x6";
import {Activity, ActivityDescriptor} from "../../models";
export interface ActivityNodeHandler {
createDesignerNode: (context: CreateUINodeContext) => Node.Metadata;
createDesignerNode: (context: UINodeContext) => Node.Metadata;
createPorts: (context: UIPortContext) => Array<any>;
}
export interface CreateUINodeContext {
activityDescriptor: ActivityDescriptor;
activity: Activity;
export interface UINodeContext extends UIPortContext{
x: number;
y: number;
}
export interface UIPortContext {
activityDescriptor: ActivityDescriptor;
activity: Activity;
}

View file

@ -1,7 +1,7 @@
import 'reflect-metadata';
import {Node} from "@antv/x6";
import {Container, Service} from "typedi"
import {ActivityNodeHandler, CreateUINodeContext} from "./activity-node-handler";
import {ActivityNodeHandler, UINodeContext, UIPortContext} from "./activity-node-handler";
import {PortProviderContext, PortProviderRegistry} from "../../services";
import {PortMode} from "../../models";
import {v4 as uuid} from 'uuid';
@ -14,8 +14,24 @@ export class DefaultNodeHandler implements ActivityNodeHandler {
this.portProviderRegistry = Container.get(PortProviderRegistry);
}
createDesignerNode(context: CreateUINodeContext): Node.Metadata {
createDesignerNode(context: UINodeContext): Node.Metadata {
const {activityDescriptor, activity, x, y} = context;
const portModels = this.createPorts(context);
return {
id: activity.id,
shape: 'activity',
activity: activity,
activityDescriptor: activityDescriptor,
x: x,
y: y,
data: activity,
ports: portModels
} as Node.Metadata;
}
createPorts(context: UIPortContext): Array<any> {
const {activityDescriptor, activity} = context;
const provider = this.portProviderRegistry.get(activityDescriptor.typeName);
const providerContext: PortProviderContext = {activityDescriptor, activity};
const inPorts = [{name: 'In', displayName: null, mode: PortMode.Port}];
@ -54,15 +70,6 @@ export class DefaultNodeHandler implements ActivityNodeHandler {
const portModels = [...leftPortModels, ...rightPortModels];
return {
id: activity.id,
shape: 'activity',
activity: activity,
activityDescriptor: activityDescriptor,
x: x,
y: y,
data: activity,
ports: portModels
} as Node.Metadata;
return portModels;
}
}

View file

@ -20,7 +20,7 @@ import FromJSONData = Model.FromJSONData;
import PointLike = Point.PointLike;
import {generateUniqueActivityName} from "../../utils/generate-activity-name";
import {DagreLayout, OutNode} from '@antv/layout';
import {adjustPortMarkupByNode, rebuildGraph} from '../../utils/graph';
import {adjustPortMarkupByNode, getPortNameByPortId, rebuildGraph} from '../../utils/graph';
import FlowchartTunnel, {FlowchartState} from "./state";
const FlowchartTypeName = 'Elsa.Flowchart';
@ -196,19 +196,25 @@ export class FlowchartComponent {
const originalId = args.originalId;
const nodeId = originalId;
const activity = args.activity;
const node = this.graph.getNodes().find(x => x.id == nodeId) as ActivityNodeShape;
const node = this.graph.getNodes().find(x => x.id == nodeId);
const nodeShape = node as ActivityNodeShape;
if (!!node) {
// Update the node's data with the activity.
node.setData(activity, {overwrite: true});
nodeShape.setData(activity, {overwrite: true});
// Updating the node's activity property to trigger a rerender.
node.activity = activity;
nodeShape.activity = activity;
// If the ID of the activity changed, we need to update connection references (X6 stores deep copies of data).
if (activityId !== originalId)
this.syncEdgeData(nodeId, activity);
// Update ports.
if (args.updatePorts) {
this.updatePorts(node, activity);
}
}
this.updateLookups();
@ -671,6 +677,22 @@ export class FlowchartComponent {
await this.setupGraph(childFlowchart);
}
private updatePorts = (node: any, activity: Activity) => {
const descriptor = this.getActivityDescriptor(activity.type);
const desiredPorts = this.nodeFactory.createPorts(descriptor, activity);
const actualPorts = node.ports.items;
const addedPorts = desiredPorts.filter(x => !actualPorts.some(y => getPortNameByPortId(y.id) == getPortNameByPortId(x.id)));
const removedPorts = actualPorts.filter(x => !desiredPorts.some(y => getPortNameByPortId(y.id) == getPortNameByPortId(x.id)));
if(addedPorts.length > 0 )
node.addPorts(addedPorts);
if(removedPorts.length > 0)
node.removePorts(removedPorts);
};
render() {
const path = this.path;

View file

@ -40,6 +40,7 @@ export interface UpdateActivityArgs {
id: string;
originalId: string;
activity: Activity;
updatePorts?: boolean;
}
export interface RenameActivityArgs {

View file

@ -16,4 +16,9 @@ export class NodeFactory {
const handler = this.handlerRegistry.createHandler(activityDescriptor.typeName);
return handler.createDesignerNode({activityDescriptor, activity, x, y});
}
public createPorts(activityDescriptor: ActivityDescriptor, activity: Activity): Array<any> {
const handler = this.handlerRegistry.createHandler(activityDescriptor.typeName);
return handler.createPorts({activityDescriptor, activity});
}
}

View file

@ -0,0 +1,25 @@
import 'reflect-metadata';
import {h} from '@stencil/core';
import {Container, Service} from "typedi";
import {ActivityIconRegistry, PortProviderRegistry} from "../../../services";
import {Plugin} from "../../../models";
import {FlowHttpRequestPortProvider} from "./flow-http-request-port-provider";
import {HttpRequestIcon} from "../icons";
@Service()
export class FlowHttpRequestPlugin implements Plugin {
static readonly ActivityTypeName: string = 'Elsa.FlowSendHttpRequest';
constructor() {
const activityTypeName = FlowHttpRequestPlugin.ActivityTypeName;
const portProviderRegistry = Container.get(PortProviderRegistry);
const iconRegistry = Container.get(ActivityIconRegistry);
portProviderRegistry.add(activityTypeName, () => Container.get(FlowHttpRequestPortProvider));
iconRegistry.add(FlowHttpRequestPlugin.ActivityTypeName, settings => <HttpRequestIcon size={settings?.size}/>);
}
async initialize(): Promise<void> {
}
}

View file

@ -0,0 +1,36 @@
import 'reflect-metadata';
import {Service} from "typedi";
import {Activity, ActivityInput, InputDescriptor, JsonExpression, Port, PortMode} from "../../../models";
import {PortProvider, PortProviderContext} from "../../../services";
import {FlowSendHttpRequest} from "./models";
@Service()
export class FlowHttpRequestPortProvider implements PortProvider {
getOutboundPorts(context: PortProviderContext): Array<Port> {
const activity = context.activity as FlowSendHttpRequest;
if (activity == null)
return [];
const expectedStatusCodes = activity.expectedStatusCodes as ActivityInput;
if(!expectedStatusCodes)
return [];
const statusCodesJson = (expectedStatusCodes.expression as JsonExpression).value;
const statusCodes = JSON.parse(statusCodesJson) as Array<string>;
const catchAllPort = {name: 'Catch all', displayName: 'Catch all', mode: PortMode.Port};
const outcomes = [...statusCodes.map(x => ({name: x.toString(), displayName: x.toString(), mode: PortMode.Port})), catchAllPort];
return outcomes;
}
resolvePort(portName: string, context: PortProviderContext): Activity | Array<Activity> {
return null;
}
assignPort(portName: string, activity: Activity, context: PortProviderContext) {
return null;
}
}

View file

@ -0,0 +1,5 @@
import {Activity, ActivityInput} from "../../../models";
export interface FlowSendHttpRequest extends Activity {
expectedStatusCodes: ActivityInput;
}

View file

@ -1,5 +1,5 @@
import {FunctionalComponent, h} from '@stencil/core';
import {ActivityIconSettings, getActivityIconCssClass} from "./models";
import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities";
export const HttpRequestIcon: FunctionalComponent<ActivityIconSettings> = (settings) => (
<svg class={getActivityIconCssClass(settings)} width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"

View file

@ -0,0 +1,25 @@
import 'reflect-metadata';
import {h} from '@stencil/core';
import {Container, Service} from "typedi";
import {ActivityIconRegistry, PortProviderRegistry} from "../../../services";
import {Plugin} from "../../../models";
import {HttpRequestPortProvider} from "./http-request-port-provider";
import {HttpRequestIcon} from "../icons";
@Service()
export class HttpRequestPlugin implements Plugin {
static readonly ActivityTypeName: string = 'Elsa.SendHttpRequest';
constructor() {
const activityTypeName = HttpRequestPlugin.ActivityTypeName;
const portProviderRegistry = Container.get(PortProviderRegistry);
const iconRegistry = Container.get(ActivityIconRegistry);
portProviderRegistry.add(activityTypeName, () => Container.get(HttpRequestPortProvider));
iconRegistry.add(HttpRequestPlugin.ActivityTypeName, settings => <HttpRequestIcon size={settings?.size}/>);
}
async initialize(): Promise<void> {
}
}

View file

@ -0,0 +1,45 @@
import 'reflect-metadata';
import {Service} from "typedi";
import {Activity, Port, PortMode} from "../../../models";
import {PortProvider, PortProviderContext} from "../../../services";
import {HttpStatusCodeCase, SendHttpRequest} from "./models";
@Service()
export class HttpRequestPortProvider implements PortProvider {
getOutboundPorts(context: PortProviderContext): Array<Port> {
const activity = context.activity as SendHttpRequest;
if(activity == null)
return [];
const defaultPort = {name: 'unmatchedStatusCode', displayName: 'Unmatched status code', mode: PortMode.Embedded, isBrowsable: false}; // Hide the port from the designer until the editor uI is finished.
const casesArray = this.getCases(activity);
const ports = casesArray.map(x => ({name: x.statusCode.toString(), displayName: x.statusCode.toString(), mode: PortMode.Embedded}));
return [...ports, defaultPort];
}
resolvePort(portName: string, context: PortProviderContext): Activity | Array<Activity> {
const activity = context.activity as SendHttpRequest;
const cases = this.getCases(activity);
const matchingStatusCode = cases.find(x => x.statusCode.toString() == portName);
return !matchingStatusCode ? activity.unmatchedStatusCode : matchingStatusCode.activity;
}
assignPort(portName: string, activity: Activity, context: PortProviderContext) {
const sendHttpRequestActivity = context.activity as SendHttpRequest;
const cases = this.getCases(sendHttpRequestActivity);
const matchingCase = cases.find(x => x.statusCode.toString() === portName);
if(!matchingCase)
return;
matchingCase.activity = activity;
}
private getCases(activity: SendHttpRequest): Array<HttpStatusCodeCase> {
const cases = activity.expectedStatusCodes;
return !cases ? [] : cases;
}
}

View file

@ -0,0 +1,11 @@
import {Activity} from "../../../models";
export interface SendHttpRequest extends Activity {
expectedStatusCodes: Array<HttpStatusCodeCase>;
unmatchedStatusCode?: Activity;
}
export interface HttpStatusCodeCase {
statusCode: number;
activity?: Activity;
}

View file

@ -1,11 +1,11 @@
import {Component, h, Prop, State, Watch} from "@stencil/core";
import {camelCase} from 'lodash';
import {ActivityInputContext} from "../../services/activity-input-driver";
import {mapSyntaxToLanguage} from "../../utils";
import {SyntaxNames} from "../../models";
import {MonacoValueChangedArgs} from "../../components/shared/monaco-editor/monaco-editor";
import {TrashBinButtonIcon} from "../../components/icons/buttons/trash-bin";
import {PlusButtonIcon} from "../../components/icons/buttons/plus";
import {ActivityInputContext} from "../../../services/activity-input-driver";
import {mapSyntaxToLanguage} from "../../../utils";
import {SyntaxNames} from "../../../models";
import {MonacoValueChangedArgs} from "../../../components/shared/monaco-editor/monaco-editor";
import {TrashBinButtonIcon} from "../../../components/icons/buttons/trash-bin";
import {PlusButtonIcon} from "../../../components/icons/buttons/plus";
import {FlowSwitchCase} from "./models";
@Component({

View file

@ -1,10 +1,10 @@
import 'reflect-metadata';
import {FunctionalComponent, h} from '@stencil/core';
import {Container, Service} from "typedi";
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../services";
import {Plugin} from "../../models";
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../../services";
import {Plugin} from "../../../models";
import {FlowSwitchPortProvider} from "./flow-switch-port-provider";
import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities";
import {ActivityIconSettings, getActivityIconCssClass} from "../../../components/icons/activities";
@Service()
export class FlowSwitchPlugin implements Plugin {

View file

@ -1,8 +1,8 @@
import 'reflect-metadata';
import {Service} from "typedi";
import {Activity, Port, PortMode} from "../../models";
import {Activity, Port, PortMode} from "../../../models";
import {FlowSwitchActivity} from "./models";
import {PortProvider, PortProviderContext} from "../../services";
import {PortProvider, PortProviderContext} from "../../../services";
@Service()
export class FlowSwitchPortProvider implements PortProvider {

View file

@ -1,4 +1,4 @@
import {Activity, Expression} from "../../models";
import {Activity, Expression} from "../../../models";
export interface FlowSwitchCase {
label: string;

View file

@ -1,4 +1,4 @@
import {Activity, ActivityInput, Expression} from "../../models";
import {Activity, ActivityInput, Expression} from "../../../models";
export interface SwitchCase {
label: string;

View file

@ -1,13 +1,12 @@
import {Component, h, Prop, State, Watch} from "@stencil/core";
import {camelCase} from 'lodash';
import {ActivityInputContext} from "../../services/activity-input-driver";
import {mapSyntaxToLanguage} from "../../utils";
import {SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../../services/activity-input-driver";
import {mapSyntaxToLanguage} from "../../../utils";
import {SyntaxNames} from "../../../models";
import {SwitchCase} from "./models";
import {MonacoValueChangedArgs} from "../../components/shared/monaco-editor/monaco-editor";
import {TrashBinButtonIcon} from "../../components/icons/buttons/trash-bin";
import {PlusButtonIcon} from "../../components/icons/buttons/plus";
import {FormEntry} from "../../components/shared/forms/form-entry";
import {MonacoValueChangedArgs} from "../../../components/shared/monaco-editor/monaco-editor";
import {TrashBinButtonIcon} from "../../../components/icons/buttons/trash-bin";
import {PlusButtonIcon} from "../../../components/icons/buttons/plus";
@Component({
tag: 'elsa-switch-editor',

View file

@ -1,10 +1,10 @@
import 'reflect-metadata';
import {FunctionalComponent, h} from '@stencil/core';
import {Container, Service} from "typedi";
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../services";
import {Plugin} from "../../models";
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../../services";
import {Plugin} from "../../../models";
import {SwitchPortProvider} from "./switch-port-provider";
import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities";
import {ActivityIconSettings, getActivityIconCssClass} from "../../../components/icons/activities";
@Service()
export class SwitchPlugin implements Plugin {

View file

@ -1,9 +1,8 @@
import 'reflect-metadata';
import {Service} from "typedi";
import {camelCase} from 'lodash';
import {Activity, Port, PortMode} from "../../models";
import {Activity, Port, PortMode} from "../../../models";
import {SwitchActivity, SwitchCase} from "./models";
import {PortProvider, PortProviderContext} from "../../services";
import {PortProvider, PortProviderContext} from "../../../services";
@Service()
export class SwitchPortProvider implements PortProvider {

View file

@ -227,6 +227,7 @@ export class WorkflowDefinitionEditor {
}
private updateActivityInternal = async (args: UpdateActivityArgs) => {
args.updatePorts = true; // TODO: Make this configurable from a activity plugin.
await this.flowchart.updateActivity(args);
this.saveChangesDebounced();
}

View file

@ -11,8 +11,7 @@ import {
IfIcon,
ReadLineIcon, WriteLineIcon,
RunJavaScriptIcon,
HttpEndpointIcon, HttpResponseIcon, HttpRequestIcon,
CorrelateIcon
HttpEndpointIcon, HttpResponseIcon, CorrelateIcon
} from "../components/icons/activities";
import {ForIcon} from "../components/icons/activities/for";
import {FinishIcon} from "../components/icons/activities/finish";
@ -56,7 +55,6 @@ export class ActivityIconRegistry {
this.add('Elsa.RunJavaScript', settings => <RunJavaScriptIcon size={settings?.size}/>);
this.add('Elsa.FlowJoin', settings => <FlowJoinIcon size={settings?.size}/>);
this.add('Elsa.FlowNode', settings => <FlowNodeIcon size={settings?.size}/>);
this.add('Elsa.SendHttpRequest', settings => <HttpRequestIcon size={settings?.size}/>);
this.add("Elsa.Correlate", settings => <CorrelateIcon size={settings?.size}/>)
this.add("Elsa.Start", settings => <StartIcon size={settings?.size}/>)
this.add("Elsa.Finish", settings => <FinishIcon size={settings?.size}/>)

View file

@ -1,15 +1,17 @@
import 'reflect-metadata';
import {Container, Service} from "typedi";
import {SwitchPlugin} from "../modules/switch/switch-plugin";
import {Plugin} from "../models";
import {SequencePlugin} from "../modules/sequence/sequence-plugin";
import {FlowSwitchPlugin} from "../modules/flow-switch/flow-switch-plugin";
import {WorkflowDefinitionsPlugin} from "../modules/workflow-definitions/plugins/workflow-definitions-plugin";
import {CompositeActivityVersionPlugin} from "../modules/workflow-definitions/plugins/composite-version-plugin";
import {WorkflowInstancesPlugin} from "../modules/workflow-instances/plugin";
import {LoginPlugin} from "../modules/login/plugin";
import {HomePagePlugin} from "../modules/home/plugin";
import {FlowchartPlugin} from "../modules/flowchart/plugin";
import { SwitchPlugin } from '../modules/switch/sequence/switch-plugin';
import {FlowSwitchPlugin} from "../modules/switch/flow/flow-switch-plugin";
import {FlowHttpRequestPlugin} from "../modules/http-request/flow/flow-http-request-plugin";
import {HttpRequestPlugin} from "../modules/http-request/sequence/http-request-plugin";
// A registry of plugins.
@Service()
@ -26,6 +28,8 @@ export class PluginRegistry {
this.add(Container.get(SequencePlugin));
this.add(Container.get(SwitchPlugin));
this.add(Container.get(FlowSwitchPlugin));
this.add(Container.get(FlowHttpRequestPlugin));
this.add(Container.get(HttpRequestPlugin));
}
add(plugin: Plugin) {

View file

@ -147,7 +147,7 @@ function findMatchingPortForEdge(node: Node<Node.Properties>, position: string,
return node.getPorts().find(p => p.position == position && p.type == portType && getPortNameByPortId(p.id) == portName);
}
function getPortNameByPortId(portId: string) {
export function getPortNameByPortId(portId: string) {
return portId.includes('_') ? (portId.split('_')[1] == 'null' ? null : portId.split('_')[1]) : portId;
}

View file

@ -1,10 +1,13 @@
using System.ComponentModel;
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 Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders;
@ -13,9 +16,112 @@ namespace Elsa.Http;
/// <summary>
/// Send an HTTP request.
/// </summary>
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)]
public class SendHttpRequest : CodeActivity<HttpResponse>
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "Flow HTTP Request", Kind = ActivityKind.Task)]
[PublicAPI]
public class FlowSendHttpRequest : SendHttpRequestBase
{
/// <summary>
/// A list of expected status codes to handle.
/// </summary>
[Input(Description = "A list of expected status codes to handle.", UIHint = InputUIHints.MultiText)]
public Input<ICollection<int>> ExpectedStatusCodes { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
{
var expectedStatusCodes = ExpectedStatusCodes.TryGet(context) ?? new List<int>(0);
var statusCode = (int)response.StatusCode;
var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode);
var outcome = hasMatchingStatusCode ? statusCode.ToString() : "Catch all";
await context.CompleteActivityWithOutcomesAsync(outcome);
}
}
/// <summary>
/// Send an HTTP request.
/// </summary>
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)]
[PublicAPI]
public class SendHttpRequest : SendHttpRequestBase
{
/// <summary>
/// A list of expected status codes to handle and the corresponding activity to execute when the status code matches.
/// </summary>
[Input(
Description = "A list of expected status codes to handle and the corresponding activity to execute when the status code matches.",
UIHint = InputUIHints.MultiText,
// TODO: Need to implement a custom UI hint for this.
IsBrowsable = false
)]
public ICollection<HttpStatusCodeCase> ExpectedStatusCodes { get; set; } = new List<HttpStatusCodeCase>();
/// <summary>
/// The activity to execute when the HTTP status code does not match any of the expected status codes.
/// </summary>
[Port]
[Browsable(false)] // TODO: Need to implement a custom UI hint for this.
public IActivity? CatchAll { get; set; }
/// <inheritdoc />
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 ?? CatchAll;
await context.ScheduleActivityAsync(activity, OnChildActivityCompletedAsync);
}
private async ValueTask OnChildActivityCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext)
{
await context.CompleteActivityAsync();
}
}
/// <summary>
/// A binding between an HTTP status code and an activity.
/// </summary>
public class HttpStatusCodeCase
{
/// <summary>
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
/// </summary>
[JsonConstructor]
public HttpStatusCodeCase()
{
}
/// <summary>
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
/// </summary>
public HttpStatusCodeCase(int statusCode, IActivity activity)
{
StatusCode = statusCode;
Activity = activity;
}
/// <summary>
/// The HTTP status code to match.
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// The activity to execute when the HTTP status code matches.
/// </summary>
public IActivity? Activity { get; set; }
}
/// <summary>
/// Base class for activities that send HTTP requests.
/// </summary>
public abstract class SendHttpRequestBase : Activity<HttpResponse>
{
/// <summary>
/// The URL to send the request to.
/// </summary>
[Input] public Input<Uri?> Url { get; set; } = default!;
/// <summary>
@ -72,15 +178,22 @@ public class SendHttpRequest : CodeActivity<HttpResponse>
{
var request = PrepareRequest(context);
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequest));
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);
}
/// <summary>
/// Handles the response.
/// </summary>
protected abstract ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response);
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent)
{
if (!HasContent(httpContent))
@ -90,10 +203,10 @@ public class SendHttpRequest : CodeActivity<HttpResponse>
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)

View file

@ -48,7 +48,7 @@ public class HttpFeature : FeatureBase
public Func<IServiceProvider, IHttpEndpointWorkflowFaultHandler> HttpEndpointWorkflowFaultHandler { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<DefaultHttpEndpointWorkflowFaultHandler>;
/// <summary>
/// A delegate to configure the <see cref="HttpClient"/> used when by the <see cref="SendHttpRequest"/> activity.
/// A delegate to configure the <see cref="HttpClient"/> used when by the <see cref="FlowSendHttpRequest"/> activity.
/// </summary>
public Action<IServiceProvider, HttpClient> HttpClient { get; set; } = (_, _) => { };
@ -84,7 +84,7 @@ public class HttpFeature : FeatureBase
Services.Configure(configureOptions);
var httpClientBuilder = Services.AddHttpClient<SendHttpRequest>(HttpClient);
var httpClientBuilder = Services.AddHttpClient<SendHttpRequestBase>(HttpClient);
HttpClientBuilder(httpClientBuilder);
Services

View file

@ -15,6 +15,9 @@ public class DefaultHttpEndpointWorkflowFaultHandler : IHttpEndpointWorkflowFaul
{
private readonly SerializerOptionsProvider _serializerOptionsProvider;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultHttpEndpointWorkflowFaultHandler"/> class.
/// </summary>
public DefaultHttpEndpointWorkflowFaultHandler(SerializerOptionsProvider serializerOptionsProvider)
{
_serializerOptionsProvider = serializerOptionsProvider;

View file

@ -1,6 +1,7 @@
using System.Text.Json;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Serialization.Converters;
namespace Elsa.Workflows.Core.Expressions;
@ -19,6 +20,7 @@ public class JsonExpression<T> : JsonExpression
public class JsonExpressionHandler : IExpressionHandler
{
/// <inheritdoc />
public ValueTask<object?> EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context)
{
var jsonExpression = (JsonExpression)expression;
@ -27,7 +29,10 @@ public class JsonExpressionHandler : IExpressionHandler
if (string.IsNullOrWhiteSpace(value))
return ValueTask.FromResult(default(object?));
var model = JsonSerializer.Deserialize(value, returnType);
var serializerOptions = new JsonSerializerOptions();
serializerOptions.Converters.Add(new IntegerConverter());
var model = JsonSerializer.Deserialize(value, returnType, serializerOptions);
return ValueTask.FromResult(model);
}
}

View file

@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Elsa.Workflows.Core.Serialization.Converters;
/// <summary>
/// Converts integers to and from JSON strings.
/// </summary>
public class IntegerConverter : JsonConverter<int>
{
/// <inheritdoc />
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Read the JSON string value and parse it as an integer
var value = reader.GetString()!;
var integer = int.Parse(value);
// Return the parsed integer
return integer;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
// Write the integer as a JSON number
writer.WriteNumberValue(value);
}
}

View file

@ -3,7 +3,6 @@ using Elsa.Http;
using Elsa.Workflows.Core.Abstractions;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Services;
using Microsoft.AspNetCore.Http;
namespace Elsa.Samples.OutboundHttpRequests.Workflows;