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
+
+
+
+
+
+
+
+
+