Implement implicit FastEndpoint assembly collection for discovery

This commit is contained in:
Sipke Schoorstra 2022-12-09 12:25:39 +01:00
parent 29a35e1cb1
commit a4011278cc
12 changed files with 180 additions and 24 deletions

View file

@ -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<RunJavaScript>()
.AddActivitiesFrom<Program>()
)
.Use<IdentityFeature>(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<IAuthorizationHandler, LocalHostRequirementHandler>();
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();

View file

@ -13,12 +13,12 @@ public static class DependencyInjectionExtensions
/// <summary>
/// Creates a new Elsa module and adds the <see cref="ElsaFeature"/> to it.
/// </summary>
public static IServiceCollection AddElsa(this IServiceCollection services, Action<IModule>? configure = default)
public static IModule AddElsa(this IServiceCollection services, Action<IModule>? configure = default)
{
var module = services.CreateModule();
module.Configure<ElsaFeature>();
configure?.Invoke(module);
module.Apply();
return services;
return module;
}
}

View file

@ -0,0 +1,53 @@
using System.Reflection;
using Elsa.Features.Services;
using FastEndpoints;
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 void AddFastEndpointsAssembly(this IModule module, Assembly assembly)
{
var assemblies = module.Properties.GetOrAdd(FastEndpointsAssembliesKey, () => new HashSet<Assembly>());
assemblies.Add(assembly);
}
/// <summary>
/// Registers the assembly for FastEndpoint assembly discovery using the specified marker type.
/// </summary>
public static void 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 void 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;
}
}

View file

@ -0,0 +1,14 @@
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];
}

View file

@ -19,17 +19,15 @@ public static class WebApplicationExtensions
/// <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 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<object?> DeserializeRequestAsync(HttpRequest httpRequest, Type modelType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken)
private static ValueTask<object?> DeserializeRequestAsync(HttpRequest httpRequest, Type modelType, JsonSerializerContext? serializerContext, CancellationToken cancellationToken)
{
var serializerOptionsProvider = httpRequest.HttpContext.RequestServices.GetRequiredService<SerializerOptionsProvider>();
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<SerializerOptionsProvider>();
var options = serializerOptionsProvider.CreateApiOptions();

View file

@ -8,6 +8,7 @@ using Microsoft.Extensions.Hosting;
namespace Elsa.Features.Implementations;
/// <inheritdoc />
public class Module : IModule
{
private record HostedServiceDescriptor(int Order, Type HostedServiceType);
@ -15,15 +16,24 @@ public class Module : IModule
private readonly ISet<IFeature> _features = new HashSet<IFeature>();
private readonly ICollection<HostedServiceDescriptor> _hostedServiceDescriptors = new List<HostedServiceDescriptor>();
/// <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 T Configure<T>(Action<T>? configure = default) where T : class, IFeature => Configure(module => (T)Activator.CreateInstance(typeof(T), module)!, configure);
/// <inheritdoc />
public T Configure<T>(Func<IModule, T> factory, Action<T>? 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;
}
/// <inheritdoc />
public IModule ConfigureHostedService<T>(int priority = 0)
{
_hostedServiceDescriptors.Add(new HostedServiceDescriptor(priority, typeof(T)));
return this;
}
/// <inheritdoc />
public void Apply()
{
var featureTypes = _features.Select(x => x.GetType()).TSort(x => x.GetCustomAttributes<DependsOn>().Select(dependsOn => dependsOn.Type)).ToList();

View file

@ -1,12 +1,40 @@
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>
/// Creates and configures a feature of the specified type.
/// </summary>
T Configure<T>(Action<T>? configure = default) 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 = default) 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);
/// <summary>
/// Will apply all configured features, causing the <see cref="Services"/> collection to be populated.
/// </summary>
void Apply();
}

View file

@ -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
/// </summary>
public Func<IServiceProvider, IActivityDefinitionStore> ActivityDefinitionStore { get; set; } = sp => sp.GetRequiredService<MemoryActivityDefinitionStore>();
/// <inheritdoc />
public override void Configure()
{
Module.AddFastEndpointsAssembly(GetType());
}
/// <inheritdoc />
public override void Apply()
{

View file

@ -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
{
/// <inheritdoc />
public IdentityFeature(IModule module) : base(module)
{
}
/// <summary>
/// A flag indicating whether a default user should be created.
/// </summary>
public bool CreateDefaultUser { get; set; }
/// <summary>
/// A delegate to configure <see cref="Options.IdentityOptions"/>.
/// </summary>
public Action<IdentityOptions>? IdentityOptions { get; set; }
/// <summary>
/// A delegate that creates an instance of an implementation of <see cref="IUserStore"/>.
/// </summary>
public Func<IServiceProvider, IUserStore> UserStore { get; set; } = sp => sp.GetRequiredService<MemoryUserStore>();
/// <summary>
/// A delegate that creates an instance of an implementation of <see cref="IRoleStore"/>.
/// </summary>
public Func<IServiceProvider, IRoleStore> RoleStore { get; set; } = sp => sp.GetRequiredService<MemoryRoleStore>();
public override void Configure()
{
Module.AddFastEndpointsAssembly(GetType());
}
/// <inheritdoc />
public override void ConfigureHostedServices()
{
if(CreateDefaultUser)
Module.ConfigureHostedService<SetupDefaultUserHostedService>();
}
/// <inheritdoc />
public override void Apply()
{
Services.Configure(IdentityOptions);

View file

@ -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;
/// <summary>
/// Enables functionality to tag workflows with labels.
/// </summary>
[DependsOn(typeof(MediatorFeature))]
public class LabelsFeature : FeatureBase
{
/// <inheritdoc />
public LabelsFeature(IModule module) : base(module)
{
}
/// <summary>
/// A delegate that provides an instance of an implementation of <see cref="ILabelStore"/>.
/// </summary>
public Func<IServiceProvider, ILabelStore> LabelStore { get; set; } = sp => sp.GetRequiredService<InMemoryLabelStore>();
public Func<IServiceProvider, IWorkflowDefinitionLabelStore> WorkflowDefinitionLabelStore { get; set; } = sp => sp.GetRequiredService<InMemoryWorkflowDefinitionLabelStore>();
/// <summary>
/// A delegate that provides an instance of an implementation of <see cref="IWorkflowDefinitionLabelStore"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowDefinitionLabelStore> WorkflowDefinitionLabelStore { get; set; } = sp => sp.GetRequiredService<InMemoryWorkflowDefinitionLabelStore>();
public override void Configure()
{
Module.AddFastEndpointsAssembly(GetType());
}
/// <inheritdoc />
public override void Apply()
{
Services

View file

@ -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;
/// <summary>
/// Adds workflows API features.
/// </summary>
public class WorkflowsApiFeature : FeatureBase
{
/// <inheritdoc />
public WorkflowsApiFeature(IModule module) : base(module)
{
}
public override void Apply()
/// <inheritdoc />
public override void Configure()
{
Services.AddSingleton<VariableDefinitionMapper>();
Module.AddFastEndpointsAssembly(GetType());
}
}

View file

@ -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<IWorkflowMaterializer, ClrWorkflowMaterializer>()
.AddSingleton<IWorkflowMaterializer, JsonWorkflowMaterializer>()
.AddSingleton<SerializerOptionsProvider>()
.AddSingleton<VariableDefinitionMapper>()
;
Services.Configure<ManagementOptions>(options =>