diff --git a/Directory.Packages.props b/Directory.Packages.props index 209366965..0c351582b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -96,6 +96,7 @@ + diff --git a/NuGet.Config b/NuGet.Config index e5fb919dc..af0e86c2c 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -5,6 +5,7 @@ + @@ -13,5 +14,9 @@ + + + + \ No newline at end of file diff --git a/docker/docker-compose-datadog.yml b/docker/docker-compose-datadog.yml index 3b397263f..d2f742b47 100644 --- a/docker/docker-compose-datadog.yml +++ b/docker/docker-compose-datadog.yml @@ -76,7 +76,7 @@ services: datadog-agent: image: datadog/agent:7 environment: - DD_API_KEY: "YOUR_API_KEY" + DD_API_KEY: "" DD_SITE: "datadoghq.eu" DD_LOGS_ENABLED: "true" DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL: "true" diff --git a/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs b/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs index 42cf941ac..d9ecad297 100644 --- a/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs +++ b/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs @@ -42,7 +42,7 @@ builder.Services.AddElsa(elsa => elsa.UseRealTimeWorkflows(); // Use Webhooks feature. - elsa.UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options)); + elsa.UseWebhooks(webhooks => webhooks.ConfigureSinks = options => builder.Configuration.GetSection("Webhooks:Sinks").Bind(options)); }); builder.Services.AddHealthChecks(); diff --git a/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/appsettings.json b/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/appsettings.json index 7fabd31fb..fffd33f33 100644 --- a/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/appsettings.json +++ b/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/appsettings.json @@ -7,10 +7,13 @@ }, "AllowedHosts": "*", "Webhooks": { - "Endpoints": [ + "Sinks": [ { - "EventTypes": [ - "RunTask" + "Id": "1", + "Filters": [ + { + "EventType": "RunTask" + } ], "Url": "https://localhost:6001/api/webhooks/run-task" } diff --git a/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs b/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs index d874d5377..9033dd6dc 100644 --- a/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs +++ b/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs @@ -21,7 +21,7 @@ builder.Services.AddElsa(elsa => }) .UseWorkflowsApi() .UseDefaultAuthentication(auth => auth.UseAdminApiKey()) - .UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options)); + .UseWebhooks(webhooks => webhooks.ConfigureSinks = options => builder.Configuration.GetSection("Webhooks:Sinks").Bind(options)); }); var app = builder.Build(); diff --git a/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/appsettings.json b/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/appsettings.json index 6ec2431a1..da51ebe72 100644 --- a/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/appsettings.json +++ b/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/appsettings.json @@ -7,10 +7,13 @@ }, "AllowedHosts": "*", "Webhooks": { - "Endpoints": [ + "Sinks": [ { - "EventTypes": [ - "RunTask" + "Id": "1", + "Filters": [ + { + "EventType": "RunTask" + } ], "Url": "https://localhost:5002/webhooks/run-task" } diff --git a/src/apps/Elsa.ServerAndStudio.Web/Program.cs b/src/apps/Elsa.ServerAndStudio.Web/Program.cs index 37f2ea6ac..5aaeb2794 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/Program.cs +++ b/src/apps/Elsa.ServerAndStudio.Web/Program.cs @@ -105,7 +105,7 @@ services http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options); }) .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) - .UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options)) + .UseWebhooks(webhooks => webhooks.ConfigureSinks = options => builder.Configuration.GetSection("Webhooks:Sinks").Bind(options)) .UseWorkflowsApi() .UseRealTimeWorkflows() .AddActivitiesFrom() diff --git a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json index a3dde55ab..d9f27f46c 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json +++ b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json @@ -85,7 +85,7 @@ "BasePath": "/api/workflows" }, "Webhooks": { - "Endpoints": [] + "Sinks": [] }, "CorsPolicy": { "Origins": ["*"], diff --git a/src/modules/Elsa.Webhooks/Commands/InvokeWebhook.cs b/src/modules/Elsa.Webhooks/Commands/InvokeWebhook.cs deleted file mode 100644 index 3482c159a..000000000 --- a/src/modules/Elsa.Webhooks/Commands/InvokeWebhook.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Elsa.Mediator.Contracts; -using Elsa.Webhooks.Models; - -namespace Elsa.Webhooks.Commands; - -/// -/// Represents a command to invoke all registered webhook endpoints. -/// -public record InvokeWebhook(WebhookRegistration WebhookRegistration, WebhookEvent WebhookEvent) : ICommand; \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj b/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj index c1e3f3b7e..d433d0dd2 100644 --- a/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj +++ b/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj @@ -10,6 +10,7 @@ + diff --git a/src/modules/Elsa.Webhooks/Features/WebhooksFeature.cs b/src/modules/Elsa.Webhooks/Features/WebhooksFeature.cs index 7130b74cf..6f7e43c23 100644 --- a/src/modules/Elsa.Webhooks/Features/WebhooksFeature.cs +++ b/src/modules/Elsa.Webhooks/Features/WebhooksFeature.cs @@ -1,18 +1,14 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; -using Elsa.Webhooks.Implementations; -using Elsa.Webhooks.Models; -using Elsa.Webhooks.Options; -using Elsa.Webhooks.Services; using Microsoft.Extensions.DependencyInjection; -using Polly; +using Microsoft.Extensions.Options; +using WebhooksCore; +using WebhooksCore.Options; namespace Elsa.Webhooks.Features; -/// -/// Installs and configures services that let the user register webhook endpoints. -/// +/// Installs and configures webhook services. public class WebhooksFeature : FeatureBase { /// @@ -20,58 +16,33 @@ public class WebhooksFeature : FeatureBase { } - /// - /// A delegate that resolves the to use. - /// - public Func WebhookDispatcher { get; set; } = sp => sp.GetRequiredService(); + public Action> ConfigureSinks { get; set; } = options => { }; - /// - /// A delegate that is invoked when configuring . - /// - public Action WebhookOptions { get; set; } = _ => { }; - - /// - /// A delegate to configure the used when invoking webhook endpoints. - /// - public Action HttpClient { get; set; } = (_, _) => { }; - - /// - /// A delegate to configure the . For example, to configure Polly policies. - /// - public Action HttpClientBuilder { get; set; } = builder => builder.AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(retry))); - - /// - /// Registers the specified webhook with - /// - public WebhooksFeature RegisterWebhook(Uri endpoint) => RegisterWebhook(new WebhookRegistration(endpoint)); - - /// - /// Registers the specified webhook with - /// - public WebhooksFeature RegisterWebhook(WebhookRegistration registration) => RegisterWebhooks(registration); - - /// - /// Registers the specified webhooks with - /// - public WebhooksFeature RegisterWebhooks(params WebhookRegistration[] registrations) + /// Registers the specified webhook with + public WebhooksFeature RegisterWebhookSink(Uri endpoint) { - Services.Configure(options => options.Endpoints.AddRange(registrations)); + var sink = new WebhookSink + { + Id = endpoint.ToString(), + Url = endpoint + }; + return RegisterSink(sink); + } + + /// Registers the specified webhook with + public WebhooksFeature RegisterSink(WebhookSink sink) => RegisterSinks(sink); + + /// Registers the specified webhooks with + public WebhooksFeature RegisterSinks(params WebhookSink[] sinks) + { + Services.Configure(ConfigureSinks); + Services.Configure(options => options.Sinks.AddRange(sinks)); return this; } /// public override void Apply() { - Services - .AddHandlersFrom() - .AddScoped() - .AddScoped(WebhookDispatcher) - .AddScoped() - .AddSingleton(); - - Services.Configure(WebhookOptions); - - var httpClientBuilder = Services.AddHttpClient(HttpClient); - HttpClientBuilder(httpClientBuilder); + Services.AddWebhooksCore(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Handlers/InvokeWebhookHandler.cs b/src/modules/Elsa.Webhooks/Handlers/InvokeWebhookHandler.cs deleted file mode 100644 index a70103204..000000000 --- a/src/modules/Elsa.Webhooks/Handlers/InvokeWebhookHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Elsa.Mediator.Contracts; -using Elsa.Mediator.Models; -using Elsa.Webhooks.Commands; -using Elsa.Webhooks.Services; -using JetBrains.Annotations; - -namespace Elsa.Webhooks.Handlers; - -/// -/// Handles the command. -/// -[UsedImplicitly] -public class InvokeWebhookHandler : ICommandHandler -{ - private readonly IWebhookInvoker _webhookInvoker; - - /// - /// Constructor. - /// - public InvokeWebhookHandler(IWebhookInvoker webhookInvoker) - { - _webhookInvoker = webhookInvoker; - } - - /// - public async Task HandleAsync(InvokeWebhook command, CancellationToken cancellationToken) - { - await _webhookInvoker.InvokeWebhookAsync(command.WebhookRegistration, command.WebhookEvent, cancellationToken); - return Unit.Instance; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Handlers/RunTaskHandler.cs b/src/modules/Elsa.Webhooks/Handlers/RunTaskHandler.cs index e3c415350..6da1d4082 100644 --- a/src/modules/Elsa.Webhooks/Handlers/RunTaskHandler.cs +++ b/src/modules/Elsa.Webhooks/Handlers/RunTaskHandler.cs @@ -1,30 +1,15 @@ -using Elsa.Common.Contracts; using Elsa.Mediator.Contracts; using Elsa.Webhooks.Models; -using Elsa.Webhooks.Services; using Elsa.Workflows.Runtime.Notifications; using JetBrains.Annotations; +using WebhooksCore; namespace Elsa.Webhooks.Handlers; -/// /// Handles the notification and asynchronously invokes all registered webhook endpoints. -/// [UsedImplicitly] -public class RunTaskHandler : INotificationHandler +public class RunTaskHandler(IWebhookEventBroadcaster webhookDispatcher) : INotificationHandler { - private readonly IWebhookDispatcher _webhookDispatcher; - private readonly ISystemClock _systemClock; - - /// - /// Constructor. - /// - public RunTaskHandler(IWebhookDispatcher webhookDispatcher, ISystemClock systemClock) - { - _webhookDispatcher = webhookDispatcher; - _systemClock = systemClock; - } - /// public async Task HandleAsync(RunTaskRequest notification, CancellationToken cancellationToken) { @@ -46,8 +31,7 @@ public class RunTaskHandler : INotificationHandler notification.TaskPayload ); - var now = _systemClock.UtcNow; - var webhookEvent = new WebhookEvent("RunTask", payload, now); - await _webhookDispatcher.DispatchAsync(webhookEvent, cancellationToken); + var webhookEvent = new NewWebhookEvent("RunTask", payload); + await webhookDispatcher.BroadcastAsync(webhookEvent, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Implementations/BackgroundWebhookDispatcher.cs b/src/modules/Elsa.Webhooks/Implementations/BackgroundWebhookDispatcher.cs deleted file mode 100644 index c439b497f..000000000 --- a/src/modules/Elsa.Webhooks/Implementations/BackgroundWebhookDispatcher.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Elsa.Mediator; -using Elsa.Mediator.Contracts; -using Elsa.Webhooks.Commands; -using Elsa.Webhooks.Models; -using Elsa.Webhooks.Services; - -namespace Elsa.Webhooks.Implementations; - -/// -/// Uses a background channel to asynchronously invoke each webhook url. -/// -public class BackgroundWebhookDispatcher : IWebhookDispatcher -{ - private readonly ICommandSender _commandSender; - private readonly IWebhookRegistrationService _webhookRegistrationService; - - /// - /// Constructor. - /// - public BackgroundWebhookDispatcher(ICommandSender commandSender, IWebhookRegistrationService webhookRegistrationService) - { - _commandSender = commandSender; - _webhookRegistrationService = webhookRegistrationService; - } - - /// - public async Task DispatchAsync(WebhookEvent webhookEvent, CancellationToken cancellationToken = default) - { - var registrations = await _webhookRegistrationService.ListByEventTypeAsync(webhookEvent.EventType, cancellationToken); - - foreach (var registration in registrations) - { - var notification = new InvokeWebhook(registration, webhookEvent); - await _commandSender.SendAsync(notification, CommandStrategy.Background, cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Implementations/DefaultWebhookRegistrationService.cs b/src/modules/Elsa.Webhooks/Implementations/DefaultWebhookRegistrationService.cs deleted file mode 100644 index ce7013fd2..000000000 --- a/src/modules/Elsa.Webhooks/Implementations/DefaultWebhookRegistrationService.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Runtime.CompilerServices; -using Elsa.Webhooks.Models; -using Elsa.Webhooks.Services; - -namespace Elsa.Webhooks.Implementations; - -/// -public class DefaultWebhookRegistrationService : IWebhookRegistrationService -{ - private readonly IEnumerable _providers; - - /// - /// Constructor. - /// - /// - public DefaultWebhookRegistrationService(IEnumerable providers) - { - _providers = providers; - } - - /// - public async ValueTask> ListByEventTypeAsync(string eventType, CancellationToken cancellationToken) => - await EnumerateByEventTypeAsync(eventType, cancellationToken).ToListAsync(cancellationToken); - - private async IAsyncEnumerable EnumerateByEventTypeAsync(string eventType, [EnumeratorCancellation] CancellationToken cancellationToken) - { - foreach (var provider in _providers) - { - var registrations = await provider.ListAsync(eventType, cancellationToken); - - foreach (var registration in registrations) - yield return registration; - } - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Implementations/HttpWebhookInvoker.cs b/src/modules/Elsa.Webhooks/Implementations/HttpWebhookInvoker.cs deleted file mode 100644 index 23b8a7fa2..000000000 --- a/src/modules/Elsa.Webhooks/Implementations/HttpWebhookInvoker.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Net.Http.Json; -using Elsa.Webhooks.Models; -using Elsa.Webhooks.Services; -using Microsoft.Extensions.Logging; - -namespace Elsa.Webhooks.Implementations; - -/// -/// An implementation of that uses a named . -/// -public class HttpWebhookInvoker : IWebhookInvoker -{ - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - - /// - /// Constructor. - /// - public HttpWebhookInvoker(HttpClient httpClient, ILogger logger) - { - _httpClient = httpClient; - _logger = logger; - } - - /// - public async Task InvokeWebhookAsync(WebhookRegistration registration, WebhookEvent webhookEvent, CancellationToken cancellationToken = default) - { - var url = registration.Url; - var response = await _httpClient.PostAsJsonAsync(url, webhookEvent, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync(cancellationToken); - _logger.LogWarning("Invoking webhook {Webhook} failed with status code {StatusCode} and content {Content}", registration.Url, response.StatusCode, content); - } - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Implementations/OptionsWebhookRegistrationProvider.cs b/src/modules/Elsa.Webhooks/Implementations/OptionsWebhookRegistrationProvider.cs deleted file mode 100644 index 1f4638dc5..000000000 --- a/src/modules/Elsa.Webhooks/Implementations/OptionsWebhookRegistrationProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Elsa.Webhooks.Models; -using Elsa.Webhooks.Options; -using Elsa.Webhooks.Services; -using Microsoft.Extensions.Options; - -namespace Elsa.Webhooks.Implementations; - -/// -/// Provides webhook registrations from the options. -/// -public class OptionsWebhookRegistrationProvider : IWebhookRegistrationProvider -{ - private readonly WebhookOptions _options; - - /// - /// Constructor. - /// - /// - public OptionsWebhookRegistrationProvider(IOptions options) - { - _options = options.Value; - } - - /// - public ValueTask> ListAsync(string eventType, CancellationToken cancellationToken) => - new(_options.Endpoints.Where(x => !x.EventTypes.Any() || x.EventTypes.Contains(eventType))); -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Models/WebhookEvent.cs b/src/modules/Elsa.Webhooks/Models/WebhookEvent.cs deleted file mode 100644 index 8be604627..000000000 --- a/src/modules/Elsa.Webhooks/Models/WebhookEvent.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Elsa.Webhooks.Models; - -/// -/// A payload sent to a webhook url. -/// -public record WebhookEvent(string EventType, object? Payload, DateTimeOffset Timestamp); \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Models/WebhookRegistration.cs b/src/modules/Elsa.Webhooks/Models/WebhookRegistration.cs deleted file mode 100644 index 25706d8b7..000000000 --- a/src/modules/Elsa.Webhooks/Models/WebhookRegistration.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Elsa.Webhooks.Models; - -/// -/// Represents a webhook url registration -/// -public class WebhookRegistration -{ - /// - /// Constructor. - /// - [JsonConstructor] - public WebhookRegistration() - { - } - - /// - /// Constructor. - /// - public WebhookRegistration(Uri url) - { - Url = url; - } - - /// - /// The URL to deliver the webhook event to. - /// - public Uri Url { get; set; } = default!; - - /// - /// A whitelist of event types to deliver. If empty, all events will be delivered. - /// - public HashSet EventTypes { get; set; } = new(); -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Options/WebhookOptions.cs b/src/modules/Elsa.Webhooks/Options/WebhookOptions.cs deleted file mode 100644 index 836891d78..000000000 --- a/src/modules/Elsa.Webhooks/Options/WebhookOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Elsa.Webhooks.Models; - -namespace Elsa.Webhooks.Options; - -/// -/// Provides various options related to webhooks. -/// -public class WebhookOptions -{ - /// - /// Stores a list of webhook registrations. - /// - public ICollection Endpoints { get; set; } = new List(); -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Services/IWebhookDispatcher.cs b/src/modules/Elsa.Webhooks/Services/IWebhookDispatcher.cs deleted file mode 100644 index 9dbf3e478..000000000 --- a/src/modules/Elsa.Webhooks/Services/IWebhookDispatcher.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Elsa.Webhooks.Models; - -namespace Elsa.Webhooks.Services; - -/// -/// Asynchronously invokes all registered webhooks. -/// -public interface IWebhookDispatcher -{ - /// - /// Dispatches the specified webhook event. - /// - Task DispatchAsync(WebhookEvent webhookEvent, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Services/IWebhookInvoker.cs b/src/modules/Elsa.Webhooks/Services/IWebhookInvoker.cs deleted file mode 100644 index 7a9277cf9..000000000 --- a/src/modules/Elsa.Webhooks/Services/IWebhookInvoker.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Elsa.Webhooks.Models; - -namespace Elsa.Webhooks.Services; - -/// -/// Invokes a single registered webhook url. -/// -public interface IWebhookInvoker -{ - /// - /// Invokes the specified webhook registration with the specified webhook event.. - /// - Task InvokeWebhookAsync(WebhookRegistration registration, WebhookEvent webhookEvent, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Services/IWebhookRegistrationProvider.cs b/src/modules/Elsa.Webhooks/Services/IWebhookRegistrationProvider.cs deleted file mode 100644 index f996c7268..000000000 --- a/src/modules/Elsa.Webhooks/Services/IWebhookRegistrationProvider.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Elsa.Webhooks.Models; - -namespace Elsa.Webhooks.Services; - -/// -/// Provides a list of webhook registrations. -/// -public interface IWebhookRegistrationProvider -{ - /// - /// Returns a list of webhook registrations. - /// - ValueTask> ListAsync(string eventType, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/modules/Elsa.Webhooks/Services/IWebhookRegistrationService.cs b/src/modules/Elsa.Webhooks/Services/IWebhookRegistrationService.cs deleted file mode 100644 index a951ce0e4..000000000 --- a/src/modules/Elsa.Webhooks/Services/IWebhookRegistrationService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Elsa.Webhooks.Models; - -namespace Elsa.Webhooks.Services; - -/// -/// Provides a list of webhook registrations. -/// -public interface IWebhookRegistrationService -{ - /// - /// Returns a list of webhook registrations matching the specified event. - /// - ValueTask> ListByEventTypeAsync(string eventType, CancellationToken cancellationToken = default); -} \ No newline at end of file