diff --git a/Elsa.sln b/Elsa.sln index ad457fe38..669b46998 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -237,6 +237,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.ChildWo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.DocumentApproval", "src\samples\aspnet\Elsa.Samples.AspNet.DocumentApproval\Elsa.Samples.AspNet.DocumentApproval.csproj", "{CA9391E3-FA3F-4F96-B6DF-25309BFE7D05}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DropIns.Core", "src\common\Elsa.DropIns.Core\Elsa.DropIns.Core.csproj", "{1B794B9C-9B8A-4D5A-9848-B51274BD1FC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DropIns", "src\common\Elsa.DropIns\Elsa.DropIns.csproj", "{4E7F15D1-729E-4DA7-992A-C3A2C6054B37}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "drop-ins", "drop-ins", "{6AB6F2F7-CAC1-45C8-9D87-EEE56BC68601}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleDropIn", "src\samples\drop-ins\SampleDropIn\SampleDropIn.csproj", "{E0567846-AC0E-4E09-96A1-EFED4B570A01}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -591,6 +599,18 @@ Global {CA9391E3-FA3F-4F96-B6DF-25309BFE7D05}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA9391E3-FA3F-4F96-B6DF-25309BFE7D05}.Release|Any CPU.ActiveCfg = Release|Any CPU {CA9391E3-FA3F-4F96-B6DF-25309BFE7D05}.Release|Any CPU.Build.0 = Release|Any CPU + {1B794B9C-9B8A-4D5A-9848-B51274BD1FC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B794B9C-9B8A-4D5A-9848-B51274BD1FC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B794B9C-9B8A-4D5A-9848-B51274BD1FC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B794B9C-9B8A-4D5A-9848-B51274BD1FC5}.Release|Any CPU.Build.0 = Release|Any CPU + {4E7F15D1-729E-4DA7-992A-C3A2C6054B37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E7F15D1-729E-4DA7-992A-C3A2C6054B37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E7F15D1-729E-4DA7-992A-C3A2C6054B37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E7F15D1-729E-4DA7-992A-C3A2C6054B37}.Release|Any CPU.Build.0 = Release|Any CPU + {E0567846-AC0E-4E09-96A1-EFED4B570A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0567846-AC0E-4E09-96A1-EFED4B570A01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0567846-AC0E-4E09-96A1-EFED4B570A01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0567846-AC0E-4E09-96A1-EFED4B570A01}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} @@ -697,5 +717,9 @@ Global {B99B23D4-6D36-4A31-BE92-DE2415514C79} = {08B41FFA-CEE3-46A7-B5C0-3EB65D37A16C} {2EA47068-6079-4276-8776-F67DE99B568D} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} {CA9391E3-FA3F-4F96-B6DF-25309BFE7D05} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {1B794B9C-9B8A-4D5A-9848-B51274BD1FC5} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {4E7F15D1-729E-4DA7-992A-C3A2C6054B37} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} + {6AB6F2F7-CAC1-45C8-9D87-EEE56BC68601} = {155227F0-A33B-40AA-A4B4-06F813EB921B} + {E0567846-AC0E-4E09-96A1-EFED4B570A01} = {6AB6F2F7-CAC1-45C8-9D87-EEE56BC68601} EndGlobalSection EndGlobal diff --git a/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj b/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj index 39c60741a..3b7f9221c 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj +++ b/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj @@ -9,35 +9,41 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index 5d20b3b54..80f5f04d2 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -1,6 +1,8 @@ using System.Text.Encodings.Web; using Elsa.Dapper.Extensions; using Elsa.Dapper.Services; +using Elsa.DropIns; +using Elsa.DropIns.Extensions; using Elsa.EntityFrameworkCore.Extensions; using Elsa.EntityFrameworkCore.Modules.Identity; using Elsa.EntityFrameworkCore.Modules.Management; @@ -35,25 +37,25 @@ var mongoDbConnectionString = configuration.GetConnectionString("MongoDb")!; services .AddElsa(elsa => { - if(useMongoDb) + if (useMongoDb) elsa.UseMongoDb(mongoDbConnectionString); - - if(useDapper) + + if (useDapper) elsa.UseDapper(dapper => { dapper.UseMigrations(); dapper.DbConnectionProvider = sp => { - if(useSqlServer) + if (useSqlServer) return new SqlServerDbConnectionProvider(sqlServerConnectionString!); else return new SqliteDbConnectionProvider(sqliteConnectionString); }; }); - + if (useHangfire) elsa.UseHangfire(); - + elsa .AddActivitiesFrom() .AddWorkflowsFrom() @@ -61,19 +63,19 @@ services .AddTypeAlias>("ApiResponse[User]") .UseIdentity(identity => { - if(useMongoDb) + if (useMongoDb) identity.UseMongoDb(); else if (useDapper) identity.UseDapper(); else identity.UseEntityFrameworkCore(ef => { - if(useSqlServer) + if (useSqlServer) ef.UseSqlServer(sqlServerConnectionString!); else ef.UseSqlite(sqliteConnectionString); }); - + identity.IdentityOptions = options => identitySection.Bind(options); identity.TokenOptions = options => identityTokenSection.Bind(options); identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options)); @@ -83,49 +85,49 @@ services .UseDefaultAuthentication() .UseWorkflowManagement(management => { - if(useMongoDb) + if (useMongoDb) management.UseMongoDb(); else if (useDapper) management.UseDapper(); else management.UseEntityFrameworkCore(ef => { - if(useSqlServer) + if (useSqlServer) ef.UseSqlServer(sqlServerConnectionString!); else ef.UseSqlite(sqliteConnectionString); }); - + management.AddVariableType>("Api"); management.AddVariableType("Api"); management.AddVariableType("Api"); }) .UseWorkflowRuntime(runtime => { - if(useMongoDb) + if (useMongoDb) runtime.UseMongoDb(); else if (useDapper) runtime.UseDapper(); else runtime.UseEntityFrameworkCore(ef => { - if(useSqlServer) + if (useSqlServer) ef.UseSqlServer(sqlServerConnectionString!); else ef.UseSqlite(sqliteConnectionString); }); - - if(useProtoActor) + + if (useProtoActor) { runtime.UseProtoActor(proto => proto.PersistenceProvider = _ => { - if(useSqlServer) + if (useSqlServer) return new SqlServerProvider(sqlServerConnectionString!, true, "", "proto_actor"); else return new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString)); }); } - + runtime.UseMassTransitDispatcher(); runtime.WorkflowInboxCleanupOptions = options => configuration.GetSection("Runtime:WorkflowInboxCleanup").Bind(options); }) @@ -141,15 +143,19 @@ services .UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default) .UseHttp(http => http.HttpEndpointAuthorizationHandler = sp => sp.GetRequiredService()) .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)); + + // Initialize drop-ins. + elsa.InstallDropIns(options => options.DropInRootDirectory = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "DropIns")); }); services.AddHealthChecks(); services.AddControllers(); services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*"))); -// Configure middleware pipeline. +// Build the web application. var app = builder.Build(); +// Configure the pipeline. if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage(); diff --git a/src/common/Elsa.DropIns.Core/Elsa.DropIns.Core.csproj b/src/common/Elsa.DropIns.Core/Elsa.DropIns.Core.csproj new file mode 100644 index 000000000..8c19d7d92 --- /dev/null +++ b/src/common/Elsa.DropIns.Core/Elsa.DropIns.Core.csproj @@ -0,0 +1,22 @@ + + + + + + + net6.0;net7.0 + + Reference this package when developing drop-ins for Elsa Workflows. + + elsa drop-ins common + + + + + + + + + + + diff --git a/src/common/Elsa.DropIns.Core/FodyWeavers.xml b/src/common/Elsa.DropIns.Core/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/common/Elsa.DropIns.Core/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/common/Elsa.DropIns.Core/IDropIn.cs b/src/common/Elsa.DropIns.Core/IDropIn.cs new file mode 100644 index 000000000..a5fd12371 --- /dev/null +++ b/src/common/Elsa.DropIns.Core/IDropIn.cs @@ -0,0 +1,20 @@ +using Elsa.Features.Services; + +namespace Elsa.DropIns.Core; + +/// +/// Implement this in your drop-in module to configure elsa and register services. +/// +public interface IDropIn +{ + /// + /// Called when the drop-in is installed. + /// + /// The Elsa module. + void Install(IModule module); + + /// + /// Called when the drop-in is being configured. + /// + ValueTask ConfigureAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Catalogs/AssembliesDropInCatalog.cs b/src/common/Elsa.DropIns/Catalogs/AssembliesDropInCatalog.cs new file mode 100644 index 000000000..6372ddd33 --- /dev/null +++ b/src/common/Elsa.DropIns/Catalogs/AssembliesDropInCatalog.cs @@ -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 _assemblies; + + public AssembliesDropInCatalog(IEnumerable assemblies) + { + _assemblies = assemblies; + } + + public IEnumerable List() + { + var allDropIns = new List(); + + foreach (var assembly in _assemblies) + { + var assemblyCatalog = new AssemblyDropInCatalog(assembly); + var dropIns = assemblyCatalog.List(); + allDropIns.AddRange(dropIns); + } + + return allDropIns; + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Catalogs/AssemblyDropInCatalog.cs b/src/common/Elsa.DropIns/Catalogs/AssemblyDropInCatalog.cs new file mode 100644 index 000000000..791757a3b --- /dev/null +++ b/src/common/Elsa.DropIns/Catalogs/AssemblyDropInCatalog.cs @@ -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 List() + { + var dropIns = TypeFinder.FindImplementationsOf(_assembly) + .Select(type => new DropInDescriptor(type)) + .ToList(); + + return dropIns; + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Catalogs/DirectoryDropInCatalog.cs b/src/common/Elsa.DropIns/Catalogs/DirectoryDropInCatalog.cs new file mode 100644 index 000000000..3c43ccff8 --- /dev/null +++ b/src/common/Elsa.DropIns/Catalogs/DirectoryDropInCatalog.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using System.Runtime.Loader; +using Elsa.DropIns.Contexts; +using Elsa.DropIns.Contracts; +using Elsa.DropIns.Helpers; +using Elsa.DropIns.Models; + +namespace Elsa.DropIns.Catalogs; + +public class DirectoryDropInCatalog : IDropInCatalog +{ + private readonly string _directoryPath; + + public DirectoryDropInCatalog(string directoryPath) + { + _directoryPath = directoryPath; + } + + public IEnumerable List() + { + var assemblies = ListAssemblies(); + var packages = ListPackages(); + var assembliesCatalog = new AssembliesDropInCatalog(assemblies); + var packagesCatalog = new NuGetPackagesCatalog(packages); + + return assembliesCatalog.List().Concat(packagesCatalog.List()); + } + + private IEnumerable 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 IEnumerable ListPackages() + { + return Directory.GetFiles(_directoryPath, "*.nupkg", SearchOption.AllDirectories); + } + + private Assembly? LoadDropInAssembly(string path) + { + return !File.Exists(path) ? null : AssemblyLoader.LoadPath(path); + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Catalogs/NuGetPackageCatalog.cs b/src/common/Elsa.DropIns/Catalogs/NuGetPackageCatalog.cs new file mode 100644 index 000000000..9f8dbcbb1 --- /dev/null +++ b/src/common/Elsa.DropIns/Catalogs/NuGetPackageCatalog.cs @@ -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 List() + { + var assemblyLoadContext = new NuGetPackageAssemblyLoadContext(_packagePath); + var assemblies = assemblyLoadContext.Assemblies; + var assembliesCatalog = new AssembliesDropInCatalog(assemblies); + + return assembliesCatalog.List(); + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Catalogs/NuGetPackagesCatalog.cs b/src/common/Elsa.DropIns/Catalogs/NuGetPackagesCatalog.cs new file mode 100644 index 000000000..a879325ee --- /dev/null +++ b/src/common/Elsa.DropIns/Catalogs/NuGetPackagesCatalog.cs @@ -0,0 +1,19 @@ +using Elsa.DropIns.Contracts; +using Elsa.DropIns.Models; + +namespace Elsa.DropIns.Catalogs; + +public class NuGetPackagesCatalog : IDropInCatalog +{ + private readonly IEnumerable _packagePaths; + + public NuGetPackagesCatalog(IEnumerable packagePaths) + { + _packagePaths = packagePaths; + } + + public IEnumerable List() + { + return _packagePaths.Select(packagePath => new NuGetPackageCatalog(packagePath)).SelectMany(catalog => catalog.List()); + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Contexts/DirectoryAssemblyLoadContext.cs b/src/common/Elsa.DropIns/Contexts/DirectoryAssemblyLoadContext.cs new file mode 100644 index 000000000..45bd6bd99 --- /dev/null +++ b/src/common/Elsa.DropIns/Contexts/DirectoryAssemblyLoadContext.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Elsa.DropIns.Contexts; + +internal 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; + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Contexts/NuGetPackageAssemblyLoadContext.cs b/src/common/Elsa.DropIns/Contexts/NuGetPackageAssemblyLoadContext.cs new file mode 100644 index 000000000..f0fd3e59b --- /dev/null +++ b/src/common/Elsa.DropIns/Contexts/NuGetPackageAssemblyLoadContext.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.Loader; +using NuGet.Packaging; + +namespace Elsa.DropIns.Contexts; + +internal class NuGetPackageAssemblyLoadContext : AssemblyLoadContext +{ + private readonly Dictionary _loadedAssemblies = new Dictionary(); + + public NuGetPackageAssemblyLoadContext(string nugetPackagePath) + { + var packageReader = new PackageArchiveReader(nugetPackagePath); + + foreach (var dllFile in packageReader.GetFiles().Where(fileName => fileName.EndsWith(".dll"))) + { + 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; + } + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + return _loadedAssemblies.TryGetValue(assemblyName.FullName, out var assembly) ? assembly : null; + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Contracts/IDropInCatalog.cs b/src/common/Elsa.DropIns/Contracts/IDropInCatalog.cs new file mode 100644 index 000000000..12f88f533 --- /dev/null +++ b/src/common/Elsa.DropIns/Contracts/IDropInCatalog.cs @@ -0,0 +1,8 @@ +using Elsa.DropIns.Models; + +namespace Elsa.DropIns.Contracts; + +public interface IDropInCatalog +{ + IEnumerable List(); +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Contracts/IDropInInstaller.cs b/src/common/Elsa.DropIns/Contracts/IDropInInstaller.cs new file mode 100644 index 000000000..a1d7df12d --- /dev/null +++ b/src/common/Elsa.DropIns/Contracts/IDropInInstaller.cs @@ -0,0 +1,8 @@ +using Elsa.Features.Services; + +namespace Elsa.DropIns.Contracts; + +public interface IDropInInstaller +{ + void Install(IModule module); +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Elsa.DropIns.csproj b/src/common/Elsa.DropIns/Elsa.DropIns.csproj new file mode 100644 index 000000000..ebdcae062 --- /dev/null +++ b/src/common/Elsa.DropIns/Elsa.DropIns.csproj @@ -0,0 +1,26 @@ + + + + + + + net6.0;net7.0 + + Provides drop-in features for Elsa workflows. + + elsa drop-ins common + + + + + + + + + + + + + + + diff --git a/src/common/Elsa.DropIns/Extensions/ModuleExtensions.cs b/src/common/Elsa.DropIns/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..9ef671b84 --- /dev/null +++ b/src/common/Elsa.DropIns/Extensions/ModuleExtensions.cs @@ -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? configureOptions = default) + { + // Install drop-ins. + var serviceProvider = new ServiceCollection().AddDropInInstaller(configureOptions).BuildServiceProvider(); + serviceProvider.GetRequiredService().Install(elsa); + + // Register drop-in monitor. + elsa.Services.AddDropInMonitor(configureOptions); + + return elsa; + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Extensions/ServiceCollectionExtensions.cs b/src/common/Elsa.DropIns/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..5ec1e8917 --- /dev/null +++ b/src/common/Elsa.DropIns/Extensions/ServiceCollectionExtensions.cs @@ -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? configureOptions = default) + { + services.AddDropInCore(configureOptions); + services.AddHostedService(); + + return services; + } + + public static IServiceCollection AddDropInInstaller(this IServiceCollection services, Action? configureOptions = default) + { + services.AddDropInCore(configureOptions); + services.AddSingleton(); + + return services; + } + + public static IServiceCollection AddDropInCore(this IServiceCollection services, Action? configureOptions = default) + { + services.Configure(configureOptions ?? (_ => { })); + + return services; + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/FodyWeavers.xml b/src/common/Elsa.DropIns/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/common/Elsa.DropIns/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Helpers/AssemblyLoader.cs b/src/common/Elsa.DropIns/Helpers/AssemblyLoader.cs new file mode 100644 index 000000000..d1dfca0db --- /dev/null +++ b/src/common/Elsa.DropIns/Helpers/AssemblyLoader.cs @@ -0,0 +1,17 @@ +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); + var assembly = loadContext.LoadFromAssemblyName(assemblyName); + + return assembly; + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Helpers/TypeFinder.cs b/src/common/Elsa.DropIns/Helpers/TypeFinder.cs new file mode 100644 index 000000000..b8b5655ac --- /dev/null +++ b/src/common/Elsa.DropIns/Helpers/TypeFinder.cs @@ -0,0 +1,18 @@ +using System.Reflection; + +namespace Elsa.DropIns.Helpers; + +public static class TypeFinder +{ + public static IEnumerable FindImplementationsOf(IEnumerable assemblies) + { + // Find all types that implement IDropInStartup: + return assemblies.SelectMany(FindImplementationsOf); + } + + public static IEnumerable FindImplementationsOf(Assembly assembly) + { + // Find all types that implement T: + return assembly.GetExportedTypes().Where(t => typeof(T).IsAssignableFrom(t) && t is { IsAbstract: false, IsInterface: false }); + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs b/src/common/Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs new file mode 100644 index 000000000..66797f7f3 --- /dev/null +++ b/src/common/Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs @@ -0,0 +1,65 @@ +using Elsa.DropIns.Catalogs; +using Elsa.DropIns.Core; +using Elsa.DropIns.Options; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using ThrottleDebounce; + +namespace Elsa.DropIns.HostedServices; + +/// +/// Monitors the drop-in directory for changes and loads any new drop-ins. +/// +public class DropInDirectoryMonitorHostedService : BackgroundService +{ + private readonly IOptions _options; + private readonly IServiceProvider _serviceProvider; + private readonly RateLimitedFunc _debouncedLoader; + + /// + public DropInDirectoryMonitorHostedService(IOptions options, IServiceProvider serviceProvider) + { + _options = options; + _serviceProvider = serviceProvider; + _debouncedLoader = Debouncer.Debounce(LoadDropInAssemblyAsync, TimeSpan.FromSeconds(2)); + } + + /// + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + var rootDirectoryPath = _options.Value.DropInRootDirectory; + + var watcher = new FileSystemWatcher(rootDirectoryPath) + { + EnableRaisingEvents = true, + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + }; + + watcher.Changed += OnChanged; + return Task.CompletedTask; + } + + private async void OnChanged(object sender, FileSystemEventArgs e) + { + var task = _debouncedLoader.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)!; + await dropIn.ConfigureAsync(_serviceProvider, CancellationToken.None); + } + } +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Models/DropInDescriptor.cs b/src/common/Elsa.DropIns/Models/DropInDescriptor.cs new file mode 100644 index 000000000..3d2a29996 --- /dev/null +++ b/src/common/Elsa.DropIns/Models/DropInDescriptor.cs @@ -0,0 +1,3 @@ +namespace Elsa.DropIns.Models; + +public record DropInDescriptor(Type Type); \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Options/DropInOptions.cs b/src/common/Elsa.DropIns/Options/DropInOptions.cs new file mode 100644 index 000000000..e2b3987a7 --- /dev/null +++ b/src/common/Elsa.DropIns/Options/DropInOptions.cs @@ -0,0 +1,6 @@ +namespace Elsa.DropIns.Options; + +public class DropInOptions +{ + public string DropInRootDirectory { get; set; } = default!; +} \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Services/DropInInstaller.cs b/src/common/Elsa.DropIns/Services/DropInInstaller.cs new file mode 100644 index 000000000..3706e7c41 --- /dev/null +++ b/src/common/Elsa.DropIns/Services/DropInInstaller.cs @@ -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; + +/// +public class DropInInstaller : IDropInInstaller +{ + private readonly IOptions _options; + + /// + /// Initializes a new instance of the class. + /// + public DropInInstaller(IOptions options) + { + _options = options; + } + + /// + 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); + } + } +} \ No newline at end of file diff --git a/src/samples/drop-ins/SampleDropIn/Activities/SampleActivity.cs b/src/samples/drop-ins/SampleDropIn/Activities/SampleActivity.cs new file mode 100644 index 000000000..608bd72c8 --- /dev/null +++ b/src/samples/drop-ins/SampleDropIn/Activities/SampleActivity.cs @@ -0,0 +1,11 @@ +using Elsa.Workflows.Core; + +namespace SampleDropIn.Activities; + +public class SampleActivity : CodeActivity +{ + protected override void Execute(ActivityExecutionContext context) + { + Console.WriteLine("Hello, world! From SampleDropIn!"); + } +} \ No newline at end of file diff --git a/src/samples/drop-ins/SampleDropIn/DropIn.cs b/src/samples/drop-ins/SampleDropIn/DropIn.cs new file mode 100644 index 000000000..1fef70a2d --- /dev/null +++ b/src/samples/drop-ins/SampleDropIn/DropIn.cs @@ -0,0 +1,24 @@ +using Elsa.DropIns.Core; +using Elsa.Extensions; +using Elsa.Features.Services; +using Elsa.Workflows.Core.Contracts; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using SampleDropIn.Activities; + +namespace SampleDropIn; + +[PublicAPI] +public class DropIn : IDropIn +{ + public void Install(IModule module) + { + module.AddActivitiesFrom(); + } + + public async ValueTask ConfigureAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var activityRegistry = serviceProvider.GetRequiredService(); + await activityRegistry.RegisterAsync(cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/src/samples/drop-ins/SampleDropIn/SampleDropIn.csproj b/src/samples/drop-ins/SampleDropIn/SampleDropIn.csproj new file mode 100644 index 000000000..332e875b8 --- /dev/null +++ b/src/samples/drop-ins/SampleDropIn/SampleDropIn.csproj @@ -0,0 +1,16 @@ + + + + net7.0 + enable + enable + true + + + + + + + + +