MassTransit activities (#3613)

* Implement MessageReceived for MT

* Implement Publish activity provider

* Only create Publish activity for class types

* Add MassTransit sample project

* Fix comment
This commit is contained in:
Sipke Schoorstra 2023-01-10 14:32:14 +01:00 committed by GitHub
parent 0220b8c891
commit 2c65081110
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 686 additions and 218 deletions

View file

@ -150,6 +150,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Webhooks.Workf
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Webhooks.ExternalApp", "src\samples\aspnet\Elsa.Samples.Webhooks.ExternalApp\Elsa.Samples.Webhooks.ExternalApp.csproj", "{036D287A-33E1-4B28-BE13-14AEA16BC91F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.MassTransitActivities", "src\samples\aspnet\Elsa.Samples.MassTransitActivities\Elsa.Samples.MassTransitActivities.csproj", "{A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -380,6 +382,10 @@ Global
{036D287A-33E1-4B28-BE13-14AEA16BC91F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{036D287A-33E1-4B28-BE13-14AEA16BC91F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{036D287A-33E1-4B28-BE13-14AEA16BC91F}.Release|Any CPU.Build.0 = Release|Any CPU
{A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@ -445,5 +451,6 @@ Global
{2BBFDE36-28A7-4875-8EBE-CC25C76B97FF} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{130A7A00-A9AF-4EA8-8107-BBEA07F166DF} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{036D287A-33E1-4B28-BE13-14AEA16BC91F} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{A1A8AD89-C9C4-41BF-BBCB-EF0544A28BFB} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
EndGlobalSection
EndGlobal

View file

@ -1,70 +0,0 @@
using Elsa.Extensions;
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
using Elsa.WorkflowServer.Web.Models;
namespace Elsa.WorkflowServer.Web.Activities;
[Activity("Demo", "Models the entire process of ordering, preparing, and delivering a pizza.")]
public class GetPizza : Composite<Pizza>
{
[Input(
UIHint = InputUIHints.Dropdown,
Options = new[] { "Margaritha", "Fungi", "Veggie", "Carbonara", "Pepperoni", "Hawaii" },
DefaultValue = "Margaritha"
)]
public Input<string> Flavor { get; set; } = new("Margaritha");
[Input(
UIHint = InputUIHints.Dropdown,
Options = new[] { 20, 30, 40, 80 }
)]
public Input<int> Size { get; set; } = default!;
public GetPizza()
{
Root = new Sequence
{
Activities =
{
new WriteLine("Submitting order..."),
Delay.FromSeconds(2),
new WriteLine("Order submitted"),
Delay.FromSeconds(2),
new Fork
{
JoinMode = ForkJoinMode.WaitAll,
Branches =
{
new Sequence
{
Activities =
{
new WriteLine("Heating oven..."),
Delay.FromSeconds(2),
new WriteLine("Oven heated."),
}
},
new Sequence
{
Activities =
{
new WriteLine("Preparing dough and toppings..."),
Delay.FromSeconds(2),
new WriteLine("Pizza ready for heating."),
}
}
}
},
new WriteLine("Heating pizza in oven..."),
Delay.FromSeconds(2),
new WriteLine("Pizza is ready for delivery!"),
Delay.FromSeconds(2),
Inline(context => context.Set(Result, new Pizza(Size.Get(context), Flavor.Get(context)))),
}
};
}
}

View file

@ -1,23 +0,0 @@
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
namespace Elsa.WorkflowServer.Web.Activities;
/// <summary>
/// A sample activity that simulates doing some very heavy lifting.
/// </summary>
[Activity("Demo", "Demo", "Simulates very heavy lifting, which takes 15 seconds to complete.", Kind = ActivityKind.Task)]
public class ProcessVideo : Activity
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
const int frameCount = 15;
Console.WriteLine("Begin processing...");
for(var frame = 0; frame < frameCount; frame++)
{
await Task.Delay(500);
Console.WriteLine("Processing frame {0}", frame + 1);
}
Console.WriteLine("Finished processing");
}
}

View file

@ -1,20 +0,0 @@
using Elsa.Jobs.Abstractions;
using Elsa.Jobs.Activities.Features;
using Elsa.Jobs.Models;
using Elsa.Jobs.Services;
namespace Elsa.WorkflowServer.Web.Jobs;
/// <summary>
/// Jobs can be scheduled manually using <see cref="IJobQueue"/>,
/// but when enabling the <see cref="JobActivitiesFeature"/>, these jobs become available as activities too.
/// </summary>
public class IndexBlockchainJob : Job
{
protected override async ValueTask ExecuteAsync(JobExecutionContext context)
{
Console.WriteLine("Indexing blockchain...");
await Task.Delay(1000);
Console.WriteLine("Finished indexing blockchain.");
}
}

View file

@ -1,3 +0,0 @@
namespace Elsa.WorkflowServer.Web.Models;
public record Pizza(int Size, string Flavor);

View file

@ -1,6 +0,0 @@
namespace Elsa.WorkflowServer.Web.Models;
/// <summary>
/// A user class for security demo purposes.
/// </summary>
public record User(string FullName);

View file

@ -1,15 +1,10 @@
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.Extensions;
using Elsa.Identity;
using Elsa.Identity.Options;
using Elsa.Jobs.Activities.Services;
using Elsa.EntityFrameworkCore.Modules.ActivityDefinitions;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Requirements;
using Elsa.WorkflowServer.Web.Jobs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Data.Sqlite;
using Proto.Persistence.Sqlite;
@ -17,7 +12,6 @@ var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!;
var identityOptions = new IdentityOptions();
var identityTokenOptions = new IdentityTokenOptions();
var identitySection = configuration.GetSection("Identity");
@ -43,13 +37,12 @@ services
runtime.UseAsyncWorkflowStateExporter();
runtime.UseMassTransitDispatcher();
})
.UseMassTransit(massTransit => massTransit.UseRabbitMq(rabbitMqConnectionString))
.UseLabels(labels => labels.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseActivityDefinitions(feature => feature.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseJobs(jobs => jobs.ConfigureOptions = options => options.WorkerCount = 10)
.UseJobActivities()
.UseScheduling()
.UseWorkflowsApi()
.UseWorkflowsApi(api => api.AddFastEndpointsAssembly<Program>())
.UseJavaScript()
.UseLiquid()
.UseHttp()
@ -62,11 +55,6 @@ services.AddHttpContextAccessor();
// Configure middleware pipeline.
var app = builder.Build();
var serviceProvider = app.Services;
// Register a dummy job for demo purposes.
var jobRegistry = serviceProvider.GetRequiredService<IJobRegistry>();
jobRegistry.Add(typeof(IndexBlockchainJob));
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();

View file

@ -8,8 +8,7 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;",
"RabbitMq": "rabbitmq://guest:guest@localhost"
"Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;"
},
"Identity": {
"CreateDefaultAdmin": true,

View file

@ -193,8 +193,6 @@ export namespace Components {
interface ElsaMultiTextInput {
"inputContext": ActivityInputContext;
}
interface ElsaNewButton {
}
interface ElsaNotificationsManager {
"modalState": boolean;
}
@ -382,10 +380,6 @@ export interface ElsaMonacoEditorCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLElsaMonacoEditorElement;
}
export interface ElsaNewButtonCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLElsaNewButtonElement;
}
export interface ElsaPagerCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLElsaPagerElement;
@ -619,12 +613,6 @@ declare global {
prototype: HTMLElsaMultiTextInputElement;
new (): HTMLElsaMultiTextInputElement;
};
interface HTMLElsaNewButtonElement extends Components.ElsaNewButton, HTMLStencilElement {
}
var HTMLElsaNewButtonElement: {
prototype: HTMLElsaNewButtonElement;
new (): HTMLElsaNewButtonElement;
};
interface HTMLElsaNotificationsManagerElement extends Components.ElsaNotificationsManager, HTMLStencilElement {
}
var HTMLElsaNotificationsManagerElement: {
@ -830,7 +818,6 @@ declare global {
"elsa-monaco-editor": HTMLElsaMonacoEditorElement;
"elsa-multi-line-input": HTMLElsaMultiLineInputElement;
"elsa-multi-text-input": HTMLElsaMultiTextInputElement;
"elsa-new-button": HTMLElsaNewButtonElement;
"elsa-notifications-manager": HTMLElsaNotificationsManagerElement;
"elsa-pager": HTMLElsaPagerElement;
"elsa-panel": HTMLElsaPanelElement;
@ -1029,9 +1016,6 @@ declare namespace LocalJSX {
interface ElsaMultiTextInput {
"inputContext"?: ActivityInputContext;
}
interface ElsaNewButton {
"onNewClicked"?: (event: ElsaNewButtonCustomEvent<any>) => void;
}
interface ElsaNotificationsManager {
"modalState"?: boolean;
}
@ -1189,7 +1173,6 @@ declare namespace LocalJSX {
"elsa-monaco-editor": ElsaMonacoEditor;
"elsa-multi-line-input": ElsaMultiLineInput;
"elsa-multi-text-input": ElsaMultiTextInput;
"elsa-new-button": ElsaNewButton;
"elsa-notifications-manager": ElsaNotificationsManager;
"elsa-pager": ElsaPager;
"elsa-panel": ElsaPanel;
@ -1255,7 +1238,6 @@ declare module "@stencil/core" {
"elsa-monaco-editor": LocalJSX.ElsaMonacoEditor & JSXBase.HTMLAttributes<HTMLElsaMonacoEditorElement>;
"elsa-multi-line-input": LocalJSX.ElsaMultiLineInput & JSXBase.HTMLAttributes<HTMLElsaMultiLineInputElement>;
"elsa-multi-text-input": LocalJSX.ElsaMultiTextInput & JSXBase.HTMLAttributes<HTMLElsaMultiTextInputElement>;
"elsa-new-button": LocalJSX.ElsaNewButton & JSXBase.HTMLAttributes<HTMLElsaNewButtonElement>;
"elsa-notifications-manager": LocalJSX.ElsaNotificationsManager & JSXBase.HTMLAttributes<HTMLElsaNotificationsManagerElement>;
"elsa-pager": LocalJSX.ElsaPager & JSXBase.HTMLAttributes<HTMLElsaPagerElement>;
"elsa-panel": LocalJSX.ElsaPanel & JSXBase.HTMLAttributes<HTMLElsaPanelElement>;

View file

@ -1,3 +1,6 @@
using System.Collections.Concurrent;
using System.Reflection;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
@ -6,6 +9,21 @@ namespace Elsa.Extensions;
/// </summary>
public static class TypeExtensions
{
private static readonly ConcurrentDictionary<Type, string> SimpleAssemblyQualifiedTypeNameCache = new();
/// <summary>
/// Gets the assembly-qualified name of the type, without any version info etc.
/// E.g. "System.String, System.Private.CoreLib"
/// </summary>
public static string GetSimpleAssemblyQualifiedName(this Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, GetSimpleAssemblyQualifiedNameInternal);
}
private static string GetSimpleAssemblyQualifiedNameInternal(Type type) => $"{type.FullName}, {Assembly.GetAssembly(type)!.GetName().Name}";
/// <summary>
/// Returns the default value for the specified type.
/// </summary>

View file

@ -47,6 +47,7 @@ public static class ObjectConverter
options.Converters.Add(new JsonStringEnumConverter());
var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
if (value is JsonElement jsonNumber && jsonNumber.ValueKind == JsonValueKind.Number && underlyingTargetType == typeof(string))
return jsonNumber.ToString().ConvertTo(underlyingTargetType);
@ -55,9 +56,24 @@ public static class ObjectConverter
{
if (jsonObject.ValueKind == JsonValueKind.String && underlyingTargetType != typeof(string))
return jsonObject.GetString().ConvertTo(underlyingTargetType);
return jsonObject.Deserialize(targetType, options);
}
if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive)
{
var stringValue = (string)value;
try
{
if (stringValue.TrimStart().StartsWith("{"))
return JsonSerializer.Deserialize(stringValue, underlyingTargetType);
}
catch (Exception e)
{
throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", value, underlyingTargetType, e);
}
}
if (targetType == typeof(object))
return value;
@ -65,8 +81,6 @@ public static class ObjectConverter
if (underlyingTargetType.IsInstanceOfType(value))
return value;
var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
if (underlyingSourceType == underlyingTargetType)
return value;
@ -94,20 +108,7 @@ public static class ObjectConverter
return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int)));
}
if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive)
{
var stringValue = (string)value;
try
{
if (stringValue.TrimStart().StartsWith("{"))
return JsonSerializer.Deserialize(stringValue, underlyingTargetType);
}
catch (Exception e)
{
throw new TypeConversionException($"Failed to deserialize {stringValue} to {underlyingTargetType}", value, underlyingTargetType, e);
}
}
if (value is string s && string.IsNullOrWhiteSpace(s))
return null;

View file

@ -9,26 +9,51 @@ using Elsa.Workflows.Runtime.Entities;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds extensions to <see cref="IRouteTable"/>.
/// </summary>
public static class RouteTableExtensions
{
private static readonly JsonSerializerOptions SerializerOptions;
static RouteTableExtensions()
{
SerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Adds routes from the specified set of triggers.
/// </summary>
public static void AddRoutes(this IRouteTable routeTable, IEnumerable<StoredTrigger> triggers)
{
var paths = Filter(triggers).Select(Deserialize).Select(x => x.Path).ToList();
var paths = Filter(triggers).Select(Deserialize).Select(x => x.Path).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
routeTable.AddRange(paths);
}
/// <summary>
/// Adds routes from the specified set of bookmarks.
/// </summary>
public static void AddRoutes(this IRouteTable routeTable, IEnumerable<Bookmark> bookmarks)
{
var paths = Filter(bookmarks).Select(Deserialize).Select(x => x.Path).ToList();
routeTable.AddRange(paths);
}
/// <summary>
/// Removes routes from the specified set of triggers.
/// </summary>
public static void RemoveRoutes(this IRouteTable routeTable, IEnumerable<StoredTrigger> triggers)
{
var paths = Filter(triggers).Select(Deserialize).Select(x => x.Path).ToList();
routeTable.RemoveRange(paths);
}
/// <summary>
/// Removes routes from the specified set of bookmarks.
/// </summary>
public static void RemoveRoutes(this IRouteTable routeTable, IEnumerable<Bookmark> bookmarks)
{
var paths = Filter(bookmarks).Select(Deserialize).Select(x => x.Path).ToList();
@ -39,5 +64,5 @@ public static class RouteTableExtensions
private static IEnumerable<Bookmark> Filter(IEnumerable<Bookmark> triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>());
private static HttpEndpointBookmarkPayload Deserialize(StoredTrigger trigger) => Deserialize(trigger.Data!);
private static HttpEndpointBookmarkPayload Deserialize(Bookmark bookmark) => Deserialize(bookmark.Data!);
private static HttpEndpointBookmarkPayload Deserialize(string model) => JsonSerializer.Deserialize<HttpEndpointBookmarkPayload>(model)!;
private static HttpEndpointBookmarkPayload Deserialize(string model) => JsonSerializer.Deserialize<HttpEndpointBookmarkPayload>(model, SerializerOptions)!;
}

View file

@ -5,26 +5,38 @@ using Microsoft.Extensions.Caching.Memory;
namespace Elsa.Http.Implementations;
/// <inheritdoc />
public class RouteTable : IRouteTable
{
private static readonly object Key = new();
private readonly IMemoryCache _cache;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="cache"></param>
public RouteTable(IMemoryCache cache) => _cache = cache;
private ConcurrentDictionary<string, string> Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary<string, string>());
private ConcurrentDictionary<string, string> Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary<string, string>())!;
/// <inheritdoc />
public void Add(string path) => Routes.TryAdd(path, path);
/// <inheritdoc />
public void Remove(string path) => Routes.TryRemove(path, out _);
/// <inheritdoc />
public void AddRange(IEnumerable<string> paths)
{
foreach (var path in paths) Add(path);
}
/// <inheritdoc />
public void RemoveRange(IEnumerable<string> paths)
{
foreach (var path in paths) Remove(path);
}
/// <inheritdoc />
public IEnumerator<string> GetEnumerator() => Routes.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View file

@ -9,7 +9,7 @@ public class JsonElementHttpResponseContentReader : IHttpResponseContentReader
public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase);
public async Task<object> ReadAsync(HttpResponseMessage response, object context, CancellationToken cancellationToken)
{
var json = (await response.Content.ReadAsStringAsync()).Trim();
var json = (await response.Content.ReadAsStringAsync(cancellationToken)).Trim();
return JsonDocument.Parse(json).RootElement;
}
}

View file

@ -3,6 +3,7 @@ namespace Elsa.Jobs.Activities.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class JobAttribute : Attribute
{
/// <inheritdoc />
public JobAttribute(string @namespace, string? category, string? description = default)
{
Namespace = @namespace;
@ -10,6 +11,7 @@ public class JobAttribute : Attribute
Category = category;
}
/// <inheritdoc />
public JobAttribute(string @namespace, string? description = default)
{
Namespace = @namespace;
@ -17,6 +19,7 @@ public class JobAttribute : Attribute
Category = @namespace;
}
/// <inheritdoc />
public JobAttribute(string @namespace, string? activityType, string? description = default, string? category = default)
{
Namespace = @namespace;

View file

@ -30,6 +30,7 @@ public class JobActivityProvider : IActivityProvider
_jobRegistry = jobRegistry;
}
/// <inheritdoc />
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
var jobTypes = _jobRegistry.List();

View file

@ -0,0 +1,58 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.MassTransit.Implementations;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
namespace Elsa.MassTransit.Activities;
/// <summary>
/// A generic activity that waits for a message of a given type to be received. Used by the <see cref="MassTransitActivityTypeProvider"/>.
/// </summary>
[Browsable(false)]
public class MessageReceived : Trigger<object>
{
internal const string InputKey = "Message";
/// <inheritdoc />
[JsonConstructor]
public MessageReceived()
{
}
/// <summary>
/// The message type to receive.
/// </summary>
public Type MessageType { get; set; } = default!;
/// <inheritdoc />
protected override object GetTriggerPayload(TriggerIndexingContext context) => GetBookmarkPayload(context.ExpressionExecutionContext);
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark.
if (!context.TryGetInput<object>(InputKey, out var message))
{
// Create bookmarks for when we receive the expected HTTP request.
context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext));
return;
}
// Provide the received message as output.
context.Set(Result, message);
// Complete.
await context.CompleteActivityAsync();
}
private object GetBookmarkPayload(ExpressionExecutionContext context)
{
// Generate bookmark data for message type.
return new MessageReceivedBookmarkPayload(MessageType);
}
}
internal record MessageReceivedBookmarkPayload(Type MessageType);

View file

@ -0,0 +1,43 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.MassTransit.Implementations;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using MassTransit;
using MassTransit.Middleware;
namespace Elsa.MassTransit.Activities;
/// <summary>
/// A generic activity that publishes a message of a given type. Used by the <see cref="MassTransitActivityTypeProvider"/>.
/// </summary>
[Browsable(false)]
public class PublishMessage : Activity
{
/// <inheritdoc />
[JsonConstructor]
public PublishMessage()
{
}
/// <summary>
/// The message type to publish.
/// </summary>
public Type MessageType { get; set; } = default!;
/// <summary>
/// The message to send. Must be a concrete implementation of the configured <see cref="MessageType"/>.
/// </summary>
[Input(Description = "The message to send. Must be a concrete implementation of the configured message type.")]
public Input<object> Message { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var bus = context.GetRequiredService<IBus>();
var message = Message.Get(context).ConvertTo(MessageType)!;
await bus.Publish(message, context.CancellationToken);
}
}

View file

@ -0,0 +1,37 @@
using Elsa.MassTransit.Activities;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Services;
using MassTransit;
namespace Elsa.MassTransit.Consumers;
/// <summary>
/// A consumer of various dispatch message types to asynchronously execute workflows.
/// </summary>
public class WorkflowMessageConsumer<T> : IConsumer<T> where T : class
{
private readonly IWorkflowDispatcher _workflowRuntime;
/// <summary>
/// Constructor.
/// </summary>
public WorkflowMessageConsumer(IWorkflowDispatcher workflowRuntime)
{
_workflowRuntime = workflowRuntime;
}
/// <inheritdoc />
public async Task Consume(ConsumeContext<T> context)
{
var cancellationToken = context.CancellationToken;
var messageType = typeof(T);
var message = context.Message;
var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(messageType);
var bookmark = new MessageReceivedBookmarkPayload(messageType);
var correlationId = context.CorrelationId?.ToString();
var input = new Dictionary<string, object> { [MessageReceived.InputKey] = message };
var request = new DispatchTriggerWorkflowsRequest(activityTypeName, bookmark, correlationId, input);
await _workflowRuntime.DispatchAsync(request, cancellationToken);
}
}

View file

@ -1,5 +1,6 @@
using Elsa.Features.Services;
using Elsa.MassTransit.Features;
using Elsa.MassTransit.Implementations;
using MassTransit;
// ReSharper disable once CheckNamespace
@ -11,6 +12,7 @@ namespace Elsa.Extensions;
public static class MassTransitFeatureExtensions
{
private static readonly object ServiceBusConsumerTypesKey = new();
private static readonly object MessageTypesKey = new();
/// <summary>
/// Registers the specified type for MassTransit service bus consumer discovery.
@ -26,9 +28,29 @@ public static class MassTransitFeatureExtensions
types.Add(type);
return feature;
}
/// <summary>
/// Registers a message type which is to be used by the <see cref="MassTransitActivityTypeProvider"/> to dynamically provide activities to send and receive these messages.
/// </summary>
public static MassTransitFeature AddMessageType<T>(this MassTransitFeature feature) where T : class => feature.AddMessageType(typeof(T));
/// <summary>
/// Returns all collected types for discovery of service bus consumers.
/// Registers a message type which is to be used by the <see cref="MassTransitActivityTypeProvider"/> to dynamically provide activities to send and receive these messages.
/// </summary>
public static MassTransitFeature AddMessageType(this MassTransitFeature feature, Type type)
{
var types = feature.Module.Properties.GetOrAdd(MessageTypesKey, () => new HashSet<Type>());
types.Add(type);
return feature;
}
/// <summary>
/// Returns all collected consumer types.
/// </summary>
internal static IEnumerable<Type> GetConsumers(this MassTransitFeature feature) => feature.Module.Properties.GetOrAdd(ServiceBusConsumerTypesKey, () => new HashSet<Type>());
/// <summary>
/// Returns all collected message types.
/// </summary>
internal static IEnumerable<Type> GetMessages(this MassTransitFeature feature) => feature.Module.Properties.GetOrAdd(MessageTypesKey, () => new HashSet<Type>());
}

View file

@ -1,10 +1,20 @@
using System.ComponentModel;
using System.Reflection;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.MassTransit.Consumers;
using Elsa.MassTransit.Implementations;
using Elsa.MassTransit.Options;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Options;
using Humanizer;
using MassTransit;
using MassTransit.Serialization;
using MassTransit.Serialization.JsonConverters;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.MassTransit.Features;
@ -22,7 +32,7 @@ public class MassTransitFeature : FeatureBase
/// <summary>
/// A delegate that can be set to configure MassTransit's <see cref="IBusRegistrationConfigurator"/>.
/// </summary>
public Action<IBusRegistrationConfigurator>? BusConfigurator { get; set; }
public Action<IBusRegistrationConfigurator>? BusConfigurator { get; set; }
/// <inheritdoc />
public override void Configure()
@ -31,12 +41,36 @@ public class MassTransitFeature : FeatureBase
{
configure.UsingInMemory((context, configurator) => { configurator.ConfigureEndpoints(context); });
};
}
/// <inheritdoc />
public override void Apply()
{
var messageTypes = this.GetMessages();
Services.AddActivityProvider<MassTransitActivityTypeProvider>();
AddMassTransit(BusConfigurator);
// Add collected message types to options.
Services.Configure<MassTransitActivityOptions>(options => options.MessageTypes = new HashSet<Type>(messageTypes));
// Add collected message types as available variable types.
Services.Configure<ManagementOptions>(options =>
{
foreach (var messageType in messageTypes)
{
var activityAttr = messageType.GetCustomAttribute<ActivityAttribute>();
var categoryAttr = messageType.GetCustomAttribute<CategoryAttribute>();
var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit";
var descriptionAttr = messageType.GetCustomAttribute<DescriptionAttribute>();
var description = descriptionAttr?.Description ?? activityAttr?.Description;
options.VariableDescriptors.Add(new VariableDescriptor(messageType, category, description));
}
});
// Configure message serializer.
SystemTextJsonMessageSerializer.Options.Converters.Add(new TypeJsonConverter(new WellKnownTypeRegistry()));
}
/// <summary>
@ -44,7 +78,12 @@ public class MassTransitFeature : FeatureBase
/// </summary>
private void AddMassTransit(Action<IBusRegistrationConfigurator>? config)
{
var consumerTypes = this.GetConsumers().ToArray();
// For each message type, create a concrete WorkflowMessageConsumer<T>.
var workflowMessageConsumerType = typeof(WorkflowMessageConsumer<>);
var workflowMessageConsumers = this.GetMessages().Select(x => workflowMessageConsumerType.MakeGenericType(x));
// Concatenate the manually registered consumers with the workflow message consumers.
var consumerTypes = this.GetConsumers().Concat(workflowMessageConsumers).ToArray();
Services.AddMassTransit(bus =>
{

View file

@ -0,0 +1,137 @@
using System.ComponentModel;
using System.Reflection;
using Elsa.Extensions;
using Elsa.MassTransit.Activities;
using Elsa.MassTransit.Options;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Services;
using Humanizer;
using Microsoft.Extensions.Options;
namespace Elsa.MassTransit.Implementations;
/// <summary>
/// Provides activities to the system from the configured MassTransit message types.
/// </summary>
public class MassTransitActivityTypeProvider : IActivityProvider
{
private readonly IActivityFactory _activityFactory;
private readonly MassTransitActivityOptions _options;
/// <summary>
/// Constructor.
/// </summary>
public MassTransitActivityTypeProvider(IActivityFactory activityFactory, IOptions<MassTransitActivityOptions> options)
{
_activityFactory = activityFactory;
_options = options.Value;
}
/// <inheritdoc />
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
var messageTypes = _options.MessageTypes;
var descriptors = CreateDescriptors(messageTypes).ToList();
return new(descriptors);
}
private IEnumerable<ActivityDescriptor> CreateDescriptors(IEnumerable<Type> messageTypes)
{
foreach (var messageType in messageTypes)
{
yield return CreateMessageReceivedDescriptor(messageType);
if(messageType.IsClass)
yield return CreatePublishMessageDescriptor(messageType);
}
}
private ActivityDescriptor CreateMessageReceivedDescriptor(Type messageType)
{
var activityAttr = messageType.GetCustomAttribute<ActivityAttribute>();
var typeName = activityAttr?.Type ?? messageType.Name;
var fullTypeName = ActivityTypeNameHelper.GenerateTypeName(messageType);
var displayNameAttr = messageType.GetCustomAttribute<DisplayNameAttribute>();
var displayName = displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title);
var categoryAttr = messageType.GetCustomAttribute<CategoryAttribute>();
var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit";
var descriptionAttr = messageType.GetCustomAttribute<DescriptionAttribute>();
var description = descriptionAttr?.Description ?? activityAttr?.Description;
return new()
{
TypeName = fullTypeName,
Version = 1,
DisplayName = displayName,
Description = description,
Category = category,
Kind = ActivityKind.Trigger,
IsBrowsable = true,
ActivityType = typeof(MessageReceived),
Outputs =
{
new OutputDescriptor
{
Description = "The received message",
DisplayName = "Received Message",
Name = nameof(MessageReceived.Result),
Type = typeof(object)
}
},
Constructor = context =>
{
var activity = _activityFactory.Create<MessageReceived>(context);
activity.Type = fullTypeName;
activity.MessageType = messageType;
return activity;
}
};
}
private ActivityDescriptor CreatePublishMessageDescriptor(Type messageType)
{
var activityAttr = messageType.GetCustomAttribute<ActivityAttribute>();
var typeName = activityAttr?.Type ?? messageType.Name;
var ns = ActivityTypeNameHelper.GenerateNamespace(messageType);
var fullTypeName = ns + ".Publish" + typeName;
var displayNameAttr = messageType.GetCustomAttribute<DisplayNameAttribute>();
var displayName = "Publish " + (displayNameAttr?.DisplayName ?? activityAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title));
var categoryAttr = messageType.GetCustomAttribute<CategoryAttribute>();
var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit";
var descriptionAttr = messageType.GetCustomAttribute<DescriptionAttribute>();
var description = descriptionAttr?.Description ?? activityAttr?.Description;
return new()
{
TypeName = fullTypeName,
Version = 1,
DisplayName = displayName,
Description = description,
Category = category,
Kind = ActivityKind.Action,
IsBrowsable = true,
ActivityType = typeof(PublishMessage),
Inputs =
{
new InputDescriptor
{
Description = "The message to publish.",
UIHint = InputUIHints.MultiLine,
DisplayName = "Message",
Type = typeof(Input<object>),
Name = nameof(PublishMessage.Message)
}
},
Constructor = context =>
{
var activity = _activityFactory.Create<PublishMessage>(context);
activity.Type = fullTypeName;
activity.MessageType = messageType;
return activity;
}
};
}
}

View file

@ -0,0 +1,12 @@
namespace Elsa.MassTransit.Options;
/// <summary>
/// Provides settings to the RabbitMQ broker for MassTransit.
/// </summary>
public class MassTransitActivityOptions
{
/// <summary>
/// A set of message types that can be sent and received in the form of workflow activities.
/// </summary>
public ISet<Type> MessageTypes { get; set; } = new HashSet<Type>();
}

View file

@ -5,7 +5,7 @@ namespace Elsa.MassTransit.Options;
/// </summary>
public class RabbitMqOptions
{
public string Host { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Host { get; set; } = default!;
public string Username { get; set; } = default!;
public string Password { get; set; } = default!;
}

View file

@ -1,5 +1,6 @@
using Elsa.Extensions;
using Elsa.Mediator.Services;
using Elsa.ProtoActor.Extensions;
using Elsa.ProtoActor.Grains;
using Elsa.ProtoActor.Protos;
using Elsa.Workflows.Core.Notifications;
@ -32,7 +33,7 @@ internal class UpdateRunningWorkflowsHandler : INotificationHandler<WorkflowExec
{
DefinitionId = workflowState.DefinitionId,
Version = workflowState.DefinitionVersion,
CorrelationId = workflowState.CorrelationId,
CorrelationId = workflowState.CorrelationId.EmptyIfNull(),
InstanceId = workflowState.Id
};

View file

@ -0,0 +1,13 @@
using Elsa.Workflows.Api.Features;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
public static class WorkflowsApiFeatureExtensions
{
public static WorkflowsApiFeature AddFastEndpointsAssembly<TMarker>(this WorkflowsApiFeature feature)
{
feature.Module.AddFastEndpointsAssembly<TMarker>();
return feature;
}
}

View file

@ -24,7 +24,6 @@ public class WorkflowsApiFeature : FeatureBase
/// <inheritdoc />
public override void Configure()
{
Module.AddFastEndpointsAssembly(GetType());
}

View file

@ -1,23 +0,0 @@
using System.Collections.Concurrent;
using System.Reflection;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
public static class TypeExtensions
{
private static readonly ConcurrentDictionary<Type, string> SimpleAssemblyQualifiedTypeNameCache = new();
/// <summary>
/// Gets the assembly-qualified name of the type, without any version info etc.
/// E.g. "System.String, System.Private.CoreLib"
/// </summary>
public static string GetSimpleAssemblyQualifiedName(this Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, GetSimpleAssemblyQualifiedNameInternal);
}
private static string GetSimpleAssemblyQualifiedNameInternal(Type type) => $"{type.FullName}, {Assembly.GetAssembly(type)!.GetName().Name}";
}

View file

@ -111,7 +111,7 @@ public class WorkflowsFeature : FeatureBase
.AddSingleton<IBookmarkHasher, BookmarkHasher>()
.AddSingleton<IIdentityGenerator, GuidIdentityGenerator>()
.AddSingleton<IWorkflowExecutionContextFactory, DefaultWorkflowExecutionContextFactory>()
.AddSingleton<IBookmarkPayloadSerializer, BookmarkPayloadSerializer>()
.AddSingleton<IBookmarkPayloadSerializer>(sp => ActivatorUtilities.CreateInstance<BookmarkPayloadSerializer>(sp))
.AddTransient<WorkflowBuilder>()
.AddSingleton(typeof(Func<IWorkflowBuilder>), sp => () => sp.GetRequiredService<WorkflowBuilder>())
.AddSingleton<IWorkflowBuilderFactory, WorkflowBuilderFactory>()

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Implementations;
@ -7,13 +8,15 @@ public class BookmarkPayloadSerializer : IBookmarkPayloadSerializer
{
private readonly JsonSerializerOptions _settings;
public BookmarkPayloadSerializer()
public BookmarkPayloadSerializer(IWellKnownTypeRegistry wellKnownTypeRegistry)
{
_settings = new JsonSerializerOptions
{
// Enables serialization of ValueTuples, which use fields instead of properties.
IncludeFields = true
};
_settings.Converters.Add(new TypeJsonConverter(wellKnownTypeRegistry));
}
public T Deserialize<T>(string json) where T : notnull => JsonSerializer.Deserialize<T>(json, _settings)!;

View file

@ -14,7 +14,7 @@ public class PolymorphicConverter : JsonConverter<object>
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
var typeName = value.GetType().GetSimpleAssemblyQualifiedName();
var wrappedValue = JsonSerializer.SerializeToNode(value)!;
var wrappedValue = JsonSerializer.SerializeToNode(value, options)!;
wrappedValue["$type"] = typeName;
wrappedValue.WriteTo(writer);
}
@ -25,7 +25,7 @@ public class PolymorphicConverter : JsonConverter<object>
var element = JsonElement.ParseValue(ref reader);
var typeName = element.GetProperty("$type").GetString()!;
var type = Type.GetType(typeName)!;
var value = element.Deserialize(type);
var value = element.Deserialize(type, options);
return value!;
}

View file

@ -5,6 +5,7 @@ using Elsa.Expressions.Services;
using Elsa.Extensions;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Comparers;
@ -30,6 +31,7 @@ public class TriggerIndexer : ITriggerIndexer
private readonly IServiceProvider _serviceProvider;
private readonly IBookmarkHasher _hasher;
private readonly ILogger _logger;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Constructor.
@ -43,6 +45,7 @@ public class TriggerIndexer : ITriggerIndexer
IEventPublisher eventPublisher,
IServiceProvider serviceProvider,
IBookmarkHasher hasher,
SerializerOptionsProvider serializerOptionsProvider,
ILogger<TriggerIndexer> logger)
{
_activityWalker = activityWalker;
@ -54,6 +57,7 @@ public class TriggerIndexer : ITriggerIndexer
_hasher = hasher;
_logger = logger;
_workflowDefinitionService = workflowDefinitionService;
_serializerOptions = serializerOptionsProvider.CreateDefaultOptions();
}
/// <inheritdoc />
@ -152,7 +156,7 @@ public class TriggerIndexer : ITriggerIndexer
Name = triggerTypeName,
ActivityId = trigger.Id,
Hash = _hasher.Hash(triggerTypeName, x),
Data = JsonSerializer.Serialize(x)
Data = JsonSerializer.Serialize(x, _serializerOptions)
});
return triggers.ToList();

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.MassTransit\Elsa.MassTransit.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Designer\Elsa.Workflows.Designer.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Pages\Index.cshtml" />
<AdditionalFiles Include="Pages\_ViewImports.cshtml" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,33 @@
using Elsa.Samples.MassTransitActivities.Messages;
using FastEndpoints;
using MassTransit;
namespace Elsa.Samples.MassTransitActivities.Endpoints.Orders.Create;
public class Create : EndpointWithoutRequest
{
private readonly IBus _bus;
public Create(IBus bus)
{
_bus = bus;
}
public override void Configure()
{
Post("orders");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
await _bus.Publish<OrderCreated>(new
{
Id = Guid.NewGuid().ToString("N"),
CustomerId = "1",
Product = "Pizza",
Quantity = 5,
Total = 62.5
}, cancellationToken);
}
}

View file

@ -0,0 +1,4 @@
namespace Elsa.Samples.MassTransitActivities.Messages;
// ReSharper disable once InconsistentNaming
public record OrderCompleted(string OrderId);

View file

@ -0,0 +1,11 @@
namespace Elsa.Samples.MassTransitActivities.Messages;
// ReSharper disable once InconsistentNaming
public record OrderCreated(
string Id,
string CustomerId,
string Product,
int Quantity,
decimal Total
);

View file

@ -0,0 +1,24 @@
@page
@using Elsa.Workflows.Designer
@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
var serverUrl = Url.Content("elsa/api");
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Elsa Workflows 3.0</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.css">
<script src="_content/Elsa.Workflows.Designer/monaco-editor/min/vs/loader.js"></script>
<script type="module" src="_content/Elsa.Workflows.Designer/elsa-workflows-designer/elsa-workflows-designer.esm.js"></script>
</head>
<body>
<component type="typeof(ElsaStudio)" render-mode="ServerPrerendered" param-ServerUrl="@serverUrl"/>
</body>
</html>

View file

@ -0,0 +1,2 @@
@namespace Elsa.Samples.MassTransitActivities.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View file

@ -0,0 +1,66 @@
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Samples.MassTransitActivities.Messages;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!;
// Add services to the container.
builder.Services.AddElsa(elsa =>
{
// Configure management feature to use EF Core.
elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite()));
// Configure runtime feature to use EF Core.
elsa.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite()));
// Expose API endpoints.
elsa.UseWorkflowsApi();
// Add services for HTTP activities and workflow middleware.
elsa.UseHttp();
// Use JavaScript and Liquid.
elsa.UseJavaScript();
elsa.UseLiquid();
// Configure identity so that we can create a default admin user.
elsa.UseIdentity(identity =>
{
identity.IdentityOptions.CreateDefaultAdmin = builder.Environment.IsDevelopment();
identity.TokenOptions.SigningKey = "secret-token-signing-key";
identity.TokenOptions.Lifetime = TimeSpan.FromDays(1);
});
// Use default authentication (JWT).
elsa.UseDefaultAuthentication();
// Configure MassTransit.
elsa.UseMassTransit(massTransit =>
{
massTransit.UseRabbitMq(rabbitMqConnectionString);
massTransit.AddMessageType<OrderCompleted>();
massTransit.AddMessageType<OrderCreated>();
});
});
builder.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
// Add Razor pages.
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseCors();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.UseWorkflows();
app.MapRazorPages();
app.Run();

View file

@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:38703",
"sslPort": 44342
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5207",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7299;http://localhost:5207",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RabbitMq": "rabbitmq://guest:guest@localhost"
}
}

View file

@ -15,9 +15,4 @@
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Designer\Elsa.Workflows.Designer.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Pages\Index.cshtml" />
<AdditionalFiles Include="Pages\_ViewImports.cshtml" />
</ItemGroup>
</Project>