Add project files.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace Elsa.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// An endpoint that maps a request to a response.
|
||||
/// </summary>
|
||||
public abstract class ElsaEndpointWithMapper<TRequest, TMapper> : EndpointWithMapper<TRequest, TMapper> where TMapper : class, IRequestMapper where TRequest : notnull
|
||||
{
|
||||
protected void ConfigurePermissions(params string[] permissions)
|
||||
{
|
||||
if (!EndpointSecurityOptions.SecurityIsEnabled)
|
||||
AllowAnonymous();
|
||||
else
|
||||
Permissions(new[] { PermissionNames.All }.Concat(permissions).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ElsaEndpointWithoutRequest : EndpointWithoutRequest
|
||||
{
|
||||
protected void ConfigurePermissions(params string[] permissions)
|
||||
{
|
||||
if (!EndpointSecurityOptions.SecurityIsEnabled)
|
||||
AllowAnonymous();
|
||||
else
|
||||
Permissions(new[] { PermissionNames.All }.Concat(permissions).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ElsaEndpointWithoutRequest<TResponse> : EndpointWithoutRequest<TResponse> where TResponse : notnull
|
||||
{
|
||||
protected void ConfigurePermissions(params string[] permissions)
|
||||
{
|
||||
if (!EndpointSecurityOptions.SecurityIsEnabled)
|
||||
AllowAnonymous();
|
||||
else
|
||||
Permissions(new[] { PermissionNames.All }.Concat(permissions).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class ElsaEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse> where TRequest : notnull, new() where TResponse : notnull
|
||||
{
|
||||
protected void ConfigurePermissions(params string[] permissions)
|
||||
{
|
||||
if (!EndpointSecurityOptions.SecurityIsEnabled)
|
||||
AllowAnonymous();
|
||||
else
|
||||
Permissions(new[] { PermissionNames.All }.Concat(permissions).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class ElsaEndpoint<TRequest, TResponse, TMapper> : Endpoint<TRequest, TResponse, TMapper> where TRequest : notnull, new() where TResponse : notnull where TMapper : class, IMapper, new()
|
||||
{
|
||||
protected void ConfigurePermissions(params string[] permissions)
|
||||
{
|
||||
if (!EndpointSecurityOptions.SecurityIsEnabled)
|
||||
AllowAnonymous();
|
||||
else
|
||||
Permissions(new[] { PermissionNames.All }.Concat(permissions).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public class ElsaEndpoint<TRequest> : Endpoint<TRequest> where TRequest : notnull, new()
|
||||
{
|
||||
protected void ConfigurePermissions(params string[] permissions)
|
||||
{
|
||||
if (!EndpointSecurityOptions.SecurityIsEnabled)
|
||||
AllowAnonymous();
|
||||
else
|
||||
Permissions(new[] { PermissionNames.All }.Concat(permissions).ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides common features to modules that expose API endpoints.
|
||||
</Description>
|
||||
<RootNamespace>Elsa</RootNamespace>
|
||||
<PackageTags>elsa api common</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastEndpoints" />
|
||||
<PackageReference Include="FastEndpoints.Security" />
|
||||
<PackageReference Include="FastEndpoints.Swagger" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Features\Elsa.Features.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Elsa;
|
||||
|
||||
public static class EndpointSecurityOptions
|
||||
{
|
||||
public static string AdminRoleName = "Admin";
|
||||
public static string ReaderRoleName = "Reader";
|
||||
public static string WriteRoleName = "Writer";
|
||||
public static bool SecurityIsEnabled = true;
|
||||
|
||||
public static void DisableSecurity() => SecurityIsEnabled = false;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="HttpRequest"/>.
|
||||
/// </summary>
|
||||
public static class HttpRequestExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the specified request is a local request.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns>True if the request is local, otherwise false.</returns>
|
||||
public static bool IsLocal(this HttpRequest request)
|
||||
{
|
||||
var connection = request.HttpContext.Connection;
|
||||
return connection.RemoteIpAddress != null
|
||||
? connection.LocalIpAddress != null
|
||||
? connection.RemoteIpAddress.Equals(connection.LocalIpAddress)
|
||||
: IPAddress.IsLoopback(connection.RemoteIpAddress)
|
||||
: connection.RemoteIpAddress == null && connection.LocalIpAddress == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Reflection;
|
||||
using Elsa.Features.Services;
|
||||
using FastEndpoints;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extensions to <see cref="IModule"/>.
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
private static readonly object FastEndpointsAssembliesKey = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified assembly for FastEndpoint assembly discovery.
|
||||
/// </summary>
|
||||
public static IModule AddFastEndpointsAssembly(this IModule module, Assembly assembly)
|
||||
{
|
||||
var assemblies = module.Properties.GetOrAdd(FastEndpointsAssembliesKey, () => new HashSet<Assembly>());
|
||||
assemblies.Add(assembly);
|
||||
return module;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the assembly for FastEndpoint assembly discovery using the specified marker type.
|
||||
/// </summary>
|
||||
public static IModule AddFastEndpointsAssembly<T>(this IModule module) => module.AddFastEndpointsAssembly(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Registers the assembly for FastEndpoint assembly discovery using the specified marker type.
|
||||
/// </summary>
|
||||
public static IModule AddFastEndpointsAssembly(this IModule module, Type markerType) => module.AddFastEndpointsAssembly(markerType.Assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all collected assemblies for discovery of endpoints.
|
||||
/// </summary>
|
||||
public static IEnumerable<Assembly> GetFastEndpointsAssembliesFromModule(this IModule module) => module.Properties.GetOrAdd(FastEndpointsAssembliesKey, () => new HashSet<Assembly>());
|
||||
|
||||
/// <summary>
|
||||
/// Adds FastEndpoints to the service container and registers all collected assemblies for discovery of endpoints.
|
||||
/// </summary>
|
||||
public static IModule AddFastEndpointsFromModule(this IModule module)
|
||||
{
|
||||
var assemblies = module.GetFastEndpointsAssembliesFromModule().ToList();
|
||||
|
||||
module.Services.AddFastEndpoints(options =>
|
||||
{
|
||||
options.DisableAutoDiscovery = true;
|
||||
options.Assemblies = assemblies;
|
||||
});
|
||||
|
||||
return module;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
static class ObjectDictionaryExtensions
|
||||
{
|
||||
public static T GetOrAdd<T>(this IDictionary<object, object> dictionary, object key, Func<T> valueFactory)
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out var value)) return (T)value;
|
||||
value = valueFactory();
|
||||
dictionary.Add(key, value!);
|
||||
return (T)value!;
|
||||
}
|
||||
|
||||
public static T Get<T>(this IDictionary<object, object> dictionary, object key) => (T)dictionary[key];
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Elsa.Features.Services;
|
||||
using FastEndpoints.Swagger;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Elsa.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to enable the generation of Swagger API Documentation and associated Swagger UI.
|
||||
/// </summary>
|
||||
public static class SwaggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers Swagger document generator.
|
||||
/// </summary>
|
||||
public static IModule AddSwagger(this IModule module)
|
||||
{
|
||||
Version ver = new(3, 0);
|
||||
|
||||
// Swagger API documentation
|
||||
module.Services.SwaggerDocument(o =>
|
||||
{
|
||||
o.EnableJWTBearerAuth = true;
|
||||
o.DocumentSettings = s =>
|
||||
{
|
||||
s.DocumentName = $"v{ver.Major}";
|
||||
s.Title = "Elsa API";
|
||||
s.Version = $"v{ver.Major}.{ver.Minor}";
|
||||
s.AddAuth("ApiKey", new()
|
||||
{
|
||||
Name = "Authorization",
|
||||
In = NSwag.OpenApiSecurityApiKeyLocation.Header,
|
||||
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
|
||||
Description = "Enter: ApiKey [your API key]"
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds middleware to enable the Swagger UI at '/swagger'
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseSwaggerUI(this IApplicationBuilder app)
|
||||
{
|
||||
return app.UseSwaggerGen();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows;
|
||||
using FastEndpoints;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods to add the FastEndpoints middleware configured for use with Elsa API endpoints.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Register the FastEndpoints middleware configured for use with with Elsa API endpoints.
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="routePrefix">The route prefix to apply to Elsa API endpoints.</param>
|
||||
/// <example>E.g. "elsa/api" will expose endpoints like this: "/elsa/api/workflow-definitions"</example>
|
||||
public static IApplicationBuilder UseWorkflowsApi(this IApplicationBuilder app, string routePrefix = "elsa/api")
|
||||
{
|
||||
return app.UseFastEndpoints(config =>
|
||||
{
|
||||
config.Endpoints.RoutePrefix = routePrefix;
|
||||
config.Serializer.RequestDeserializer = DeserializeRequestAsync;
|
||||
config.Serializer.ResponseSerializer = SerializeRequestAsync;
|
||||
|
||||
config.Binding.ValueParserFor<DateTimeOffset>(s =>
|
||||
new(DateTimeOffset.TryParse(s.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result), result));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the FastEndpoints middleware configured for use with with Elsa API endpoints.
|
||||
/// </summary>
|
||||
/// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to register the endpoints with.</param>
|
||||
/// <param name="routePrefix">The route prefix to apply to Elsa API endpoints.</param>
|
||||
/// <example>E.g. "elsa/api" will expose endpoints like this: "/elsa/api/workflow-definitions"</example>
|
||||
public static IEndpointRouteBuilder MapWorkflowsApi(this IEndpointRouteBuilder routes, string routePrefix = "elsa/api") =>
|
||||
routes.MapFastEndpoints(config =>
|
||||
{
|
||||
config.Endpoints.RoutePrefix = routePrefix;
|
||||
config.Serializer.RequestDeserializer = DeserializeRequestAsync;
|
||||
config.Serializer.ResponseSerializer = SerializeRequestAsync;
|
||||
});
|
||||
|
||||
private static ValueTask<object?> DeserializeRequestAsync(HttpRequest httpRequest, Type modelType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var serializer = httpRequest.HttpContext.RequestServices.GetRequiredService<IApiSerializer>();
|
||||
var options = serializer.GetOptions();
|
||||
|
||||
return serializerContext == null
|
||||
? JsonSerializer.DeserializeAsync(httpRequest.Body, modelType, options, cancellationToken)
|
||||
: JsonSerializer.DeserializeAsync(httpRequest.Body, modelType, serializerContext, cancellationToken);
|
||||
}
|
||||
|
||||
private static Task SerializeRequestAsync(HttpResponse httpResponse, object? dto, string contentType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var serializer = httpResponse.HttpContext.RequestServices.GetRequiredService<IApiSerializer>();
|
||||
var options = serializer.GetOptions();
|
||||
|
||||
httpResponse.ContentType = contentType;
|
||||
return serializerContext == null
|
||||
? JsonSerializer.SerializeAsync(httpResponse.Body, dto, dto?.GetType() ?? typeof(object), options, cancellationToken)
|
||||
: JsonSerializer.SerializeAsync(httpResponse.Body, dto, dto?.GetType() ?? typeof(object), serializerContext, cancellationToken);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Elsa.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a count response that offers a unified format for returning count of things from API endpoints.
|
||||
/// </summary>
|
||||
/// <param name="Count">The number of items found for a given query.</param>
|
||||
public record CountResponse(long Count);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Elsa.Models;
|
||||
|
||||
public record LinkedResource(Link[] Links)
|
||||
{
|
||||
public LinkedResource() : this([]) { }
|
||||
}
|
||||
|
||||
public record Link(string Href, string Rel, string Method);
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Elsa.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a generic list response that offers a unified format for returning list of things from API endpoints.
|
||||
/// </summary>
|
||||
public class ListResponse<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default constructor.
|
||||
/// </summary>
|
||||
public ListResponse()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor accepting a list of items.
|
||||
/// </summary>
|
||||
public ListResponse(ICollection<T> items)
|
||||
{
|
||||
Items = items;
|
||||
Count = items.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of items.
|
||||
/// </summary>
|
||||
public ICollection<T> Items { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The number of items in the list.
|
||||
/// </summary>
|
||||
public long Count { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Elsa.Common.Models;
|
||||
|
||||
namespace Elsa.Models;
|
||||
|
||||
public record PagedListResponse<T>: LinkedResource
|
||||
{
|
||||
public PagedListResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public PagedListResponse(Page<T> page)
|
||||
{
|
||||
Items = page.Items;
|
||||
TotalCount = page.TotalCount;
|
||||
}
|
||||
|
||||
public ICollection<T> Items { get; set; } = default!;
|
||||
public long TotalCount { get; set; }
|
||||
|
||||
public static PagedListResponse<T> From(Page<T> page) => new(page);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Elsa.Models;
|
||||
|
||||
public record ValidateCredentialsResult(bool IsValid, object? Context = null)
|
||||
{
|
||||
public static ValidateCredentialsResult Invalid() => new(false);
|
||||
public static ValidateCredentialsResult Valid(object? context = null) => new(true, context);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Elsa;
|
||||
|
||||
public static class PermissionNames
|
||||
{
|
||||
public const string All = "*";
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Security.Claims;
|
||||
using Elsa.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Adda the "create:application" permission to the current user if the request is local.
|
||||
/// </summary>
|
||||
public class LocalHostPermissionRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[PublicAPI]
|
||||
public class LocalHostPermissionRequirementHandler : AuthorizationHandler<LocalHostPermissionRequirement>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LocalHostPermissionRequirementHandler(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalHostPermissionRequirement requirement)
|
||||
{
|
||||
if (_httpContextAccessor.HttpContext?.Request.IsLocal() == false)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var currentIdentity = context.User.Identity;
|
||||
|
||||
if (currentIdentity?.IsAuthenticated == false)
|
||||
{
|
||||
var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
|
||||
identity.AddClaim(new Claim("permissions", "create:application"));
|
||||
identity.AddClaim(new Claim("permissions", "create:user"));
|
||||
identity.AddClaim(new Claim("permissions", "create:role"));
|
||||
context.User.AddIdentity(identity);
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Elsa.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Requirements;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an authorization requirement for localhost requests, meaning that if a request comes from the localhost, the requirement is met.
|
||||
/// </summary>
|
||||
public class LocalHostRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[PublicAPI]
|
||||
public class LocalHostRequirementHandler : AuthorizationHandler<LocalHostRequirement>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LocalHostRequirementHandler(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalHostRequirement requirement)
|
||||
{
|
||||
if (_httpContextAccessor.HttpContext?.Request.IsLocal() == false)
|
||||
context.Fail(new AuthorizationFailureReason(this, "Only requests from localhost are allowed"));
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Reference this package when developing drop-ins for Elsa Workflows.
|
||||
</Description>
|
||||
<PackageTags>elsa drop-ins common</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Features\Elsa.Features.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Elsa.Features.Services;
|
||||
|
||||
namespace Elsa.DropIns.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Implement this in your drop-in module to configure elsa and register services.
|
||||
/// </summary>
|
||||
public interface IDropIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the drop-in is installed.
|
||||
/// </summary>
|
||||
/// <param name="module">The Elsa module.</param>
|
||||
void Install(IModule module);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the drop-in is being configured.
|
||||
/// </summary>
|
||||
ValueTask ConfigureAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Unconfigure the drop-in when it's deleted.
|
||||
/// </summary>
|
||||
void Unconfigure(IServiceProvider serviceProvider);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Reflection;
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.Models;
|
||||
|
||||
namespace Elsa.DropIns.Catalogs;
|
||||
|
||||
public class AssembliesDropInCatalog : IDropInCatalog
|
||||
{
|
||||
private readonly IEnumerable<Assembly> _assemblies;
|
||||
|
||||
public AssembliesDropInCatalog(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
_assemblies = assemblies;
|
||||
}
|
||||
|
||||
public IEnumerable<DropInDescriptor> List()
|
||||
{
|
||||
var allDropIns = new List<DropInDescriptor>();
|
||||
|
||||
foreach (var assembly in _assemblies)
|
||||
{
|
||||
var assemblyCatalog = new AssemblyDropInCatalog(assembly);
|
||||
var dropIns = assemblyCatalog.List();
|
||||
allDropIns.AddRange(dropIns);
|
||||
}
|
||||
|
||||
return allDropIns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Reflection;
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.Core;
|
||||
using Elsa.DropIns.Helpers;
|
||||
using Elsa.DropIns.Models;
|
||||
|
||||
namespace Elsa.DropIns.Catalogs;
|
||||
|
||||
public class AssemblyDropInCatalog : IDropInCatalog
|
||||
{
|
||||
private readonly Assembly _assembly;
|
||||
|
||||
public AssemblyDropInCatalog(Assembly assembly)
|
||||
{
|
||||
_assembly = assembly;
|
||||
}
|
||||
|
||||
public IEnumerable<DropInDescriptor> List()
|
||||
{
|
||||
var dropIns = TypeFinder.FindImplementationsOf<IDropIn>(_assembly)
|
||||
.Select(type => new DropInDescriptor(type))
|
||||
.ToList();
|
||||
|
||||
return dropIns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Reflection;
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.Helpers;
|
||||
using Elsa.DropIns.Models;
|
||||
|
||||
namespace Elsa.DropIns.Catalogs;
|
||||
|
||||
/// <summary>
|
||||
/// A catalog that lists drop-ins from a directory.
|
||||
/// </summary>
|
||||
public class DirectoryDropInCatalog : IDropInCatalog
|
||||
{
|
||||
private readonly string _directoryPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DirectoryDropInCatalog"/> class.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">The directory path to list drop-ins from.</param>
|
||||
public DirectoryDropInCatalog(string directoryPath)
|
||||
{
|
||||
_directoryPath = directoryPath;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<DropInDescriptor> List()
|
||||
{
|
||||
if (!Directory.Exists(_directoryPath))
|
||||
return Enumerable.Empty<DropInDescriptor>();
|
||||
|
||||
var assemblies = ListAssemblies();
|
||||
var packages = ListPackages();
|
||||
var assembliesCatalog = new AssembliesDropInCatalog(assemblies);
|
||||
var packagesCatalog = new NuGetPackagesCatalog(packages);
|
||||
|
||||
return assembliesCatalog.List().Concat(packagesCatalog.List());
|
||||
}
|
||||
|
||||
private IEnumerable<Assembly> ListAssemblies()
|
||||
{
|
||||
var assemblyPaths = Directory.GetFiles(_directoryPath, "*.dll", SearchOption.AllDirectories);
|
||||
|
||||
foreach (var assemblyPath in assemblyPaths)
|
||||
{
|
||||
var assembly = LoadDropInAssembly(assemblyPath);
|
||||
|
||||
if (assembly == null)
|
||||
continue;
|
||||
|
||||
yield return assembly;
|
||||
}
|
||||
}
|
||||
|
||||
private string[] ListPackages()
|
||||
{
|
||||
return Directory.GetFiles(_directoryPath, "*.nupkg", SearchOption.AllDirectories);
|
||||
}
|
||||
|
||||
private static Assembly? LoadDropInAssembly(string path)
|
||||
{
|
||||
return !File.Exists(path) ? null : AssemblyLoader.LoadPath(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Elsa.DropIns.Contexts;
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.Models;
|
||||
|
||||
namespace Elsa.DropIns.Catalogs;
|
||||
|
||||
public class NuGetPackageCatalog : IDropInCatalog
|
||||
{
|
||||
private readonly string _packagePath;
|
||||
|
||||
public NuGetPackageCatalog(string packagePath)
|
||||
{
|
||||
_packagePath = packagePath;
|
||||
}
|
||||
|
||||
public IEnumerable<DropInDescriptor> List()
|
||||
{
|
||||
var assemblyLoadContext = new NuGetPackageAssemblyLoadContext(_packagePath);
|
||||
var assemblies = assemblyLoadContext.Assemblies;
|
||||
var assembliesCatalog = new AssembliesDropInCatalog(assemblies);
|
||||
|
||||
return assembliesCatalog.List();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.Models;
|
||||
|
||||
namespace Elsa.DropIns.Catalogs;
|
||||
|
||||
public class NuGetPackagesCatalog : IDropInCatalog
|
||||
{
|
||||
private readonly IEnumerable<string> _packagePaths;
|
||||
|
||||
public NuGetPackagesCatalog(IEnumerable<string> packagePaths)
|
||||
{
|
||||
_packagePaths = packagePaths;
|
||||
}
|
||||
|
||||
public IEnumerable<DropInDescriptor> List()
|
||||
{
|
||||
return _packagePaths.Select(packagePath => new NuGetPackageCatalog(packagePath)).SelectMany(catalog => catalog.List());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Elsa.DropIns.Contexts;
|
||||
|
||||
internal sealed class DirectoryAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
|
||||
public DirectoryAssemblyLoadContext(string dropInPath)
|
||||
{
|
||||
_resolver = new AssemblyDependencyResolver(dropInPath);
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null;
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using NuGet.Packaging;
|
||||
|
||||
namespace Elsa.DropIns.Contexts;
|
||||
|
||||
internal sealed class NuGetPackageAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly Dictionary<string, Assembly> _loadedAssemblies = new Dictionary<string, Assembly>();
|
||||
|
||||
public NuGetPackageAssemblyLoadContext(string nugetPackagePath)
|
||||
{
|
||||
var packageReader = new PackageArchiveReader(nugetPackagePath);
|
||||
|
||||
foreach (var dllFile in packageReader.GetFiles().Where(fileName => fileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
using var dllStream = packageReader.GetStream(dllFile);
|
||||
using var memoryStream = new MemoryStream();
|
||||
dllStream.CopyTo(memoryStream);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
var assembly = LoadFromStream(memoryStream);
|
||||
|
||||
_loadedAssemblies[assembly.FullName!] = assembly;
|
||||
}
|
||||
//Closes the package reader, closing the .nupkg file
|
||||
packageReader.Dispose();
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
return _loadedAssemblies.GetValueOrDefault(assemblyName.FullName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Elsa.DropIns.Models;
|
||||
|
||||
namespace Elsa.DropIns.Contracts;
|
||||
|
||||
public interface IDropInCatalog
|
||||
{
|
||||
IEnumerable<DropInDescriptor> List();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Elsa.Features.Services;
|
||||
|
||||
namespace Elsa.DropIns.Contracts;
|
||||
|
||||
public interface IDropInInstaller
|
||||
{
|
||||
void Install(IModule module);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides drop-in features for Elsa workflows.
|
||||
</Description>
|
||||
<PackageTags>elsa drop-ins common</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="NuGet.Packaging" />
|
||||
<PackageReference Include="NuGet.Protocol" />
|
||||
<PackageReference Include="System.Formats.Asn1" />
|
||||
<PackageReference Include="ThrottleDebounce" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--Overridden
|
||||
for vulnerability reasons with dependencies referencing older versions.-->
|
||||
<ItemGroup>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.DropIns.Core\Elsa.DropIns.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.Options;
|
||||
using Elsa.Features.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.DropIns.Extensions;
|
||||
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
public static IModule InstallDropIns(this IModule elsa, Action<DropInOptions>? configureOptions = default)
|
||||
{
|
||||
// Install drop-ins.
|
||||
var serviceProvider = new ServiceCollection().AddDropInInstaller(configureOptions).BuildServiceProvider();
|
||||
serviceProvider.GetRequiredService<IDropInInstaller>().Install(elsa);
|
||||
|
||||
// Register drop-in monitor.
|
||||
elsa.Services.AddDropInMonitor(configureOptions);
|
||||
|
||||
return elsa;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.HostedServices;
|
||||
using Elsa.DropIns.Options;
|
||||
using Elsa.DropIns.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.DropIns.Extensions;
|
||||
|
||||
internal static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddDropInMonitor(this IServiceCollection services, Action<DropInOptions>? configureOptions = default)
|
||||
{
|
||||
services.AddDropInCore(configureOptions);
|
||||
services.AddHostedService<DropInDirectoryMonitorHostedService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddDropInInstaller(this IServiceCollection services, Action<DropInOptions>? configureOptions = default)
|
||||
{
|
||||
services.AddDropInCore(configureOptions);
|
||||
services.AddSingleton<IDropInInstaller, DropInInstaller>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddDropInCore(this IServiceCollection services, Action<DropInOptions>? configureOptions = default)
|
||||
{
|
||||
services.Configure(configureOptions ?? (_ => { }));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Elsa.DropIns.Contexts;
|
||||
|
||||
namespace Elsa.DropIns.Helpers;
|
||||
|
||||
public static class AssemblyLoader
|
||||
{
|
||||
public static Assembly LoadPath(string path)
|
||||
{
|
||||
var loadContext = new DirectoryAssemblyLoadContext(path);
|
||||
var assemblyName = AssemblyLoadContext.GetAssemblyName(path);
|
||||
|
||||
// Copy drop in to memory stream to avoid file locking
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using var memoryStream = new MemoryStream();
|
||||
fileStream.CopyTo(memoryStream);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
fileStream.Close();
|
||||
var assembly = loadContext.LoadFromStream(memoryStream);
|
||||
|
||||
return assembly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Elsa.DropIns.Helpers;
|
||||
|
||||
public static class TypeFinder
|
||||
{
|
||||
public static IEnumerable<Type> FindImplementationsOf<T>(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
// Find all types that implement IDropInStartup:
|
||||
return assemblies.SelectMany(FindImplementationsOf<T>);
|
||||
}
|
||||
|
||||
public static IEnumerable<Type> FindImplementationsOf<T>(Assembly assembly)
|
||||
{
|
||||
// Find all types that implement T:
|
||||
return assembly.GetExportedTypes().Where(t => typeof(T).IsAssignableFrom(t) && t is { IsAbstract: false, IsInterface: false });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using Elsa.DropIns.Catalogs;
|
||||
using Elsa.DropIns.Core;
|
||||
using Elsa.DropIns.Options;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ThrottleDebounce;
|
||||
|
||||
namespace Elsa.DropIns.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors the drop-in directory for changes and loads any new drop-ins.
|
||||
/// </summary>
|
||||
public class DropInDirectoryMonitorHostedService : BackgroundService
|
||||
{
|
||||
private readonly IOptions<DropInOptions> _options;
|
||||
private readonly ILogger<DropInDirectoryMonitorHostedService> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly RateLimitedFunc<string, Task> _debouncedLoader;
|
||||
private readonly RateLimitedFunc<string, Task> _debouncedUnloader;
|
||||
private readonly FileSystemWatcher _watcher;
|
||||
|
||||
private readonly Dictionary<string, List<IDropIn>> _installedDropIns;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public DropInDirectoryMonitorHostedService(
|
||||
IOptions<DropInOptions> options,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<DropInDirectoryMonitorHostedService> logger
|
||||
)
|
||||
{
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_debouncedLoader = Debouncer.Debounce<string, Task>(LoadDropInAssemblyAsync, TimeSpan.FromSeconds(2));
|
||||
_debouncedUnloader = Debouncer.Debounce<string, Task>(UnloadDropInAssemblyAsync, TimeSpan.FromSeconds(2));
|
||||
|
||||
_installedDropIns = [];
|
||||
|
||||
var rootDirectoryPath = _options.Value.DropInRootDirectory;
|
||||
|
||||
if (!Directory.Exists(rootDirectoryPath))
|
||||
Directory.CreateDirectory(rootDirectoryPath);
|
||||
|
||||
_watcher = new FileSystemWatcher(rootDirectoryPath)
|
||||
{
|
||||
EnableRaisingEvents = true,
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await LoadDropInAssemblyAsync(_options.Value.DropInRootDirectory);
|
||||
|
||||
_watcher.Changed += OnChanged;
|
||||
_watcher.Deleted += OnDeleted;
|
||||
}
|
||||
|
||||
private async void OnChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
var task = _debouncedLoader.Invoke(e.FullPath);
|
||||
|
||||
if (task == null)
|
||||
return;
|
||||
|
||||
await task;
|
||||
}
|
||||
|
||||
private async void OnDeleted(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
var task = _debouncedUnloader.Invoke(e.FullPath);
|
||||
|
||||
if (task == null)
|
||||
return;
|
||||
|
||||
await task;
|
||||
}
|
||||
|
||||
private async Task LoadDropInAssemblyAsync(string fullPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(fullPath)!;
|
||||
var directoryCatalog = new DirectoryDropInCatalog(directory);
|
||||
var dropInDescriptors = directoryCatalog.List();
|
||||
|
||||
foreach (var dropInDescriptor in dropInDescriptors)
|
||||
{
|
||||
var dropIn = (IDropIn)Activator.CreateInstance(dropInDescriptor.Type)!;
|
||||
if (_installedDropIns.TryGetValue(fullPath, out var installedDropIns))
|
||||
{
|
||||
installedDropIns.Add(dropIn);
|
||||
}
|
||||
else
|
||||
{
|
||||
_installedDropIns[fullPath] = [dropIn];
|
||||
}
|
||||
await dropIn.ConfigureAsync(_serviceProvider, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private Task UnloadDropInAssemblyAsync(string fullPath)
|
||||
{
|
||||
if (_installedDropIns.TryGetValue(fullPath, out var installedDropIn))
|
||||
{
|
||||
installedDropIn.ForEach(dropIn =>
|
||||
{
|
||||
try
|
||||
{
|
||||
dropIn.Unconfigure(_serviceProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error unconfiguring drop-in {DropIn}", dropIn.GetType().Name);
|
||||
}
|
||||
});
|
||||
_installedDropIns.Remove(fullPath);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Elsa.DropIns.Models;
|
||||
|
||||
public class DropInDescriptor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the drop-in.
|
||||
/// The DynamicallyAccessedMembers attribute ensures that the IL2072 warning is suppressed.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
public Type Type { get; set; } = type;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Elsa.DropIns.Options;
|
||||
|
||||
public class DropInOptions
|
||||
{
|
||||
public string DropInRootDirectory { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Elsa.DropIns.Catalogs;
|
||||
using Elsa.DropIns.Contracts;
|
||||
using Elsa.DropIns.Core;
|
||||
using Elsa.DropIns.Options;
|
||||
using Elsa.Features.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.DropIns.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class DropInInstaller : IDropInInstaller
|
||||
{
|
||||
private readonly IOptions<DropInOptions> _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DropInInstaller"/> class.
|
||||
/// </summary>
|
||||
public DropInInstaller(IOptions<DropInOptions> options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Install(IModule module)
|
||||
{
|
||||
var dropInRootDirectory = _options.Value.DropInRootDirectory;
|
||||
var directoryCatalog = new DirectoryDropInCatalog(dropInRootDirectory);
|
||||
var dropInDescriptors = directoryCatalog.List();
|
||||
|
||||
foreach (var dropInDescriptor in dropInDescriptors)
|
||||
{
|
||||
var dropIn = (IDropIn)Activator.CreateInstance(dropInDescriptor.Type)!;
|
||||
dropIn.Install(module);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Elsa.Features.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Elsa.Features.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Base type for classes that represent a feature.
|
||||
/// </summary>
|
||||
public abstract class FeatureBase : IFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
protected FeatureBase(IModule module)
|
||||
{
|
||||
Module = module;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The module this feature is a part of.
|
||||
/// </summary>
|
||||
public IModule Module { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the <see cref="IServiceCollection"/> to which services can be added.
|
||||
/// </summary>
|
||||
public IServiceCollection Services => Module.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to configure your feature.
|
||||
/// </summary>
|
||||
public virtual void Configure()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to register any hosted services provided by your feature.
|
||||
/// </summary>
|
||||
public virtual void ConfigureHostedServices()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to register services with <see cref="Services"/>.
|
||||
/// </summary>
|
||||
public virtual void Apply()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the specified hosted service using an optional priority to control in which order it will be registered with the service container.
|
||||
/// </summary>
|
||||
/// <param name="priority">The priority.</param>
|
||||
/// <typeparam name="T">The type of hosted service to configure.</typeparam>
|
||||
protected void ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService
|
||||
{
|
||||
Module.ConfigureHostedService<T>(priority);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Elsa.Features.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the feature is enabled automatically when the specified feature is enabled.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
public class DependencyOfAttribute : Attribute
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public DependencyOfAttribute(Type type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of the feature this feature is a dependency of.
|
||||
/// </summary>
|
||||
public Type Type { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Elsa.Features.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the feature depends on another feature.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
public class DependsOnAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the feature this feature depends on.</param>
|
||||
public DependsOnAttribute(Type type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of the feature this feature depends on.
|
||||
/// </summary>
|
||||
public Type Type { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Elsa.Features.Models;
|
||||
|
||||
namespace Elsa.Features.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a registry of installed features.
|
||||
/// </summary>
|
||||
public interface IInstalledFeatureRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a feature descriptor to the registry.
|
||||
/// </summary>
|
||||
/// <param name="descriptor">The feature descriptor.</param>
|
||||
void Add(FeatureDescriptor descriptor);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all installed features.
|
||||
/// </summary>
|
||||
/// <returns>All installed features.</returns>
|
||||
IEnumerable<FeatureDescriptor> List();
|
||||
|
||||
/// <summary>
|
||||
/// Finds a feature descriptor by its full name.
|
||||
/// </summary>
|
||||
/// <param name="fullName">The full name of the feature.</param>
|
||||
/// <returns>The feature descriptor or null if not found.</returns>
|
||||
FeatureDescriptor? Find(string fullName);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides utilities to turn your class library into a module that provides features.
|
||||
</Description>
|
||||
<PackageTags>elsa common</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
using Elsa.Features.Implementations;
|
||||
using Elsa.Features.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Adds extension methods to <see cref="IServiceCollection"/> that creates and configures modules.
|
||||
/// </summary>
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a new instance of an <see cref="IModule"/> implementation.
|
||||
/// </summary>
|
||||
public static IModule CreateModule(this IServiceCollection services) => new Module(services);
|
||||
|
||||
/// <summary>
|
||||
/// Installs and configures the specified feature. If the feature was already installed, it is not added twice, which means it is safe to call this method multiple times.
|
||||
/// </summary>
|
||||
public static IModule Use<T>(this IModule module, Action<T>? configure = null) where T: class, IFeature
|
||||
{
|
||||
module.Configure(configure);
|
||||
return module;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Adds extension methods to sort collections by their dependencies, also known as a topological sort.
|
||||
/// </summary>
|
||||
public static class EnumerableTopologicalSortExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a topologically sorted copy of the specified list.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static IEnumerable<T> TSort<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle = false)
|
||||
{
|
||||
var sorted = new List<T>();
|
||||
var visited = new HashSet<T>();
|
||||
|
||||
foreach (var item in source)
|
||||
Visit(item, visited, sorted, dependencies, throwOnCycle);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static void Visit<T>(T item, ISet<T> visited, ICollection<T> sorted, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle)
|
||||
{
|
||||
if (visited.Add(item))
|
||||
{
|
||||
foreach (var dep in dependencies(item))
|
||||
Visit(dep, visited, sorted, dependencies, throwOnCycle);
|
||||
|
||||
sorted.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (throwOnCycle && !sorted.Contains(item))
|
||||
throw new Exception("Cyclic dependency found");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Contracts;
|
||||
using Elsa.Features.Models;
|
||||
using Elsa.Features.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Elsa.Features.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class Module : IModule
|
||||
{
|
||||
private sealed record HostedServiceDescriptor(int Order, Type Type);
|
||||
|
||||
private Dictionary<Type, IFeature> _features = new();
|
||||
private readonly HashSet<IFeature> _configuredFeatures = new();
|
||||
private readonly List<HostedServiceDescriptor> _hostedServiceDescriptors = new();
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public Module(IServiceCollection services)
|
||||
{
|
||||
Services = services;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFeature<T>() where T : class, IFeature
|
||||
{
|
||||
return HasFeature(typeof(T));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasFeature(Type featureType)
|
||||
{
|
||||
return _features.ContainsKey(featureType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Configure<T>(Action<T>? configure = null) where T : class, IFeature
|
||||
{
|
||||
return Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Configure<T>(Func<IModule, T> factory, Action<T>? configure = null) where T : class, IFeature
|
||||
{
|
||||
if (!_features.TryGetValue(typeof(T), out var feature))
|
||||
{
|
||||
feature = factory(this);
|
||||
_features[typeof(T)] = feature;
|
||||
}
|
||||
|
||||
configure?.Invoke((T)feature);
|
||||
|
||||
if (!_isApplying)
|
||||
return (T)feature;
|
||||
|
||||
var dependencies = GetDependencyTypes(feature.GetType()).ToHashSet();
|
||||
foreach (var dependency in dependencies.Select(GetOrCreateFeature))
|
||||
ConfigureFeature(dependency);
|
||||
|
||||
ConfigureFeature(feature);
|
||||
return (T)feature;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService
|
||||
{
|
||||
return ConfigureHostedService(typeof(T), priority);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IModule ConfigureHostedService(Type hostedServiceType, int priority = 0)
|
||||
{
|
||||
_hostedServiceDescriptors.Add(new(priority, hostedServiceType));
|
||||
return this;
|
||||
}
|
||||
|
||||
private bool _isApplying;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply()
|
||||
{
|
||||
_isApplying = true;
|
||||
var featureTypes = GetFeatureTypes();
|
||||
_features = featureTypes.ToDictionary(featureType => featureType, featureType => _features.TryGetValue(featureType, out var existingFeature) ? existingFeature : (IFeature)Activator.CreateInstance(featureType, this)!);
|
||||
|
||||
// Iterate over a copy of the features to avoid concurrent modification exceptions.
|
||||
foreach (var feature in _features.Values.ToList())
|
||||
{
|
||||
// This will cause additional features to be added to _features.
|
||||
ConfigureFeature(feature);
|
||||
}
|
||||
|
||||
// Filter out features that depend on other features that are not installed.
|
||||
_features = ExcludeFeaturesWithMissingDependencies(_features.Values).ToDictionary(x => x.GetType(), x => x);
|
||||
|
||||
// Add hosted services in order of priority.
|
||||
foreach (var hostedServiceDescriptor in _hostedServiceDescriptors.OrderBy(x => x.Order))
|
||||
Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.Type));
|
||||
|
||||
// Make sure to use the complete list of features when applying them.
|
||||
foreach (var feature in _features.Values)
|
||||
feature.Apply();
|
||||
|
||||
// Add a registry of enabled features to the service collection for client applications to reflect on what features are installed.
|
||||
var registry = new InstalledFeatureRegistry();
|
||||
foreach (var feature in _features.Values)
|
||||
{
|
||||
var type = feature.GetType();
|
||||
var name = type.Name.Replace("Feature", string.Empty);
|
||||
var ns = "Elsa";
|
||||
var displayName = type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? name;
|
||||
var description = type.GetCustomAttribute<DescriptionAttribute>()?.Description;
|
||||
registry.Add(new(name, ns, displayName, description));
|
||||
}
|
||||
|
||||
Services.AddSingleton<IInstalledFeatureRegistry>(registry);
|
||||
}
|
||||
|
||||
private IEnumerable<IFeature> ExcludeFeaturesWithMissingDependencies(IEnumerable<IFeature> features)
|
||||
{
|
||||
return
|
||||
from feature in features
|
||||
let featureType = feature.GetType()
|
||||
let dependencyOfAttributes = featureType.GetCustomAttributes<DependencyOfAttribute>(true).ToList()
|
||||
let missingDependencies = dependencyOfAttributes.Where(x => !_features.ContainsKey(x.Type)).ToList()
|
||||
where missingDependencies.Count == 0
|
||||
select feature;
|
||||
}
|
||||
|
||||
private void ConfigureFeature(IFeature feature)
|
||||
{
|
||||
if (_configuredFeatures.Contains(feature))
|
||||
return;
|
||||
|
||||
feature.Configure();
|
||||
feature.ConfigureHostedServices();
|
||||
_features[feature.GetType()] = feature;
|
||||
_configuredFeatures.Add(feature);
|
||||
}
|
||||
|
||||
private IFeature GetOrCreateFeature(Type featureType)
|
||||
{
|
||||
return _features.TryGetValue(featureType, out var existingFeature) ? existingFeature : (IFeature)Activator.CreateInstance(featureType, this)!;
|
||||
}
|
||||
|
||||
private HashSet<Type> GetFeatureTypes()
|
||||
{
|
||||
var featureTypes = _features.Keys.ToHashSet();
|
||||
var featureTypesWithDependencies = featureTypes.Concat(featureTypes.SelectMany(GetDependencyTypes)).ToHashSet();
|
||||
return featureTypesWithDependencies.TSort(x => x.GetCustomAttributes<DependsOnAttribute>(true).Select(dependsOn => dependsOn.Type)).ToHashSet();
|
||||
}
|
||||
|
||||
// Recursively get dependency types.
|
||||
private IEnumerable<Type> GetDependencyTypes(Type type)
|
||||
{
|
||||
var dependencies = type.GetCustomAttributes<DependsOnAttribute>(true).Select(dependsOn => dependsOn.Type).ToList();
|
||||
return dependencies.Concat(dependencies.SelectMany(GetDependencyTypes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Features.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a feature descriptor.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public class FeatureDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FeatureDescriptor"/> class.
|
||||
/// </summary>
|
||||
public FeatureDescriptor()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FeatureDescriptor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the feature.</param>
|
||||
/// <param name="ns">The namespace of the feature.</param>
|
||||
/// <param name="displayName">The display name for the feature.</param>
|
||||
/// <param name="description">The description of the feature.</param>
|
||||
public FeatureDescriptor(string name, string ns, string displayName, string? description = default)
|
||||
{
|
||||
Name = name;
|
||||
Namespace = ns;
|
||||
DisplayName = displayName;
|
||||
Description = description ?? "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the feature.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the namespace of the feature.
|
||||
/// </summary>
|
||||
public string Namespace { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full name of the feature.
|
||||
/// </summary>
|
||||
public string FullName => $"{Namespace}.{Name}";
|
||||
|
||||
/// <summary>
|
||||
/// The display name for the feature.
|
||||
/// </summary>
|
||||
public string DisplayName { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The description of the feature.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = "";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Features.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a feature.
|
||||
/// </summary>
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Members)]
|
||||
public interface IFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the module that this feature belongs to.
|
||||
/// </summary>
|
||||
IModule Module { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Configures the feature.
|
||||
/// </summary>
|
||||
void Configure();
|
||||
|
||||
/// <summary>
|
||||
/// Configures the hosted services.
|
||||
/// </summary>
|
||||
void ConfigureHostedServices();
|
||||
|
||||
/// <summary>
|
||||
/// Applies the feature by adding services to the service collection.
|
||||
/// </summary>
|
||||
void Apply();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Elsa.Features.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A thin abstraction on top of <see cref="IServiceCollection"/> to help organize features and dependencies.
|
||||
/// </summary>
|
||||
public interface IModule
|
||||
{
|
||||
/// <summary>
|
||||
/// The service collection being populated.
|
||||
/// </summary>
|
||||
IServiceCollection Services { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary into which features can stash away values for later use.
|
||||
/// </summary>
|
||||
IDictionary<object, object> Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a feature of the specified type has been configured.
|
||||
/// </summary>
|
||||
bool HasFeature<T>() where T : class, IFeature;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a feature of the specified type has been configured.
|
||||
/// </summary>
|
||||
bool HasFeature(Type featureType);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and configures a feature of the specified type.
|
||||
/// </summary>
|
||||
T Configure<T>(Action<T>? configure = null) where T : class, IFeature;
|
||||
|
||||
/// <summary>
|
||||
/// Creates and configures a feature of the specified type.
|
||||
/// </summary>
|
||||
T Configure<T>(Func<IModule, T> factory, Action<T>? configure = null) where T : class, IFeature;
|
||||
|
||||
/// <summary>
|
||||
/// Configures a <see cref="IHostedService"/> using an optional priority to control in which order it will be registered with the service container.
|
||||
/// </summary>
|
||||
IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService;
|
||||
|
||||
/// <summary>
|
||||
/// Configures a <see cref="IHostedService"/> using an optional priority to control in which order it will be registered with the service container.
|
||||
/// </summary>
|
||||
IModule ConfigureHostedService(Type hostedServiceType, int priority = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Will apply all configured features, causing the <see cref="Services"/> collection to be populated.
|
||||
/// </summary>
|
||||
void Apply();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Elsa.Features.Contracts;
|
||||
using Elsa.Features.Models;
|
||||
|
||||
namespace Elsa.Features.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class InstalledFeatureRegistry : IInstalledFeatureRegistry
|
||||
{
|
||||
private readonly Dictionary<string, FeatureDescriptor> _descriptors = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(FeatureDescriptor descriptor) => _descriptors[descriptor.FullName] = descriptor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<FeatureDescriptor> List() => _descriptors.Values;
|
||||
|
||||
/// <inheritdoc />
|
||||
public FeatureDescriptor? Find(string fullName) => _descriptors.GetValueOrDefault(fullName);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Elsa.Mediator.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// A base class for channels.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the items in the channel.</typeparam>
|
||||
public abstract class ChannelBase<T>
|
||||
{
|
||||
private readonly Channel<T> _channel;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChannelBase{T}"/> class.
|
||||
/// </summary>
|
||||
protected ChannelBase()
|
||||
{
|
||||
_channel = Channel.CreateUnbounded<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the writer for the channel.
|
||||
/// </summary>
|
||||
public ChannelWriter<T> Writer => _channel.Writer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reader for the channel.
|
||||
/// </summary>
|
||||
public ChannelReader<T> Reader => _channel.Reader;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Elsa.Mediator.Abstractions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
|
||||
namespace Elsa.Mediator.Channels;
|
||||
|
||||
/// <inheritdoc cref="Elsa.Mediator.Contracts.ICommandsChannel" />
|
||||
public class CommandsChannel : ChannelBase<CommandContext>, ICommandsChannel
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Elsa.Mediator.Abstractions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Models;
|
||||
|
||||
namespace Elsa.Mediator.Channels;
|
||||
|
||||
/// <inheritdoc cref="Elsa.Mediator.Contracts.IJobsChannel" />
|
||||
public class JobsChannel : ChannelBase<EnqueuedJob>, IJobsChannel
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Elsa.Mediator.Abstractions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
|
||||
namespace Elsa.Mediator.Channels;
|
||||
|
||||
/// <inheritdoc cref="Elsa.Mediator.Contracts.INotificationsChannel" />
|
||||
public class NotificationsChannel : ChannelBase<NotificationContext>, INotificationsChannel
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Elsa.Mediator.Contexts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Mediator.CommandStrategies;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes command handlers in the background and does not wait for the result.
|
||||
/// </summary>
|
||||
public class BackgroundStrategy : ICommandStrategy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<TResult> ExecuteAsync<TResult>(CommandStrategyContext context)
|
||||
{
|
||||
var commandsChannel = context.ServiceProvider.GetRequiredService<ICommandsChannel>();
|
||||
await commandsChannel.Writer.WriteAsync(context.CommandContext, context.CancellationToken);
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Elsa.Mediator.Contexts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Extensions;
|
||||
|
||||
namespace Elsa.Mediator.CommandStrategies;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes command handlers using the default strategy.
|
||||
/// </summary>
|
||||
public class DefaultStrategy : ICommandStrategy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<TResult> ExecuteAsync<TResult>(CommandStrategyContext context)
|
||||
{
|
||||
var commandContext = context.CommandContext;
|
||||
var command = commandContext.Command;
|
||||
var commandType = command.GetType();
|
||||
var handleMethod = commandType.GetCommandHandlerMethod();
|
||||
var handler = context.Handler;
|
||||
|
||||
return await handler.InvokeAsync<TResult>(handleMethod, commandContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Elsa.Mediator.CommandStrategies;
|
||||
|
||||
namespace Elsa.Mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a set of strategies for publishing events.
|
||||
/// </summary>
|
||||
public static class CommandStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes command handlers in parallel and waits for the result.
|
||||
/// </summary>
|
||||
public static readonly DefaultStrategy Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Invokes command handlers in the background and does not wait for the result.
|
||||
/// </summary>
|
||||
public static readonly BackgroundStrategy Background = new();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
|
||||
namespace Elsa.Mediator.Contexts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a context for executing a command.
|
||||
/// </summary>
|
||||
/// <param name="CommandContext">The command context to execute.</param>
|
||||
/// <param name="Handler">The command handler.</param>
|
||||
/// <param name="ServiceProvider">The service provider to resolve services from.</param>
|
||||
/// <param name="CancellationToken">The cancellation token.</param>
|
||||
public record CommandStrategyContext(CommandContext CommandContext, ICommandHandler Handler, IServiceProvider ServiceProvider, CancellationToken CancellationToken = default);
|
||||
@@ -0,0 +1,15 @@
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Mediator.Contexts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a context for publishing events.
|
||||
/// </summary>
|
||||
/// <param name="NotificationContext">The notification to publish.</param>
|
||||
/// <param name="Handlers">The handlers to publish the notification to.</param>
|
||||
/// <param name="Logger">The logger.</param>
|
||||
/// <param name="ServiceProvider">The service provider to resolve services from.</param>
|
||||
/// <param name="CancellationToken">The cancellation token.</param>
|
||||
public record NotificationStrategyContext(NotificationContext NotificationContext, INotificationHandler[] Handlers, ILogger Logger, IServiceProvider ServiceProvider, CancellationToken CancellationToken = default);
|
||||
@@ -0,0 +1,20 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public interface ICommand
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
[PublicAPI]
|
||||
public interface ICommand<T> : ICommand
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Elsa.Mediator.Models;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command handler.
|
||||
/// </summary>
|
||||
public interface ICommandHandler
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command handler.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCommand">The type of the command to handle.</typeparam>
|
||||
/// <typeparam name="TResult">The type of the result.</typeparam>
|
||||
public interface ICommandHandler<in TCommand, TResult> : ICommandHandler where TCommand : ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to handle.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The result.</returns>
|
||||
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command handler.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCommand">The type of the command to handle.</typeparam>
|
||||
public interface ICommandHandler<in TCommand> : ICommandHandler<TCommand, Unit> where TCommand : ICommand
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command.
|
||||
/// </summary>
|
||||
public interface ICommandSender
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a command using the default strategy.
|
||||
/// </summary>
|
||||
Task<T> SendAsync<T>(ICommand<T> command, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the default strategy.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="headers">Any headers to pass along.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <returns>The result.</returns>
|
||||
Task<T> SendAsync<T>(ICommand<T> command, IDictionary<object, object> headers, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the specified strategy.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="strategy">The command strategy to use.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <returns>The result.</returns>
|
||||
Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy strategy, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the specified strategy.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="strategy">The command strategy to use.</param>
|
||||
/// <param name="headers">Any headers to pass along.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <returns>The result.</returns>
|
||||
Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the default strategy.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task SendAsync(ICommand command, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the specified strategy.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="strategy">The command strategy to use.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task SendAsync(ICommand command, ICommandStrategy strategy, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the specified strategy.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="strategy">The command strategy to use.</param>
|
||||
/// <param name="headers">Any headers to pass along.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task SendAsync(ICommand command, ICommandStrategy strategy, IDictionary<object, object> headers, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Elsa.Mediator.Contexts;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a strategy for executing commands.
|
||||
/// </summary>
|
||||
public interface ICommandStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a command.
|
||||
/// </summary>
|
||||
/// <param name="context">The context for executing the command.</param>
|
||||
Task<TResult> ExecuteAsync<TResult>(CommandStrategyContext context);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// A channel that can be used to enqueue commands.
|
||||
/// </summary>
|
||||
public interface ICommandsChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the writer for the commands queue.
|
||||
/// </summary>
|
||||
ChannelWriter<CommandContext> Writer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reader for the commands queue.
|
||||
/// </summary>
|
||||
ChannelReader<CommandContext> Reader { get; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a consumer of a given message type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the message to consume.</typeparam>
|
||||
public interface IConsumer<in T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Consumes the given message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to consume.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
ValueTask ConsumeAsync(T message, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Elsa.Mediator.Contexts;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a strategy for publishing events.
|
||||
/// </summary>
|
||||
public interface IEventPublishingStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes an event.
|
||||
/// </summary>
|
||||
/// <param name="context">The context for publishing the event.</param>
|
||||
Task PublishAsync(NotificationStrategyContext context);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Manages jobs.
|
||||
/// </summary>
|
||||
public interface IJobQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a pending job. The caller must use the returned ID to schedule the job for execution.
|
||||
/// </summary>
|
||||
string Create(Func<CancellationToken, Task> job);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a job for execution.
|
||||
/// </summary>
|
||||
/// <param name="jobId">The ID of the job to enqueue.</param>
|
||||
void Enqueue(string jobId);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a job for execution.
|
||||
/// </summary>
|
||||
/// <param name="job">The job to enqueue.</param>
|
||||
/// <returns>The ID of the job.</returns>
|
||||
string Enqueue(Func<CancellationToken, Task> job);
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues a job.
|
||||
/// </summary>
|
||||
/// <param name="jobId">The ID of the job to dequeue.</param>
|
||||
/// <returns><c>true</c> if the job was dequeued; otherwise, <c>false</c>.</returns>
|
||||
bool Dequeue(string jobId);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a job.
|
||||
/// </summary>
|
||||
/// <param name="jobId">The ID of the job to cancel.</param>
|
||||
/// <returns><c>true</c> if the job was cancelled; otherwise, <c>false</c>.</returns>
|
||||
bool Cancel(string jobId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Models;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// A channel that can be used to enqueue jobs.
|
||||
/// </summary>
|
||||
public interface IJobsChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the writer for the job queue.
|
||||
/// </summary>
|
||||
ChannelWriter<EnqueuedJob> Writer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reader for the job queue.
|
||||
/// </summary>
|
||||
ChannelReader<EnqueuedJob> Reader { get; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mediator that can send requests, commands and publish events.
|
||||
/// </summary>
|
||||
public interface IMediator : IRequestSender, ICommandSender, INotificationSender
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a notification.
|
||||
/// </summary>
|
||||
public interface INotification
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a notification handler.
|
||||
/// </summary>
|
||||
public interface INotificationHandler
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a notification handler.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the notification.</typeparam>
|
||||
public interface INotificationHandler<in T> : INotificationHandler where T : INotification
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the given notification.
|
||||
/// </summary>
|
||||
/// <param name="notification">The notification to handle.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task HandleAsync(T notification, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Elsa.Mediator.PublishingStrategies;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes notifications.
|
||||
/// </summary>
|
||||
public interface INotificationSender
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes the given notification.
|
||||
/// </summary>
|
||||
/// <param name="notification">The notification to publish.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task SendAsync(INotification notification, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the given notification.
|
||||
/// </summary>
|
||||
/// <param name="notification">The notification to publish.</param>
|
||||
/// <param name="strategy"><see cref="SequentialProcessingStrategy"/><see cref="ParallelProcessingStrategy"/></param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task SendAsync(INotification notification, IEventPublishingStrategy? strategy, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// A channel that can be used to enqueue notifications.
|
||||
/// </summary>
|
||||
public interface INotificationsChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the writer for the notification queue.
|
||||
/// </summary>
|
||||
ChannelWriter<NotificationContext> Writer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reader for the notification queue.
|
||||
/// </summary>
|
||||
ChannelReader<NotificationContext> Reader { get; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request.
|
||||
/// </summary>
|
||||
public interface IRequest
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the response.</typeparam>
|
||||
public interface IRequest<T> : IRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request handler.
|
||||
/// </summary>
|
||||
public interface IRequestHandler
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request handler.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">The type of the request.</typeparam>
|
||||
/// <typeparam name="TResponse">The type of the response.</typeparam>
|
||||
public interface IRequestHandler<in TRequest, TResponse> : IRequestHandler where TRequest : IRequest<TResponse>?
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the given request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request to handle.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The response.</returns>
|
||||
Task<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Elsa.Mediator.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request sender.
|
||||
/// </summary>
|
||||
public interface IRequestSender
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request to send.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="T">The type of the response.</typeparam>
|
||||
/// <returns>The response.</returns>
|
||||
Task<T> SendAsync<T>(IRequest<T> request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides a mediator service inspired by MediatR.
|
||||
</Description>
|
||||
<PackageTags>elsa module common mediator</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,228 @@
|
||||
using System.Reflection;
|
||||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.HostedServices;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
using Elsa.Mediator.Middleware.Command.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Elsa.Mediator.Middleware.Notification.Contracts;
|
||||
using Elsa.Mediator.Middleware.Request;
|
||||
using Elsa.Mediator.Middleware.Request.Contracts;
|
||||
using Elsa.Mediator.Models;
|
||||
using Elsa.Mediator.Options;
|
||||
using Elsa.Mediator.Services;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Adds mediator services to the <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds mediator services to the <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddMediator(this IServiceCollection services, Action<MediatorOptions>? configure = default)
|
||||
{
|
||||
services.Configure(configure ?? (_ => { }));
|
||||
|
||||
return services
|
||||
.AddScoped<IMediator, DefaultMediator>()
|
||||
.AddScoped<IRequestSender>(sp => sp.GetRequiredService<IMediator>())
|
||||
.AddScoped<ICommandSender>(sp => sp.GetRequiredService<IMediator>())
|
||||
.AddScoped<INotificationSender>(sp => sp.GetRequiredService<IMediator>())
|
||||
.AddSingleton<IRequestPipeline, RequestPipeline>()
|
||||
.AddSingleton<ICommandPipeline, CommandPipeline>()
|
||||
.AddSingleton<INotificationPipeline, NotificationPipeline>()
|
||||
;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds mediator hosted services to the <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddMediatorHostedServices(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddSingleton<INotificationsChannel, NotificationsChannel>()
|
||||
.AddSingleton<ICommandsChannel, CommandsChannel>()
|
||||
.AddSingleton<IJobsChannel, JobsChannel>()
|
||||
.AddSingleton<IJobQueue, JobQueue>()
|
||||
.AddHostedService<JobRunnerHostedService>()
|
||||
.AddHostedService<BackgroundCommandSenderHostedService>()
|
||||
.AddHostedService<BackgroundEventPublisherHostedService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="Channel{T}"/> to the <see cref="IServiceCollection"/> and a hosted service that continuously reads from the channel and executes each received message.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddMessageChannel<T>(this IServiceCollection services, int workerCount = 1) where T : notnull
|
||||
{
|
||||
services.AddSingleton<Channel<T>>(_ => CreateChannel<T>());
|
||||
|
||||
services.AddHostedService(sp =>
|
||||
{
|
||||
var channel = sp.GetRequiredService<Channel<T>>();
|
||||
var consumers = sp.GetServices<IConsumer<T>>();
|
||||
var logger = sp.GetRequiredService<ILogger<MessageProcessorHostedService<T>>>();
|
||||
return new MessageProcessorHostedService<T>(workerCount, channel, consumers, logger);
|
||||
});
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a channel consumer.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddMessageConsumer<T, TConsumer>(this IServiceCollection services) where TConsumer : class, IConsumer<T> where T : notnull
|
||||
{
|
||||
return services.AddScoped<IConsumer<T>, TConsumer>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="ICommandHandler{T}"/> with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCommandHandler<THandler>(this IServiceCollection services) where THandler : class, ICommandHandler =>
|
||||
services.AddScoped<ICommandHandler, THandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="ICommandHandler{T}"/> with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCommandHandler<THandler, TCommand>(this IServiceCollection services)
|
||||
where THandler : class, ICommandHandler<TCommand> where TCommand : ICommand<Unit> =>
|
||||
services.AddCommandHandler<THandler, TCommand, Unit>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="ICommandHandler{T}"/> with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCommandHandler<THandler, TCommand, TResult>(this IServiceCollection services)
|
||||
where THandler : class, ICommandHandler<TCommand, TResult>
|
||||
where TCommand : ICommand<TResult>
|
||||
{
|
||||
return services.AddScoped<ICommandHandler, THandler>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="ICommandHandler{T}"/> with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddNotificationHandler<THandler>(this IServiceCollection services)
|
||||
where THandler : class, INotificationHandler
|
||||
{
|
||||
return services.AddScoped<INotificationHandler, THandler>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="INotificationHandler{T}"/> with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddNotificationHandler<THandler, TNotification>(this IServiceCollection services)
|
||||
where THandler : class, INotificationHandler<TNotification>
|
||||
where TNotification : INotification =>
|
||||
services.AddScoped<INotificationHandler, THandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="INotificationHandler{T}"/> with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddNotificationHandler<THandler, TNotification>(this IServiceCollection services, Func<IServiceProvider, THandler> factory)
|
||||
where THandler : class, INotificationHandler<TNotification>
|
||||
where TNotification : INotification =>
|
||||
services.AddScoped<INotificationHandler, THandler>(factory);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="IRequestHandler{TRequest,TResponse}"/> with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRequestHandler<THandler, TRequest, TResponse>(this IServiceCollection services)
|
||||
where THandler : class, IRequestHandler<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>?
|
||||
{
|
||||
return services.AddScoped<IRequestHandler, THandler>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified <c>TMarker</c> type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddHandlersFrom<TMarker>(this IServiceCollection services) => services.AddHandlersFrom(typeof(TMarker));
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified marker type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFrom(markerType.Assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the specified assembly with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddHandlersFrom(this IServiceCollection services, Assembly assembly)
|
||||
{
|
||||
return services
|
||||
.AddNotificationHandlersFrom(assembly)
|
||||
.AddRequestHandlersFrom(assembly)
|
||||
.AddCommandHandlersFrom(assembly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified <c>TMarker</c> type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddNotificationHandlersFrom<TMarker>(this IServiceCollection services) => services.AddHandlersFromInternal<INotificationHandler>(typeof(TMarker));
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified marker type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddNotificationHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFromInternal<INotificationHandler>(markerType);
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the specified assembly with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddNotificationHandlersFrom(this IServiceCollection services, Assembly assembly) => services.AddHandlersFromInternal<INotificationHandler>(assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified <c>TMarker</c> type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRequestHandlersFrom<TMarker>(this IServiceCollection services) => services.AddHandlersFromInternal<IRequestHandler>(typeof(TMarker));
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified marker type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRequestHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFromInternal<IRequestHandler>(markerType);
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the specified assembly with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRequestHandlersFrom(this IServiceCollection services, Assembly assembly) => services.AddHandlersFromInternal<IRequestHandler>(assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified <c>TMarker</c> type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCommandHandlersFrom<TMarker>(this IServiceCollection services) => services.AddHandlersFromInternal<ICommandHandler>(typeof(TMarker));
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the assembly of the specified marker type with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCommandHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFromInternal<ICommandHandler>(markerType);
|
||||
|
||||
/// <summary>
|
||||
/// Registers all handlers from the specified assembly with the service container.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCommandHandlersFrom(this IServiceCollection services, Assembly assembly) => services.AddHandlersFromInternal<ICommandHandler>(assembly);
|
||||
|
||||
private static IServiceCollection AddHandlersFromInternal<TService, TMarker>(this IServiceCollection services) => services.AddHandlersFromInternal<TService>(typeof(TMarker));
|
||||
private static IServiceCollection AddHandlersFromInternal<TService>(this IServiceCollection services, Type assemblyMarkerType) => services.AddHandlersFromInternal<TService>(assemblyMarkerType.Assembly);
|
||||
|
||||
private static IServiceCollection AddHandlersFromInternal<TService>(this IServiceCollection services, Assembly assembly)
|
||||
{
|
||||
var serviceType = typeof(TService);
|
||||
var types = assembly.DefinedTypes.Where(x => serviceType.IsAssignableFrom(x));
|
||||
|
||||
foreach (var type in types)
|
||||
services.AddScoped(serviceType, type);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static Channel<T> CreateChannel<T>() => Channel.CreateUnbounded<T>(new UnboundedChannelOptions());
|
||||
private static ChannelReader<T> CreateChannelReader<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Reader;
|
||||
private static ChannelWriter<T> CreateChannelWriter<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Writer;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Elsa.Mediator.Models;
|
||||
|
||||
namespace Elsa.Mediator.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Contains helper methods for invoking notification handlers.
|
||||
/// </summary>
|
||||
public static class HandlerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the handle method for a notification handler of the given notification type.
|
||||
/// </summary>
|
||||
/// <param name="notificationType">The notification type.</param>
|
||||
/// <returns>The handle method.</returns>
|
||||
public static MethodInfo GetNotificationHandlerMethod(this Type notificationType)
|
||||
{
|
||||
var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType);
|
||||
return handlerType.GetMethod("HandleAsync")!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handle method for a command handler of the given command type.
|
||||
/// </summary>
|
||||
/// <param name="commandType">The command type.</param>
|
||||
/// <returns>The handle method.</returns>
|
||||
public static MethodInfo GetCommandHandlerMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type commandType)
|
||||
{
|
||||
var commandTypeInterfaces = commandType.GetInterfaces();
|
||||
var commandTypeInterface = commandTypeInterfaces.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICommand<>));
|
||||
var resultType = commandTypeInterface?.GetGenericArguments()[0] ?? typeof(Unit);
|
||||
var handlerType = typeof(ICommandHandler<,>).MakeGenericType(commandType, resultType);
|
||||
return handlerType.GetMethod("HandleAsync")!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the given handler for the given notification.
|
||||
/// </summary>
|
||||
/// <param name="handler">The handler to invoke.</param>
|
||||
/// <param name="handleMethod">The handle method.</param>
|
||||
/// <param name="notificationContext">The notification to handle.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static Task InvokeAsync(this INotificationHandler handler, MethodBase handleMethod, NotificationContext notificationContext)
|
||||
{
|
||||
var notification = notificationContext.Notification;
|
||||
var cancellationToken = notificationContext.CancellationToken;
|
||||
return (Task)handleMethod.Invoke(handler, [notification, cancellationToken])!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the given handler for the given command.
|
||||
/// </summary>
|
||||
/// <param name="handler">The handler to invoke.</param>
|
||||
/// <param name="handleMethod">The handle method.</param>
|
||||
/// <param name="commandContext">The command to handle.</param>
|
||||
public static Task<TResult> InvokeAsync<TResult>(this ICommandHandler handler, MethodBase handleMethod, CommandContext commandContext)
|
||||
{
|
||||
var command = commandContext.Command;
|
||||
var cancellationToken = commandContext.CancellationToken;
|
||||
var task = (Task<TResult>)handleMethod.Invoke(handler, [command, cancellationToken])!;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Options;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a set of extension methods for <see cref="IMediator"/>.
|
||||
/// </summary>
|
||||
public static class MediatorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a notification using the default notification strategy configured via <see cref="MediatorOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="mediator">The mediator.</param>
|
||||
/// <param name="notification">The notification to send.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static async Task SendAsync(this IMediator mediator, INotification notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await mediator.SendAsync(notification, default, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification using the default notification strategy configured via <see cref="MediatorOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="publisher">The publisher.</param>
|
||||
/// <param name="notification">The notification to send.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static async Task SendAsync(this INotificationSender publisher, INotification notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await publisher.SendAsync(notification, default, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the default command strategy configured via <see cref="MediatorOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="mediator">The mediator.</param>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static async Task SendAsync(this IMediator mediator, ICommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await mediator.SendAsync(command, default, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command using the default command strategy configured via <see cref="MediatorOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="commandSender">The command sender.</param>
|
||||
/// <param name="command">The command to send.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public static async Task SendAsync(this ICommandSender commandSender, ICommand command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await commandSender.SendAsync(command, default, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which commands can be sent, executing each received command.
|
||||
/// </summary>
|
||||
public class BackgroundCommandSenderHostedService : BackgroundService
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly ICommandsChannel _commandsChannel;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly List<Channel<CommandContext>> _outputs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public BackgroundCommandSenderHostedService(IOptions<MediatorOptions> options, ICommandsChannel commandsChannel, IServiceScopeFactory scopeFactory, ILogger<BackgroundCommandSenderHostedService> logger)
|
||||
{
|
||||
_workerCount = options.Value.CommandWorkerCount;
|
||||
_commandsChannel = commandsChannel; // The shared input channel for all commands
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_outputs = new(_workerCount); // Prepare a list to hold worker-specific channels
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var index = 0; // Used for round-robin distribution of work
|
||||
|
||||
// Set up worker channels and start background tasks for each worker
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<CommandContext>();
|
||||
_outputs.Add(output);
|
||||
// Start a background task that processes commands from this worker's channel
|
||||
_ = ReadOutputAsync(output, cancellationToken);
|
||||
}
|
||||
|
||||
// Main dispatcher loop: read from the input channel and distribute to worker channels
|
||||
await foreach (var commandContext in _commandsChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = _outputs[index];
|
||||
await output.Writer.WriteAsync(commandContext, cancellationToken);
|
||||
// Round-robin distribution - move to next worker
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
|
||||
// If the input channel is completed, complete all worker channels
|
||||
foreach (var output in _outputs)
|
||||
output.Writer.Complete();
|
||||
}
|
||||
|
||||
private async Task ReadOutputAsync(Channel<CommandContext> output, CancellationToken cancellationToken)
|
||||
{
|
||||
// Worker task: process commands from the worker's channel
|
||||
await foreach (var commandContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a fresh scope for each command to ensure proper service lifetime
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
|
||||
// Link the service cancellation token with the command's token to ensure proper cancellation
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken,
|
||||
commandContext.CancellationToken);
|
||||
|
||||
// Process the command using the command sender service with the linked token
|
||||
await commandSender.SendAsync(
|
||||
commandContext.Command,
|
||||
CommandStrategy.Default,
|
||||
commandContext.Headers,
|
||||
linkedTokenSource.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Log errors but continue processing other commands
|
||||
_logger.LogError(e, "An unhandled exception occurred while processing the queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which notifications can be sent, publishing each received notification.
|
||||
/// </summary>
|
||||
public class BackgroundEventPublisherHostedService : BackgroundService
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly INotificationsChannel _notificationsChannel;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly List<Channel<NotificationContext>> _outputs;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public BackgroundEventPublisherHostedService(IOptions<MediatorOptions> options, INotificationsChannel notificationsChannel, IServiceScopeFactory scopeFactory, ILogger<BackgroundEventPublisherHostedService> logger)
|
||||
{
|
||||
_workerCount = options.Value.NotificationWorkerCount;
|
||||
_notificationsChannel = notificationsChannel;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_outputs = new(_workerCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Index to round-robin distribute notifications across worker channels
|
||||
var index = 0;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var notificationSender = scope.ServiceProvider.GetRequiredService<INotificationSender>();
|
||||
|
||||
// Create multiple output channels and start worker tasks for parallel processing
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var output = Channel.CreateUnbounded<NotificationContext>();
|
||||
_outputs.Add(output);
|
||||
// Start a background task to process notifications from this output channel
|
||||
_ = ReadOutputAsync(output, notificationSender, cancellationToken);
|
||||
}
|
||||
|
||||
var channelReader = _notificationsChannel.Reader;
|
||||
|
||||
// Continuously read notifications from the input channel and distribute them to worker channels
|
||||
// using round-robin distribution for load balancing
|
||||
await foreach (var notification in channelReader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var output = _outputs[index];
|
||||
await output.Writer.WriteAsync(notification, cancellationToken);
|
||||
// Move to the next worker in a circular fashion
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
|
||||
// When the input channel is completed, complete all output channels
|
||||
foreach (var output in _outputs)
|
||||
{
|
||||
output.Writer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes notifications from an output channel asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="output">The channel to read notifications from</param>
|
||||
/// <param name="notificationSender">The service used to send notifications</param>
|
||||
/// <param name="cancellationToken">Cancellation token from the hosted service</param>
|
||||
private async Task ReadOutputAsync(Channel<NotificationContext> output, INotificationSender notificationSender, CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var notificationContext in output.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var notification = notificationContext.Notification;
|
||||
// Link the cancellation tokens so that cancellation can happen from either source
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, notificationContext.CancellationToken);
|
||||
await notificationSender.SendAsync(notification, NotificationStrategy.Sequential, linkedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
_logger.LogDebug(e, "An operation was cancelled while processing the queue");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "An unhandled exception occured while processing the queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Options;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Mediator.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// A hosted service that runs jobs.
|
||||
/// </summary>
|
||||
public class JobRunnerHostedService : BackgroundService
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly IJobsChannel _jobsChannel;
|
||||
private readonly ILogger<JobRunnerHostedService> _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public JobRunnerHostedService(IOptions<MediatorOptions> options, IJobsChannel jobsChannel, ILogger<JobRunnerHostedService> logger)
|
||||
{
|
||||
_workerCount = options.Value.JobWorkerCount;
|
||||
_jobsChannel = jobsChannel;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Create an array of worker tasks to process jobs in parallel
|
||||
var workers = new Task[_workerCount];
|
||||
|
||||
// Start multiple workers (tasks) to process jobs concurrently
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
workers[i] = ProcessJobsAsync(stoppingToken);
|
||||
|
||||
// Wait for all worker tasks to complete (typically when the application is shutting down)
|
||||
await Task.WhenAll(workers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuously processes jobs from the job channel until cancellation is requested.
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken">Cancellation token from the hosted service</param>
|
||||
private async Task ProcessJobsAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Process all jobs from the channel until it's completed or cancellation is requested
|
||||
await foreach (var jobItem in _jobsChannel.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Link the cancellation tokens so that cancellation can happen from either source
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, jobItem.CancellationTokenSource.Token);
|
||||
await jobItem.Action(linkedTokenSource.Token);
|
||||
_logger.LogInformation("Worker {CurrentTaskId} processed job {JobId}", Task.CurrentId, jobItem.JobId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Job {JobId} was canceled", jobItem.JobId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Job {JobId} failed", jobItem.JobId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Notify that the job has completed (whether successfully or not)
|
||||
jobItem.OnJobCompleted(jobItem.JobId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Threading.Channels;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Mediator.HostedServices;
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from a channel to which commands can be sent, executing each received command.
|
||||
/// </summary>
|
||||
public class MessageProcessorHostedService<T> : BackgroundService where T : notnull
|
||||
{
|
||||
private readonly int _workerCount;
|
||||
private readonly Channel<T> _channel;
|
||||
private readonly IEnumerable<IConsumer<T>> _consumers;
|
||||
private readonly ILogger _logger;
|
||||
private readonly List<MessageWorker<T>> _workers;
|
||||
|
||||
/// <inheritdoc />
|
||||
// ReSharper disable once ContextualLoggerProblem
|
||||
public MessageProcessorHostedService(int workerCount, Channel<T> channel, IEnumerable<IConsumer<T>> consumers, ILogger<MessageProcessorHostedService<T>> logger)
|
||||
{
|
||||
_workerCount = workerCount;
|
||||
_channel = channel;
|
||||
_consumers = consumers;
|
||||
_logger = logger;
|
||||
_workers = new List<MessageWorker<T>>(workerCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var index = 0;
|
||||
|
||||
for (var i = 0; i < _workerCount; i++)
|
||||
{
|
||||
var worker = new MessageWorker<T>(Channel.CreateUnbounded<T>(), _consumers, _logger);
|
||||
_workers.Add(worker);
|
||||
_ = worker.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var worker = _workers[index];
|
||||
await worker.DeliverMessageAsync(message, cancellationToken);
|
||||
index = (index + 1) % _workerCount;
|
||||
}
|
||||
|
||||
foreach (var worker in _workers)
|
||||
worker.Complete();
|
||||
|
||||
_workers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a worker that continuously reads from a channel and processes each received message.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of message to process.</typeparam>
|
||||
public class MessageWorker<T> where T : notnull
|
||||
{
|
||||
private readonly Channel<T> _channel;
|
||||
private readonly IEnumerable<IConsumer<T>> _consumers;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageWorker{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to read from.</param>
|
||||
/// <param name="consumers">The consumers that will process each received message.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public MessageWorker(Channel<T> channel, IEnumerable<IConsumer<T>> consumers, ILogger logger)
|
||||
{
|
||||
_channel = channel;
|
||||
_consumers = consumers;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuously reads from the channel and processes each received message.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken))
|
||||
foreach (var consumer in _consumers)
|
||||
await InvokeConsumerAsync(consumer, message, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delivers a message to the channel.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to deliver.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public async Task DeliverMessageAsync(T message, CancellationToken cancellationToken)
|
||||
{
|
||||
await _channel.Writer.WriteAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the channel.
|
||||
/// </summary>
|
||||
public void Complete()
|
||||
{
|
||||
_channel.Writer.Complete();
|
||||
}
|
||||
|
||||
private async Task InvokeConsumerAsync(IConsumer<T> consumer, T message, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await consumer.ConsumeAsync(message, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while invoking consumer {ConsumerType} for message {MessageType}", consumer.GetType(), message.GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Elsa.Mediator.Contracts;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Command;
|
||||
|
||||
/// <summary>
|
||||
/// Provides context for a command.
|
||||
/// </summary>
|
||||
public class CommandContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CommandContext"/> class.
|
||||
/// </summary>
|
||||
public CommandContext(ICommand command, ICommandStrategy commandStrategy, Type resultType, IDictionary<object, object> headers, IServiceProvider serviceProvider, CancellationToken cancellationToken)
|
||||
{
|
||||
Command = command;
|
||||
CommandStrategy = commandStrategy;
|
||||
ResultType = resultType;
|
||||
Headers = headers;
|
||||
ServiceProvider = serviceProvider;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command.
|
||||
/// </summary>
|
||||
public ICommand Command { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command strategy.
|
||||
/// </summary>
|
||||
public ICommandStrategy CommandStrategy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the headers associated with the command context.
|
||||
/// </summary>
|
||||
public IDictionary<object, object> Headers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service provider used to resolve services for the command context.
|
||||
/// </summary>
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the result type.
|
||||
/// </summary>
|
||||
public Type ResultType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cancellation token.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the result.
|
||||
/// </summary>
|
||||
public object? Result { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Elsa.Mediator.Middleware.Command;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command middleware delegate.
|
||||
/// </summary>
|
||||
public delegate ValueTask CommandMiddlewareDelegate(CommandContext context);
|
||||
@@ -0,0 +1,33 @@
|
||||
using Elsa.Mediator.Middleware.Command.Contracts;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Command;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class CommandPipeline : ICommandPipeline
|
||||
{
|
||||
private readonly CommandPipelineBuilder _builder;
|
||||
private CommandMiddlewareDelegate _pipeline = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public CommandPipeline(IServiceProvider serviceProvider)
|
||||
{
|
||||
_builder = new(serviceProvider);
|
||||
Setup(x => x.UseCommandInvoker().UseCommandLogging());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CommandMiddlewareDelegate Pipeline => _pipeline;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CommandMiddlewareDelegate Setup(Action<ICommandPipelineBuilder>? setup = null)
|
||||
{
|
||||
setup?.Invoke(_builder);
|
||||
_pipeline = _builder.Build();
|
||||
return _pipeline;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InvokeAsync(CommandContext context) => await Pipeline(context);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Elsa.Mediator.Middleware.Command.Contracts;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Command;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class CommandPipelineBuilder : ICommandPipelineBuilder
|
||||
{
|
||||
private const string ServicesKey = "mediator.Services";
|
||||
private readonly List<Func<CommandMiddlewareDelegate, CommandMiddlewareDelegate>> _components = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CommandPipelineBuilder"/> class.
|
||||
/// </summary>
|
||||
public CommandPipelineBuilder(IServiceProvider serviceProvider)
|
||||
{
|
||||
ApplicationServices = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, object?> Properties { get; } = new Dictionary<string, object?>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServiceProvider ApplicationServices
|
||||
{
|
||||
get => GetProperty<IServiceProvider>(ServicesKey)!;
|
||||
set => SetProperty(ServicesKey, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICommandPipelineBuilder Use(Func<CommandMiddlewareDelegate, CommandMiddlewareDelegate> middleware)
|
||||
{
|
||||
_components.Add(middleware);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICommandPipelineBuilder Use(int index, Func<CommandMiddlewareDelegate, CommandMiddlewareDelegate> middleware)
|
||||
{
|
||||
_components.Insert(index, middleware);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICommandPipelineBuilder Remove(Func<CommandMiddlewareDelegate, CommandMiddlewareDelegate> middleware)
|
||||
{
|
||||
_components.Remove(middleware);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICommandPipelineBuilder RemoveAt(int index)
|
||||
{
|
||||
_components.RemoveAt(index);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICommandPipelineBuilder Clear()
|
||||
{
|
||||
_components.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CommandMiddlewareDelegate Build()
|
||||
{
|
||||
CommandMiddlewareDelegate pipeline = _ => new();
|
||||
|
||||
for (var i = _components.Count - 1; i >= 0; i--)
|
||||
pipeline = _components[i](pipeline);
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
private T? GetProperty<T>(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default;
|
||||
private void SetProperty<T>(string key, T value) => Properties[key] = value;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Elsa.Mediator.Middleware.Command.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command.Components;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Command;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="ICommandPipelineBuilder"/>.
|
||||
/// </summary>
|
||||
public static class CommandPipelineBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="CommandHandlerInvokerMiddleware"/> to the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="builder">The pipeline builder.</param>
|
||||
/// <returns>The pipeline builder.</returns>
|
||||
public static ICommandPipelineBuilder UseCommandInvoker(this ICommandPipelineBuilder builder) => builder.UseMiddleware<CommandHandlerInvokerMiddleware>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds the <see cref="CommandLoggingMiddleware"/> to the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="builder">The pipeline builder.</param>
|
||||
/// <returns>The pipeline builder.</returns>
|
||||
public static ICommandPipelineBuilder UseCommandLogging(this ICommandPipelineBuilder builder) => builder.UseMiddleware<CommandLoggingMiddleware>();
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using Elsa.Mediator.Contexts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Command.Contracts;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Command.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A command middleware that invokes the command.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class CommandHandlerInvokerMiddleware(CommandMiddlewareDelegate next) : ICommandMiddleware
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InvokeAsync(CommandContext context)
|
||||
{
|
||||
// Find all handlers for the specified command.
|
||||
var command = context.Command;
|
||||
var commandType = command.GetType();
|
||||
var resultType = context.ResultType;
|
||||
var handlerType = typeof(ICommandHandler<,>).MakeGenericType(commandType, resultType);
|
||||
var serviceProvider = context.ServiceProvider;
|
||||
var commandHandlers = serviceProvider.GetServices<ICommandHandler>();
|
||||
var handlers = commandHandlers.DistinctBy(x => x.GetType()).Where(x => handlerType.IsInstanceOfType(x)).ToArray();
|
||||
|
||||
if (handlers.Length == 0)
|
||||
throw new InvalidOperationException($"There is no handler to handle the {commandType.FullName} command");
|
||||
|
||||
if (handlers.Length > 1)
|
||||
throw new InvalidOperationException($"Multiple handlers were found to handle the {commandType.FullName} command");
|
||||
|
||||
var handler = handlers.First();
|
||||
var strategyContext = new CommandStrategyContext(context, handler, serviceProvider, context.CancellationToken);
|
||||
var strategy = context.CommandStrategy;
|
||||
var executeMethod = strategy.GetType().GetMethod(nameof(ICommandStrategy.ExecuteAsync))!;
|
||||
var executeMethodWithReturnType = executeMethod.MakeGenericMethod(resultType);
|
||||
|
||||
// Execute command.
|
||||
var task = executeMethodWithReturnType.Invoke(strategy, [strategyContext]);
|
||||
|
||||
// Get the result of the task.
|
||||
var taskWithReturnType = typeof(Task<>).MakeGenericType(resultType);
|
||||
var resultProperty = taskWithReturnType.GetProperty(nameof(Task<object>.Result))!;
|
||||
context.Result = resultProperty.GetValue(task);
|
||||
|
||||
// Invoke next middleware.
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Elsa.Mediator.Middleware.Command.Contracts;
|
||||
using Elsa.Mediator.Models;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Command.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A command middleware that logs the command being invoked.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class CommandLoggingMiddleware(CommandMiddlewareDelegate next, ILogger<CommandLoggingMiddleware> logger) : ICommandMiddleware
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InvokeAsync(CommandContext context)
|
||||
{
|
||||
var commandType = context.Command.GetType();
|
||||
logger.LogInformation("Invoking {CommandName}", commandType.Name);
|
||||
|
||||
await next(context);
|
||||
|
||||
if (context.Result is null or Unit)
|
||||
logger.LogInformation("{CommandName} completed with no result", commandType.Name);
|
||||
else
|
||||
logger.LogInformation("{CommandName} completed with result {CommandResult}", commandType.Name, context.Result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Elsa.Mediator.Middleware.Command.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command middleware.
|
||||
/// </summary>
|
||||
public interface ICommandMiddleware
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes the middleware.
|
||||
/// </summary>
|
||||
/// <param name="context">The command context.</param>
|
||||
ValueTask InvokeAsync(CommandContext context);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Elsa.Mediator.Middleware.Command.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pipeline for processing commands. The pipeline is responsible for orchestrating the execution of registered middleware in sequence.
|
||||
/// </summary>
|
||||
public interface ICommandPipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="setup">A delegate that configures the pipeline.</param>
|
||||
/// <returns>The pipeline.</returns>
|
||||
CommandMiddlewareDelegate Setup(Action<ICommandPipelineBuilder> setup);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pipeline.
|
||||
/// </summary>
|
||||
CommandMiddlewareDelegate Pipeline { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="context">The command context.</param>
|
||||
Task InvokeAsync(CommandContext context);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Elsa.Mediator.Middleware.Command.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command pipeline builder.
|
||||
/// </summary>
|
||||
public interface ICommandPipelineBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a property bag that can be used to share data between middleware components.
|
||||
/// </summary>
|
||||
public IDictionary<string, object?> Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service provider.
|
||||
/// </summary>
|
||||
IServiceProvider ApplicationServices { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Appends a middleware component to the pipeline.
|
||||
/// </summary>
|
||||
ICommandPipelineBuilder Use(Func<CommandMiddlewareDelegate, CommandMiddlewareDelegate> middleware);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a middleware component at the specified index.
|
||||
/// </summary>
|
||||
ICommandPipelineBuilder Use(int index, Func<CommandMiddlewareDelegate, CommandMiddlewareDelegate> middleware);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a middleware component from the pipeline.
|
||||
/// </summary>
|
||||
ICommandPipelineBuilder Remove(Func<CommandMiddlewareDelegate, CommandMiddlewareDelegate> middleware);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a middleware component at the specified index from the pipeline.
|
||||
/// </summary>
|
||||
ICommandPipelineBuilder RemoveAt(int index);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the pipeline.
|
||||
/// </summary>
|
||||
ICommandPipelineBuilder Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Builds the pipeline.
|
||||
/// </summary>
|
||||
/// <returns>The pipeline.</returns>
|
||||
public CommandMiddlewareDelegate Build();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Elsa.Mediator.Middleware.Command.Contracts;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Command;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="ICommandPipelineBuilder"/>.
|
||||
/// </summary>
|
||||
public static class MiddlewareExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds middleware to the pipeline.
|
||||
/// </summary>
|
||||
public static ICommandPipelineBuilder UseMiddleware<TMiddleware>(this ICommandPipelineBuilder builder, params object[] args) where TMiddleware : ICommandMiddleware
|
||||
{
|
||||
return builder.Use(next => BuildMiddlewareDelegate<TMiddleware>(builder, next, args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts middleware at a specific index in the pipeline.
|
||||
/// </summary>
|
||||
public static ICommandPipelineBuilder UseMiddleware<TMiddleware>(this ICommandPipelineBuilder builder, int index, params object[] args) where TMiddleware : ICommandMiddleware
|
||||
{
|
||||
return builder.Use(index, next => BuildMiddlewareDelegate<TMiddleware>(builder, next, args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a delegate for the middleware type.
|
||||
/// </summary>
|
||||
private static CommandMiddlewareDelegate BuildMiddlewareDelegate<TMiddleware>(
|
||||
ICommandPipelineBuilder builder,
|
||||
CommandMiddlewareDelegate next,
|
||||
object[] args
|
||||
) where TMiddleware : ICommandMiddleware
|
||||
{
|
||||
var middleware = typeof(TMiddleware);
|
||||
var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware);
|
||||
var ctorParams = new[]
|
||||
{
|
||||
next
|
||||
}.Concat(args).Select(x => x!).ToArray();
|
||||
var instance = ActivatorUtilities.CreateInstance(builder.ApplicationServices, middleware, ctorParams);
|
||||
return (CommandMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(CommandMiddlewareDelegate), instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Elsa.Mediator.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a set of static methods for working with middleware.
|
||||
/// </summary>
|
||||
public static class MiddlewareHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Invoke or InvokeAsync method from the middleware type.
|
||||
/// </summary>
|
||||
/// <param name="middleware">The middleware type.</param>
|
||||
/// <returns>The Invoke or InvokeAsync method.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the Invoke or InvokeAsync method cannot be found or the return type is not Task or ValueTask.</exception>
|
||||
public static MethodInfo GetInvokeMethod(Type middleware)
|
||||
{
|
||||
const string invokeMethodName = "Invoke";
|
||||
const string invokeAsyncMethodName = "InvokeAsync";
|
||||
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
|
||||
var invokeMethods = methods.Where(m => string.Equals(m.Name, invokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, invokeAsyncMethodName, StringComparison.Ordinal)).ToArray();
|
||||
|
||||
switch (invokeMethods.Length)
|
||||
{
|
||||
case > 1:
|
||||
throw new InvalidOperationException("Multiple Invoke methods were found. Use either Invoke or InvokeAsync.");
|
||||
case 0:
|
||||
throw new InvalidOperationException("No Invoke methods were found. Use either Invoke or InvokeAsync");
|
||||
}
|
||||
|
||||
var methodInfo = invokeMethods[0];
|
||||
|
||||
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType) && !typeof(ValueTask).IsAssignableFrom(methodInfo.ReturnType))
|
||||
throw new InvalidOperationException($"The {methodInfo.Name} method must return Task or ValueTask");
|
||||
|
||||
return methodInfo;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using Elsa.Mediator.Contexts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Mediator.Middleware.Notification.Contracts;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Mediator.Middleware.Notification.Components;
|
||||
|
||||
/// <inheritdoc />
|
||||
[UsedImplicitly]
|
||||
public class NotificationHandlerInvokerMiddleware(
|
||||
NotificationMiddlewareDelegate next,
|
||||
ILogger<NotificationHandlerInvokerMiddleware> logger)
|
||||
: INotificationMiddleware
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InvokeAsync(NotificationContext context)
|
||||
{
|
||||
// Find all handlers for the specified notification.
|
||||
var notification = context.Notification;
|
||||
var notificationType = notification.GetType();
|
||||
var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType);
|
||||
var serviceProvider = context.ServiceProvider;
|
||||
var notificationHandlers = serviceProvider.GetServices<INotificationHandler>();
|
||||
var handlers = notificationHandlers.Where(x => handlerType.IsInstanceOfType(x)).DistinctBy(x => x.GetType()).ToArray();
|
||||
var strategyContext = new NotificationStrategyContext(context, handlers, logger, serviceProvider, context.CancellationToken);
|
||||
|
||||
await context.NotificationStrategy.PublishAsync(strategyContext);
|
||||
|
||||
// Invoke next middleware.
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user