From a4011278ccba6c60091a67fdca1bd4dd2021cb99 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 9 Dec 2022 12:25:39 +0100 Subject: [PATCH] Implement implicit FastEndpoint assembly collection for discovery --- .../Elsa.WorkflowServer.Web/Program.cs | 15 ++---- .../DependencyInjectionExtensions.cs | 4 +- .../Extensions/ModuleExtensions.cs | 53 +++++++++++++++++++ .../Extensions/ObjectDictionaryExtensions.cs | 14 +++++ .../Extensions/WebApplicationExtensions.cs | 12 ++--- .../Elsa.Features/Implementations/Module.cs | 12 +++++ src/common/Elsa.Features/Services/IModule.cs | 28 ++++++++++ .../Features/ActivityDefinitionsFeature.cs | 7 +++ .../Elsa.Identity/Features/IdentityFeature.cs | 25 +++++++++ .../Elsa.Labels/Features/LabelsFeature.cs | 20 ++++++- .../Features/WorkflowsApiFeature.cs | 12 +++-- .../Features/WorkflowManagementFeature.cs | 2 + 12 files changed, 180 insertions(+), 24 deletions(-) create mode 100644 src/common/Elsa.Api.Common/Extensions/ModuleExtensions.cs create mode 100644 src/common/Elsa.Api.Common/Extensions/ObjectDictionaryExtensions.cs diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index 0514b36bc..acb7f81dc 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -1,8 +1,8 @@ using Elsa.Extensions; -using Elsa.Features.Extensions; using Elsa.Http; using Elsa.Http.Extensions; -using Elsa.Identity.Features; +using Elsa.Identity; +using Elsa.Identity.Extensions; using Elsa.Identity.Options; using Elsa.JavaScript.Activities; using Elsa.JavaScript.Extensions; @@ -27,20 +27,16 @@ using Elsa.Scheduling.Extensions; using Elsa.WorkflowContexts.Extensions; using Elsa.Workflows.Api.Extensions; using Elsa.Workflows.Core.Activities; -using Elsa.Workflows.Core.Activities.Flowchart.Activities; using Elsa.Workflows.Core.Middleware.Workflows; using Elsa.Workflows.Management.Extensions; using Elsa.Workflows.Management.Services; using Elsa.Workflows.Runtime.Extensions; using Elsa.Workflows.Runtime.Implementations; -using Elsa.WorkflowServer.Web.Activities; using Elsa.WorkflowServer.Web.Jobs; -using FastEndpoints; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.Data.Sqlite; using Proto.Persistence.Sqlite; -using Event = Elsa.Workflows.Core.Activities.Event; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; @@ -61,7 +57,7 @@ services .AddActivitiesFrom() .AddActivitiesFrom() ) - .Use(identity => + .UseIdentity(identity => { identity.CreateDefaultUser = true; identity.IdentityOptions = options => identitySection.Bind(options); @@ -81,9 +77,8 @@ services .UseJavaScript() .UseLiquid() .UseHttp() - ); + ).AddFastEndpointsFromModule(); -services.AddFastEndpoints(); services.AddHealthChecks(); services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin())); @@ -94,7 +89,7 @@ services services.AddHttpContextAccessor(); services.AddSingleton(); -services.AddAuthorization(options => options.AddPolicy("SecurityRoot", policy => policy.AddRequirements(new LocalHostRequirement()))); +services.AddAuthorization(options => options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.AddRequirements(new LocalHostRequirement()))); // Configure middleware pipeline. var app = builder.Build(); diff --git a/src/bundles/Elsa/Extensions/DependencyInjectionExtensions.cs b/src/bundles/Elsa/Extensions/DependencyInjectionExtensions.cs index 487cf94c7..22fe42889 100644 --- a/src/bundles/Elsa/Extensions/DependencyInjectionExtensions.cs +++ b/src/bundles/Elsa/Extensions/DependencyInjectionExtensions.cs @@ -13,12 +13,12 @@ public static class DependencyInjectionExtensions /// /// Creates a new Elsa module and adds the to it. /// - public static IServiceCollection AddElsa(this IServiceCollection services, Action? configure = default) + public static IModule AddElsa(this IServiceCollection services, Action? configure = default) { var module = services.CreateModule(); module.Configure(); configure?.Invoke(module); module.Apply(); - return services; + return module; } } \ No newline at end of file diff --git a/src/common/Elsa.Api.Common/Extensions/ModuleExtensions.cs b/src/common/Elsa.Api.Common/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..c3ed4861f --- /dev/null +++ b/src/common/Elsa.Api.Common/Extensions/ModuleExtensions.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using Elsa.Features.Services; +using FastEndpoints; + +namespace Elsa.Extensions; + +/// +/// Provides extensions to . +/// +public static class ModuleExtensions +{ + private static readonly object FastEndpointsAssembliesKey = new(); + + /// + /// Registers the specified assembly for FastEndpoint assembly discovery. + /// + public static void AddFastEndpointsAssembly(this IModule module, Assembly assembly) + { + var assemblies = module.Properties.GetOrAdd(FastEndpointsAssembliesKey, () => new HashSet()); + assemblies.Add(assembly); + } + + /// + /// Registers the assembly for FastEndpoint assembly discovery using the specified marker type. + /// + public static void AddFastEndpointsAssembly(this IModule module) => module.AddFastEndpointsAssembly(typeof(T)); + + /// + /// Registers the assembly for FastEndpoint assembly discovery using the specified marker type. + /// + public static void AddFastEndpointsAssembly(this IModule module, Type markerType) => module.AddFastEndpointsAssembly(markerType.Assembly); + + /// + /// Returns all collected assemblies for discovery of endpoints. + /// + public static IEnumerable GetFastEndpointsAssembliesFromModule(this IModule module) => module.Properties.GetOrAdd(FastEndpointsAssembliesKey, () => new HashSet()); + + /// + /// Adds FastEndpoints to the service container and registers all collected assemblies for discovery of endpoints. + /// + public static IModule AddFastEndpointsFromModule(this IModule module) + { + var assemblies = module.GetFastEndpointsAssembliesFromModule().ToList(); + + module.Services.AddFastEndpoints(options => + { + options.DisableAutoDiscovery = true; + options.Assemblies = assemblies; + }); + + return module; + } +} \ No newline at end of file diff --git a/src/common/Elsa.Api.Common/Extensions/ObjectDictionaryExtensions.cs b/src/common/Elsa.Api.Common/Extensions/ObjectDictionaryExtensions.cs new file mode 100644 index 000000000..9afec4193 --- /dev/null +++ b/src/common/Elsa.Api.Common/Extensions/ObjectDictionaryExtensions.cs @@ -0,0 +1,14 @@ +namespace Elsa.Extensions; + +static class ObjectDictionaryExtensions +{ + public static T GetOrAdd(this IDictionary dictionary, object key, Func valueFactory) + { + if (dictionary.TryGetValue(key, out var value)) return (T)value; + value = valueFactory(); + dictionary.Add(key, value!); + return (T)value!; + } + + public static T Get(this IDictionary dictionary, object key) => (T)dictionary[key]; +} \ No newline at end of file diff --git a/src/common/Elsa.Api.Common/Extensions/WebApplicationExtensions.cs b/src/common/Elsa.Api.Common/Extensions/WebApplicationExtensions.cs index 27c21ec0a..59a82b6e0 100644 --- a/src/common/Elsa.Api.Common/Extensions/WebApplicationExtensions.cs +++ b/src/common/Elsa.Api.Common/Extensions/WebApplicationExtensions.cs @@ -19,17 +19,15 @@ public static class WebApplicationExtensions /// /// The route prefix to apply to Elsa API endpoints. /// E.g. "elsa/api" will expose endpoints like this: "/elsa/api/workflow-definitions" - public static IApplicationBuilder UseElsaFastEndpoints(this IApplicationBuilder app, string routePrefix = "elsa/api") - { - return app.UseFastEndpoints(config => + public static IApplicationBuilder UseElsaFastEndpoints(this IApplicationBuilder app, string routePrefix = "elsa/api") => + app.UseFastEndpoints(config => { config.Endpoints.RoutePrefix = routePrefix; config.Serializer.RequestDeserializer = DeserializeRequestAsync; config.Serializer.ResponseSerializer = SerializeRequestAsync; - }); - } + }); - static ValueTask DeserializeRequestAsync(HttpRequest httpRequest, Type modelType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken) + private static ValueTask DeserializeRequestAsync(HttpRequest httpRequest, Type modelType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken) { var serializerOptionsProvider = httpRequest.HttpContext.RequestServices.GetRequiredService(); var options = serializerOptionsProvider.CreateApiOptions(); @@ -39,7 +37,7 @@ public static class WebApplicationExtensions : JsonSerializer.DeserializeAsync(httpRequest.Body, modelType, serializerContext, cancellationToken); } - static Task SerializeRequestAsync(HttpResponse httpResponse, object? dto, string contentType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken) + private static Task SerializeRequestAsync(HttpResponse httpResponse, object? dto, string contentType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken) { var serializerOptionsProvider = httpResponse.HttpContext.RequestServices.GetRequiredService(); var options = serializerOptionsProvider.CreateApiOptions(); diff --git a/src/common/Elsa.Features/Implementations/Module.cs b/src/common/Elsa.Features/Implementations/Module.cs index fc156b236..7e4bce663 100644 --- a/src/common/Elsa.Features/Implementations/Module.cs +++ b/src/common/Elsa.Features/Implementations/Module.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Hosting; namespace Elsa.Features.Implementations; +/// public class Module : IModule { private record HostedServiceDescriptor(int Order, Type HostedServiceType); @@ -15,15 +16,24 @@ public class Module : IModule private readonly ISet _features = new HashSet(); private readonly ICollection _hostedServiceDescriptors = new List(); + /// + /// Constructor. + /// public Module(IServiceCollection services) { Services = services; } + /// public IServiceCollection Services { get; } + + /// + public IDictionary Properties { get; } = new Dictionary(); + /// public T Configure(Action? configure = default) where T : class, IFeature => Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure); + /// public T Configure(Func factory, Action? configure = default) where T : class, IFeature { if (_features.FirstOrDefault(x => x is T) is not T feature) @@ -36,12 +46,14 @@ public class Module : IModule return feature; } + /// public IModule ConfigureHostedService(int priority = 0) { _hostedServiceDescriptors.Add(new HostedServiceDescriptor(priority, typeof(T))); return this; } + /// public void Apply() { var featureTypes = _features.Select(x => x.GetType()).TSort(x => x.GetCustomAttributes().Select(dependsOn => dependsOn.Type)).ToList(); diff --git a/src/common/Elsa.Features/Services/IModule.cs b/src/common/Elsa.Features/Services/IModule.cs index 79b283bee..0bee045f7 100644 --- a/src/common/Elsa.Features/Services/IModule.cs +++ b/src/common/Elsa.Features/Services/IModule.cs @@ -1,12 +1,40 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Elsa.Features.Services; +/// +/// A thin abstraction on top of to help organize features and dependencies. +/// public interface IModule { + /// + /// The service collection being populated. + /// IServiceCollection Services { get; } + + /// + /// A dictionary into which features can stash away values for later use. + /// + IDictionary Properties { get; } + + /// + /// Creates and configures a feature of the specified type. + /// T Configure(Action? configure = default) where T : class, IFeature; + + /// + /// Creates and configures a feature of the specified type. + /// T Configure(Func factory, Action? configure = default) where T : class, IFeature; + + /// + /// Configures a using an optional priority to control in which order it will be registered with the service container. + /// IModule ConfigureHostedService(int priority = 0); + + /// + /// Will apply all configured features, causing the collection to be populated. + /// void Apply(); } \ No newline at end of file diff --git a/src/modules/Elsa.ActivityDefinitions/Features/ActivityDefinitionsFeature.cs b/src/modules/Elsa.ActivityDefinitions/Features/ActivityDefinitionsFeature.cs index 0c297490a..958c750a7 100644 --- a/src/modules/Elsa.ActivityDefinitions/Features/ActivityDefinitionsFeature.cs +++ b/src/modules/Elsa.ActivityDefinitions/Features/ActivityDefinitionsFeature.cs @@ -3,6 +3,7 @@ using Elsa.ActivityDefinitions.Implementations; using Elsa.ActivityDefinitions.Services; using Elsa.Common.Extensions; using Elsa.Common.Features; +using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; @@ -31,6 +32,12 @@ public class ActivityDefinitionsFeature : FeatureBase /// public Func ActivityDefinitionStore { get; set; } = sp => sp.GetRequiredService(); + /// + public override void Configure() + { + Module.AddFastEndpointsAssembly(GetType()); + } + /// public override void Apply() { diff --git a/src/modules/Elsa.Identity/Features/IdentityFeature.cs b/src/modules/Elsa.Identity/Features/IdentityFeature.cs index cdd2a3851..c0635ab68 100644 --- a/src/modules/Elsa.Identity/Features/IdentityFeature.cs +++ b/src/modules/Elsa.Identity/Features/IdentityFeature.cs @@ -1,5 +1,7 @@ +using System.Reflection; using Elsa.Common.Extensions; using Elsa.Common.Features; +using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; @@ -18,22 +20,45 @@ namespace Elsa.Identity.Features; [DependsOn(typeof(SystemClockFeature))] public class IdentityFeature : FeatureBase { + /// public IdentityFeature(IModule module) : base(module) { } + /// + /// A flag indicating whether a default user should be created. + /// public bool CreateDefaultUser { get; set; } + + + /// + /// A delegate to configure . + /// public Action? IdentityOptions { get; set; } + /// + /// A delegate that creates an instance of an implementation of . + /// public Func UserStore { get; set; } = sp => sp.GetRequiredService(); + + /// + /// A delegate that creates an instance of an implementation of . + /// public Func RoleStore { get; set; } = sp => sp.GetRequiredService(); + public override void Configure() + { + Module.AddFastEndpointsAssembly(GetType()); + } + + /// public override void ConfigureHostedServices() { if(CreateDefaultUser) Module.ConfigureHostedService(); } + /// public override void Apply() { Services.Configure(IdentityOptions); diff --git a/src/modules/Elsa.Labels/Features/LabelsFeature.cs b/src/modules/Elsa.Labels/Features/LabelsFeature.cs index d64f783aa..591241325 100644 --- a/src/modules/Elsa.Labels/Features/LabelsFeature.cs +++ b/src/modules/Elsa.Labels/Features/LabelsFeature.cs @@ -1,4 +1,5 @@ using Elsa.Common.Extensions; +using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; @@ -11,16 +12,33 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Labels.Features; +/// +/// Enables functionality to tag workflows with labels. +/// [DependsOn(typeof(MediatorFeature))] public class LabelsFeature : FeatureBase { + /// public LabelsFeature(IModule module) : base(module) { } + /// + /// A delegate that provides an instance of an implementation of . + /// public Func LabelStore { get; set; } = sp => sp.GetRequiredService(); - public Func WorkflowDefinitionLabelStore { get; set; } = sp => sp.GetRequiredService(); + /// + /// A delegate that provides an instance of an implementation of . + /// + public Func WorkflowDefinitionLabelStore { get; set; } = sp => sp.GetRequiredService(); + + public override void Configure() + { + Module.AddFastEndpointsAssembly(GetType()); + } + + /// public override void Apply() { Services diff --git a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs index 244822050..9e0e432cc 100644 --- a/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs +++ b/src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs @@ -1,18 +1,22 @@ +using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; -using Elsa.Workflows.Management.Mappers; -using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Api.Features; +/// +/// Adds workflows API features. +/// public class WorkflowsApiFeature : FeatureBase { + /// public WorkflowsApiFeature(IModule module) : base(module) { } - public override void Apply() + /// + public override void Configure() { - Services.AddSingleton(); + Module.AddFastEndpointsAssembly(GetType()); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs index 2098e8800..2ee255ce4 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs @@ -13,6 +13,7 @@ using Elsa.Workflows.Core.Services; using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Management.Extensions; using Elsa.Workflows.Management.Implementations; +using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Management.Materializers; using Elsa.Workflows.Management.Models; using Elsa.Workflows.Management.Options; @@ -151,6 +152,7 @@ public class WorkflowManagementFeature : FeatureBase .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() ; Services.Configure(options =>