Add Conductor project and SendCommand activity

This commit is contained in:
Sipke Schoorstra 2021-05-30 15:04:33 +02:00
parent 0f8a0e9f3d
commit 589cab520b
21 changed files with 363 additions and 4 deletions

View file

@ -297,6 +297,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Providers.Redis", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DistributedLocking.Redis", "src\locking\Elsa.DistributedLocking.Redis\Elsa.DistributedLocking.Redis.csproj", "{D1644D32-AA51-4D10-942E-F2722DB963B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Conductor", "src\activities\Elsa.Activities.Conductor\Elsa.Activities.Conductor.csproj", "{E62077E1-59F4-4937-B45B-59F86417BCF8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleClientApp.Web", "src\samples\server\SampleClientApp.Web\SampleClientApp.Web.csproj", "{7756E22A-AD59-46C1-A019-1D28D7217347}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -695,6 +699,14 @@ Global
{D1644D32-AA51-4D10-942E-F2722DB963B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1644D32-AA51-4D10-942E-F2722DB963B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1644D32-AA51-4D10-942E-F2722DB963B4}.Release|Any CPU.Build.0 = Release|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Release|Any CPU.Build.0 = Release|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -830,6 +842,8 @@ Global
{EC5D4AFD-3F7F-4B51-9C38-F18C60618731} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925}
{9ACA06DA-9AFE-41A2-8109-EFED8B9B14A1} = {EC5D4AFD-3F7F-4B51-9C38-F18C60618731}
{D1644D32-AA51-4D10-942E-F2722DB963B4} = {DBBD242E-4437-4BDB-919F-A70839BE75FA}
{E62077E1-59F4-4937-B45B-59F86417BCF8} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{7756E22A-AD59-46C1-A019-1D28D7217347} = {BBCFC623-2113-464D-8A1F-3CCDABF1241C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}

View file

@ -0,0 +1,46 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Design;
using Elsa.Expressions;
using Elsa.Services;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Conductor
{
[Activity(
Category = "Conductor",
Description = "Sends a command to your application.",
Outcomes = new[] { OutcomeNames.Done }
)]
public class SendCommand : Activity
{
private readonly IEventPublisher _eventPublisher;
public SendCommand(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
[ActivityInput(
Label = "Command",
Hint = "The command to send.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string CommandName { get; set; } = default!;
[ActivityInput(
UIHint = ActivityInputUIHints.MultiLine,
Hint = "Optional data to send to your application.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json })]
public object? Payload { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
await _eventPublisher.PublishAsync(new SendCommandModel(CommandName, Payload));
return Done();
}
}
}

View file

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Services;
using Rebus.Handlers;
namespace Elsa.Activities.Conductor.Consumers
{
public class SendCommandConsumer : IHandleMessages<SendCommandModel>
{
private readonly RemoteApplicationClient _remoteApplicationClient;
public SendCommandConsumer(RemoteApplicationClient remoteApplicationClient) => _remoteApplicationClient = remoteApplicationClient;
public async Task Handle(SendCommandModel message) => await _remoteApplicationClient.SendCommandAsync(message.Command, message.Payload);
}
}

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Description>
Elsa is a set of workflow libraries and tools that enable lean and mean workflowing capabilities in any .NET Core application.
This package provides activities to implement conductor-style workflows where Elsa sends commands to your application, which responds asynchronously with events.
</Description>
<PackageTags>elsa, workflows, conductor</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="5.0.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,37 @@
using System;
using Elsa.Activities.Conductor.Consumers;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Options;
using Elsa.Activities.Conductor.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Polly;
namespace Elsa.Activities.Conductor.Extensions
{
public static class ElsaOptionsBuilderExtensions
{
public static ElsaOptionsBuilder AddConductorActivities(this ElsaOptionsBuilder elsa, Action<ConductorOptions>? configureOptions = default, Action<IHttpClientBuilder>? configureHttpClient = default)
{
var services = elsa.Services;
if (configureOptions != null)
services.Configure(configureOptions);
var httpClientBuilder = services.AddHttpClient<RemoteApplicationClient>((sp, httpClient) =>
{
var options = sp.GetRequiredService<IOptions<ConductorOptions>>().Value;
httpClient.BaseAddress = options.ApplicationHookUrl;
});
if (configureHttpClient == null)
httpClientBuilder.AddTransientHttpErrorPolicy(x => x.WaitAndRetryAsync(10, retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount))));
else
configureHttpClient(httpClientBuilder);
elsa.AddActivitiesFrom<SendCommand>();
elsa.AddCompetingConsumer<SendCommandConsumer, SendCommandModel>();
return elsa;
}
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
}

View file

@ -0,0 +1,4 @@
namespace Elsa.Activities.Conductor.Models
{
public record SendCommandModel(string Command, object? Payload);
}

View file

@ -0,0 +1,23 @@
using System;
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Serialization.JsonNet;
namespace Elsa.Activities.Conductor.Options
{
public class ConductorOptions
{
public ConductorOptions()
{
SerializerSettings = new JsonSerializerSettings();
SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
}
/// <summary>
/// The URL to post commands to.
/// </summary>
public Uri ApplicationHookUrl { get; set; } = default!;
public JsonSerializerSettings SerializerSettings { get; set; }
}
}

View file

@ -0,0 +1,38 @@
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Options;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Elsa.Activities.Conductor.Services
{
public class RemoteApplicationClient
{
private readonly HttpClient _httpClient;
private readonly ConductorOptions _options;
public RemoteApplicationClient(HttpClient httpClient, IOptions<ConductorOptions> options)
{
_httpClient = httpClient;
_options = options.Value;
}
public async Task SendCommandAsync(string command, object? payload, CancellationToken cancellationToken = default)
{
payload ??= new object();
var model = new
{
Command = command,
Payload = payload
};
var json = JsonConvert.SerializeObject(model, _options.SerializerSettings);
var content = new StringContent(json, Encoding.UTF8, "application/json");
await _httpClient.PostAsync("", content, cancellationToken);
}
}
}

View file

@ -64,9 +64,10 @@ namespace Elsa.Activities.Workflows
public string? ContextId { get; set; }
[ActivityInput(
UIHint = ActivityInputUIHints.Json,
UIHint = ActivityInputUIHints.MultiLine,
Hint = "Optional custom attributes to associate with the workflow to run.",
Category = PropertyCategories.Advanced)]
Category = PropertyCategories.Advanced,
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json })]
public Variables? CustomAttributes { get; set; } = default!;
[ActivityInput(

View file

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Conductor\Elsa.Activities.Conductor.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Email\Elsa.Activities.Email.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Temporal.Hangfire\Elsa.Activities.Temporal.Hangfire.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Temporal.Quartz\Elsa.Activities.Temporal.Quartz.csproj" />

View file

@ -1,3 +1,4 @@
using Elsa.Activities.Conductor.Extensions;
using Elsa.Activities.UserTask.Extensions;
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Elsa.Persistence.EntityFramework.Sqlite;
@ -16,8 +17,6 @@ namespace Elsa.Samples.Server.Host
{
Environment = environment;
Configuration = configuration;
//BsonClassMap.RegisterClassMap<HttpRequestModel>(cm => { cm.AutoMap(); });
}
private IWebHostEnvironment Environment { get; }
@ -58,6 +57,7 @@ namespace Elsa.Samples.Server.Host
//.AddHangfireTemporalActivities(hangfire => hangfire.UseSqlServerStorage(sqlServerConnectionString), (_, hangfireServer) => hangfireServer.SchedulePollingInterval = TimeSpan.FromSeconds(5))
.AddJavaScriptActivities()
.AddUserTaskActivities()
.AddConductorActivities(options => elsaSection.GetSection("Conductor").Bind(options))
.AddActivitiesFrom<Startup>()
.AddWorkflowsFrom<Startup>()
);

View file

@ -24,6 +24,9 @@
"Host": "localhost",
"Port": "2525",
"DefaultSender": "noreply@acme.com"
},
"Conductor": {
"ApplicationHookUrl": "https://localhost:16001/elsa-hook"
}
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace SampleClientApp.Web.Controllers
{
[ApiController]
[Route("elsa-hook")]
public class ElsaHookController : ControllerBase
{
[HttpPost]
public IActionResult Post(ElsaCommand model)
{
Console.WriteLine("Received command {0} with payload {1}", model.Command, model.Payload);
return Ok();
}
}
public record ElsaCommand(string Command, JsonElement Payload);
}

View file

@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace SampleClientApp.Web
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}

View file

@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "https://localhost:16001",
"sslPort": 16001
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"SampleClientApp.Web": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:16001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace SampleClientApp.Web
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View file

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}