Add DropIn support (#4471)

* Implemented dynamic loading of activities at runtime

* Add support for loading NuGet packages
This commit is contained in:
Sipke Schoorstra 2023-09-23 19:41:37 +02:00 committed by GitHub
parent a5d13e4e9b
commit da34e23c2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 629 additions and 45 deletions

View file

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

View file

@ -9,35 +9,41 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Elsa.Dapper.Migrations\Elsa.Dapper.Migrations.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Dapper\Elsa.Dapper.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.SqlServer\Elsa.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Email\Elsa.Email.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Environments\Elsa.Environments.csproj" />
<ProjectReference Include="..\..\modules\Elsa.MassTransit\Elsa.MassTransit.csproj" />
<ProjectReference Include="..\..\modules\Elsa.MongoDb\Elsa.MongoDb.csproj" />
<ProjectReference Include="..\..\modules\Elsa.ProtoActor\Elsa.ProtoActor.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\modules\Elsa.ProtoActor.Cluster.AzureContainerApps\Elsa.ProtoActor.Cluster.AzureContainerApps.csproj" />
<ProjectReference Include="..\..\modules\Elsa.WorkflowProviders.BlobStorage\Elsa.WorkflowProviders.BlobStorage.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Hangfire\Elsa.Hangfire.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Labels\Elsa.Labels.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz\Elsa.Quartz.csproj" />
<ProjectReference Include="..\..\modules\Elsa.WorkflowContexts\Elsa.WorkflowContexts.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
<ProjectReference Include="..\..\common\Elsa.DropIns\Elsa.DropIns.csproj" />
<ProjectReference Include="..\Elsa\Elsa.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Dapper.Migrations\Elsa.Dapper.Migrations.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Dapper\Elsa.Dapper.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.SqlServer\Elsa.EntityFrameworkCore.SqlServer.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Email\Elsa.Email.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Environments\Elsa.Environments.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.MassTransit\Elsa.MassTransit.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.MongoDb\Elsa.MongoDb.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.ProtoActor\Elsa.ProtoActor.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.ProtoActor.Cluster.AzureContainerApps\Elsa.ProtoActor.Cluster.AzureContainerApps.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.WorkflowProviders.BlobStorage\Elsa.WorkflowProviders.BlobStorage.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Hangfire\Elsa.Hangfire.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Http\Elsa.Http.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Labels\Elsa.Labels.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Quartz\Elsa.Quartz.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.WorkflowContexts\Elsa.WorkflowContexts.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.8.2" />
<PackageReference Include="Proto.Persistence.Sqlite" Version="1.1.0" />
<PackageReference Include="Proto.Persistence.SqlServer" Version="1.3.1-alpha.0.2" />
<PackageReference Include="Azure.Identity" Version="1.8.2"/>
<PackageReference Include="Proto.Persistence.Sqlite" Version="1.1.0"/>
<PackageReference Include="Proto.Persistence.SqlServer" Version="1.3.1-alpha.0.2"/>
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\DropIns\"/>
</ItemGroup>
</Project>

View file

@ -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<Program>()
.AddWorkflowsFrom<Program>()
@ -61,19 +63,19 @@ services
.AddTypeAlias<ApiResponse<User>>("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<ApiResponse<User>>("Api");
management.AddVariableType<User>("Api");
management.AddVariableType<Support>("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<AllowAnonymousHttpEndpointAuthorizationHandler>())
.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();

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<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" Version="7.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Features\Elsa.Features.csproj"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,20 @@
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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<DropInDescriptor> 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<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 IEnumerable<string> ListPackages()
{
return Directory.GetFiles(_directoryPath, "*.nupkg", SearchOption.AllDirectories);
}
private Assembly? LoadDropInAssembly(string path)
{
return !File.Exists(path) ? null : AssemblyLoader.LoadPath(path);
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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<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")))
{
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;
}
}

View file

@ -0,0 +1,8 @@
using Elsa.DropIns.Models;
namespace Elsa.DropIns.Contracts;
public interface IDropInCatalog
{
IEnumerable<DropInDescriptor> List();
}

View file

@ -0,0 +1,8 @@
using Elsa.Features.Services;
namespace Elsa.DropIns.Contracts;
public interface IDropInInstaller
{
void Install(IModule module);
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<Description>
Provides drop-in features for Elsa workflows.
</Description>
<PackageTags>elsa drop-ins common</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1"/>
<PackageReference Include="NuGet.Packaging" Version="6.7.0" />
<PackageReference Include="NuGet.Protocol" Version="6.7.0" />
<PackageReference Include="ThrottleDebounce" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.DropIns.Core\Elsa.DropIns.Core.csproj"/>
</ItemGroup>
</Project>

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -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;
}
}

View file

@ -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 });
}
}

View file

@ -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;
/// <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 IServiceProvider _serviceProvider;
private readonly RateLimitedFunc<string, Task> _debouncedLoader;
/// <inheritdoc />
public DropInDirectoryMonitorHostedService(IOptions<DropInOptions> options, IServiceProvider serviceProvider)
{
_options = options;
_serviceProvider = serviceProvider;
_debouncedLoader = Debouncer.Debounce<string, Task>(LoadDropInAssemblyAsync, TimeSpan.FromSeconds(2));
}
/// <inheritdoc />
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);
}
}
}

View file

@ -0,0 +1,3 @@
namespace Elsa.DropIns.Models;
public record DropInDescriptor(Type Type);

View file

@ -0,0 +1,6 @@
namespace Elsa.DropIns.Options;
public class DropInOptions
{
public string DropInRootDirectory { get; set; } = default!;
}

View file

@ -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);
}
}
}

View file

@ -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!");
}
}

View file

@ -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<DropIn>();
}
public async ValueTask ConfigureAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
await activityRegistry.RegisterAsync<SampleActivity>(cancellationToken: cancellationToken);
}
}

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Elsa.DropIns.Core\Elsa.DropIns.Core.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
</ItemGroup>
</Project>