diff --git a/Remora.Plugins.Abstractions/Attributes/RemoraPlugin.cs b/Remora.Plugins.Abstractions/Attributes/RemoraPlugin.cs
index 1c23f16..7b6b585 100644
--- a/Remora.Plugins.Abstractions/Attributes/RemoraPlugin.cs
+++ b/Remora.Plugins.Abstractions/Attributes/RemoraPlugin.cs
@@ -31,19 +31,4 @@ namespace Remora.Plugins.Abstractions.Attributes;
///
[PublicAPI]
[AttributeUsage(AttributeTargets.Assembly)]
-public sealed class RemoraPlugin : Attribute
-{
- ///
- /// Gets the plugin descriptor that the assembly exports.
- ///
- public Type PluginDescriptor { get; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The descriptor type.
- public RemoraPlugin(Type pluginDescriptor)
- {
- this.PluginDescriptor = pluginDescriptor;
- }
-}
+public sealed class RemoraPlugin : Attribute;
diff --git a/Remora.Plugins.Abstractions/IMigratablePlugin.cs b/Remora.Plugins.Abstractions/IMigratablePlugin.cs
index 96b01fb..6747337 100644
--- a/Remora.Plugins.Abstractions/IMigratablePlugin.cs
+++ b/Remora.Plugins.Abstractions/IMigratablePlugin.cs
@@ -37,8 +37,7 @@ public interface IMigratablePlugin : IPluginDescriptor
///
/// Performs any migrations required by the plugin.
///
- /// The available services.
/// The cancellation token for this operation.
/// A representing the asynchronous operation.
- Task MigrateAsync(IServiceProvider serviceProvider, CancellationToken ct = default);
+ Task MigrateAsync(CancellationToken ct = default);
}
diff --git a/Remora.Plugins.Abstractions/IPluginDescriptor.cs b/Remora.Plugins.Abstractions/IPluginDescriptor.cs
index 6b2d68e..4d997ef 100644
--- a/Remora.Plugins.Abstractions/IPluginDescriptor.cs
+++ b/Remora.Plugins.Abstractions/IPluginDescriptor.cs
@@ -21,6 +21,7 @@
//
using System;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@@ -48,20 +49,22 @@ public interface IPluginDescriptor
///
/// Gets the version of the plugin.
///
- Version Version { get; }
+ Version Version => Assembly.GetAssembly(GetType())?.GetName().Version ?? new Version(1, 0, 0);
///
/// Configures services provided by the plugin in the application's service collection.
///
/// The service collection.
/// A result that may or may not have succeeded.
- Result ConfigureServices(IServiceCollection serviceCollection);
+ static virtual IServiceCollection ConfigureServices(IServiceCollection serviceCollection) => serviceCollection;
///
/// Performs any post-registration initialization required by the plugin.
///
- /// The service provider.
/// The cancellation token for this operation.
/// A result that may or may not have succeeded.
- ValueTask InitializeAsync(IServiceProvider serviceProvider, CancellationToken ct = default);
+ ValueTask InitializeAsync(CancellationToken ct = default);
+
+ ///
+ string ToString() => this.Name;
}
diff --git a/Remora.Plugins.Abstractions/PluginDescriptor.cs b/Remora.Plugins.Abstractions/PluginDescriptor.cs
deleted file mode 100644
index 18be397..0000000
--- a/Remora.Plugins.Abstractions/PluginDescriptor.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-//
-// PluginDescriptor.cs
-//
-// Author:
-// Jarl Gullberg
-//
-// Copyright (c) Jarl Gullberg
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Lesser General Public License for more details.
-//
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program. If not, see .
-//
-
-using System;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using JetBrains.Annotations;
-using Microsoft.Extensions.DependencyInjection;
-using Remora.Results;
-
-namespace Remora.Plugins.Abstractions;
-
-///
-/// Acts as a base class for plugin descriptors.
-///
-[PublicAPI]
-public abstract class PluginDescriptor : IPluginDescriptor
-{
- ///
- public abstract string Name { get; }
-
- ///
- public abstract string Description { get; }
-
- ///
- public virtual Version Version => Assembly.GetAssembly(GetType())?.GetName().Version ?? new Version(1, 0, 0);
-
- ///
- public virtual Result ConfigureServices(IServiceCollection serviceCollection)
- {
- return Result.FromSuccess();
- }
-
- ///
- public virtual ValueTask InitializeAsync(IServiceProvider serviceProvider, CancellationToken ct = default)
- {
- return new(Result.FromSuccess());
- }
-
- ///
- public sealed override string ToString()
- {
- return this.Name;
- }
-}
diff --git a/Remora.Plugins.Abstractions/Remora.Plugins.Abstractions.csproj b/Remora.Plugins.Abstractions/Remora.Plugins.Abstractions.csproj
index 393017d..2dfb9e6 100644
--- a/Remora.Plugins.Abstractions/Remora.Plugins.Abstractions.csproj
+++ b/Remora.Plugins.Abstractions/Remora.Plugins.Abstractions.csproj
@@ -1,6 +1,8 @@
5.0.1
+ false
+ net8.0;net9.0
Abstract representations of a plugin system.
diff --git a/Remora.Plugins/PluginTree.cs b/Remora.Plugins/PluginTree.cs
index e1f5c04..cb493e6 100644
--- a/Remora.Plugins/PluginTree.cs
+++ b/Remora.Plugins/PluginTree.cs
@@ -27,7 +27,6 @@
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
-using Microsoft.Extensions.DependencyInjection;
using Remora.Plugins.Abstractions;
using Remora.Plugins.Errors;
using Remora.Results;
@@ -37,10 +36,14 @@ namespace Remora.Plugins;
///
/// Represents a tree of plugins, ordered by their dependencies.
///
+///
+/// Initializes a new instance of the class.
+///
+/// The dependency branches.
[PublicAPI]
-public sealed class PluginTree
+public sealed class PluginTree(List? branches = null)
{
- private readonly List _branches;
+ private readonly List _branches = branches ?? [];
///
/// Gets the root nodes of the identified plugin dependency branches. The root node is considered to be the
@@ -48,44 +51,12 @@ public sealed class PluginTree
///
public IReadOnlyCollection Branches => _branches;
- ///
- /// Initializes a new instance of the class.
- ///
- /// The dependency branches.
- public PluginTree(List? branches = null)
- {
- _branches = branches ?? new List();
- }
-
- ///
- /// Configures the services required by the plugins.
- ///
- /// The service collection to configure.
- /// A result which may or may not have succeeded.
- public Result ConfigureServices(IServiceCollection serviceCollection)
- {
- var results = Walk
- (
- node => new PluginConfigurationFailed
- (
- node.Plugin,
- "One or more of the plugin's dependencies failed to configure their services."
- ),
- node => node.Plugin.ConfigureServices(serviceCollection)
- ).ToList();
-
- return results.Any(r => !r.IsSuccess)
- ? new AggregateError(results.Where(r => !r.IsSuccess).Cast().ToList())
- : Result.FromSuccess();
- }
-
///
/// Initializes the plugins in the tree.
///
- /// The available services.
/// The cancellation token for this operation.
/// A result which may or may not have succeeded.
- public async Task InitializeAsync(IServiceProvider services, CancellationToken ct = default)
+ public async Task InitializeAsync(CancellationToken ct = default)
{
var results = await WalkAsync
(
@@ -94,7 +65,7 @@ public async Task InitializeAsync(IServiceProvider services, Cancellatio
node.Plugin,
"One or more of the plugin's dependencies failed to initialize."
),
- async (node, c) => await node.Plugin.InitializeAsync(services, c),
+ async (node, c) => await node.Plugin.InitializeAsync(c),
ct: ct
).ToListAsync(ct);
@@ -106,10 +77,9 @@ public async Task InitializeAsync(IServiceProvider services, Cancellatio
///
/// Migrates any persistent data stores of the plugins in the tree.
///
- /// The available services.
/// The cancellation token for this operation.
/// A result which may or may not have succeeded.
- public async Task MigrateAsync(IServiceProvider services, CancellationToken ct = default)
+ public async Task MigrateAsync(CancellationToken ct = default)
{
var results = await WalkAsync
(
@@ -125,7 +95,7 @@ public async Task MigrateAsync(IServiceProvider services, CancellationTo
return Result.FromSuccess();
}
- return await migratablePlugin.MigrateAsync(services, c);
+ return await migratablePlugin.MigrateAsync(c);
},
ct: ct
).ToListAsync(ct);
diff --git a/Remora.Plugins/PluginTreeBuilder.cs b/Remora.Plugins/PluginTreeBuilder.cs
new file mode 100644
index 0000000..814373c
--- /dev/null
+++ b/Remora.Plugins/PluginTreeBuilder.cs
@@ -0,0 +1,69 @@
+//
+// PluginTreeBuilder.cs
+//
+// Author:
+// Jarl Gullberg
+//
+// Copyright (c) Jarl Gullberg
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program. If not, see .
+//
+
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using Remora.Plugins.Abstractions;
+
+namespace Remora.Plugins
+{
+ ///
+ /// A type that facilitates the creation of a .
+ ///
+ /// A filter predicate which determines if the plugin should be loaded.
+ [PublicAPI]
+ public sealed class PluginTreeBuilder(Predicate filter)
+ {
+ private readonly List _treeNodeBuilders = [];
+
+ ///
+ /// Adds a tree node builder to the collection.
+ ///
+ /// The node to add.
+ public void AddTreeNode(PluginTreeNodeBuilder node)
+ {
+ _treeNodeBuilders.Add(node);
+ }
+
+ ///
+ /// Builds the .
+ ///
+ /// The service provider.
+ /// A new .
+ [Pure]
+ [PublicAPI]
+ public PluginTree Build(IServiceProvider serviceProvider)
+ {
+ var tree = new PluginTree();
+ foreach (var node in _treeNodeBuilders)
+ {
+ var pluginTreeNode = node.Build(serviceProvider);
+ if (filter.Invoke(pluginTreeNode.Plugin))
+ {
+ tree.AddBranch(pluginTreeNode);
+ }
+ }
+ return tree;
+ }
+ }
+}
diff --git a/Remora.Plugins/PluginTreeNode.cs b/Remora.Plugins/PluginTreeNode.cs
index df47441..9277db0 100644
--- a/Remora.Plugins/PluginTreeNode.cs
+++ b/Remora.Plugins/PluginTreeNode.cs
@@ -57,7 +57,7 @@ public PluginTreeNode
)
{
this.Plugin = plugin;
- _dependents = dependants ?? new List();
+ _dependents = dependants ?? [];
}
///
@@ -94,6 +94,6 @@ public IEnumerable GetAllDependents()
///
public override string ToString()
{
- return $"{this.Plugin} => ({string.Join(", ", _dependents.Select(d => d.Plugin))})";
+ return $"{this.Plugin} => ({string.Join(", ", _dependents.Select(d => d.Plugin.ToString()))})";
}
}
diff --git a/Remora.Plugins/PluginTreeNodeBuilder.cs b/Remora.Plugins/PluginTreeNodeBuilder.cs
new file mode 100644
index 0000000..86254c8
--- /dev/null
+++ b/Remora.Plugins/PluginTreeNodeBuilder.cs
@@ -0,0 +1,98 @@
+//
+// PluginTreeNodeBuilder.cs
+//
+// Author:
+// Jarl Gullberg
+//
+// Copyright (c) Jarl Gullberg
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program. If not, see .
+//
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+using Remora.Plugins.Abstractions;
+
+namespace Remora.Plugins
+{
+ ///
+ /// A type which stages the creation of a .
+ ///
+ /// The assembly the plugin belongs to.
+ /// The type of the plugin to construct.
+ [PublicAPI]
+ public sealed class PluginTreeNodeBuilder(Assembly pluginAssembly, Type pluginType)
+ {
+ ///
+ /// Gets the plugin type.
+ ///
+ [PublicAPI]
+ public Type PluginType { get; } = pluginType;
+
+ ///
+ /// Gets the plugin assembly.
+ ///
+ [PublicAPI]
+ public Assembly PluginAssembly { get; } = pluginAssembly;
+
+ ///
+ /// Gets the dependents of this plugin node.
+ ///
+ [PublicAPI]
+ public List Dependents { get; } = [];
+
+ ///
+ /// Adds a dependent.
+ ///
+ /// The dependent to add.
+ public void AddDependent(PluginTreeNodeBuilder dependent)
+ {
+ this.Dependents.Add(dependent);
+ }
+
+ ///
+ /// Builds the plugin tree node.
+ ///
+ /// The service provider used to construct the plugins.
+ /// A newly constructed .
+ [Pure]
+ public PluginTreeNode Build(IServiceProvider services)
+ {
+ var plugin = BuildPluginDescriptor(services, this.PluginType);
+ var node = new PluginTreeNode(plugin);
+
+ foreach (var dependent in this.Dependents)
+ {
+ var dependentPlugin = BuildPluginDescriptor(services, dependent.PluginType);
+ var dependentNode = new PluginTreeNode(dependentPlugin);
+ node.AddDependent(dependentNode);
+ }
+
+ return node;
+ }
+
+ ///
+ /// Builds a from the provided .
+ ///
+ /// The service provider.
+ /// The plugin type.
+ /// The newly constructed .
+ [Pure]
+ internal static IPluginDescriptor BuildPluginDescriptor(IServiceProvider services, Type pluginType)
+ => (IPluginDescriptor)ActivatorUtilities.CreateInstance(services, pluginType);
+ }
+}
diff --git a/Remora.Plugins/README.md b/Remora.Plugins/README.md
index c580415..4aed304 100644
--- a/Remora.Plugins/README.md
+++ b/Remora.Plugins/README.md
@@ -17,30 +17,28 @@ Generally, plugins should only reference Remora.Plugins.Abstractions, while the
main application should reference and use Remora.Plugins.
```c#
-[assembly: RemoraPlugin(typeof(MyPlugin))]
+[assembly: RemoraPlugin]
-public sealed class MyPlugin : PluginDescriptor
+public sealed class MyPlugin(MyService myService) : IPluginDescriptor
{
///
- public override string Name => "My Plugin";
+ public string Name => "My Plugin";
///
- public override string Description => "My plugin that does a thing.";
+ public string Description => "My plugin that does a thing.";
///
- public override Result ConfigureServices(IServiceCollection serviceCollection)
+ public static IServiceCollection ConfigureServices(IServiceCollection serviceCollection)
{
- serviceCollection
- .AddScoped();
+ serviceCollection.AddScoped();
- return Result.FromSuccess();
+ return serviceCollection;
}
///
- public override async ValueTask InitializeAsync(IServiceProvider serviceProvider)
+ public override async ValueTask InitializeAsync(CancellationToken ct = default)
{
- var myService = serviceProvider.GetRequiredService();
- var doThing = await myService.DoTheThingAsync();
+ var doThing = await myService.DoTheThingAsync(ct);
if (!doThing.IsSuccess)
{
return doThing;
@@ -55,29 +53,25 @@ Loading plugins in your application is equally simple. The example below is
perhaps a little convoluted, but shows the flexibility of the system.
```c#
-var pluginService = new PluginService();
+var pluginServiceOptions = new(["./plugins", "./mods"], scanAssemblyDirectory: false);
+var pluginService = new PluginService(pluginServiceOptions);
-var serviceCollection = new ServiceCollection()
- .AddSingleton(pluginService);
+var serviceCollection = new ServiceCollection();
-var pluginTree = pluginService.LoadPluginTree();
-var configurePlugins = pluginTree.ConfigureServices(serviceCollection);
-if (!configurePlugins.IsSuccess)
-{
- // check configurePlugins.Error to figure out why
- return;
-}
+// Load plugins where the plugin name is greater than 3 characters long.
+var pluginTreeBuilder = pluginService.LoadPluginTree(serviceCollection, filter: plugin => plugin.Name.Length > 3);
_services = serviceCollection.BuildServiceProvider();
+var pluginTree = pluginTreeBuilder.Build(_services);
-var initializePlugins = await pluginTree.InitializeAsync(_services, ct);
+var initializePlugins = await pluginTree.InitializeAsync(ct);
if (!initializePlugins.IsSuccess)
{
// check initializePlugins.Error to figure out why
return;
}
-var migratePlugins = await pluginTree.MigrateAsync(_services, ct);
+var migratePlugins = await pluginTree.MigrateAsync(ct);
if (!migratePlugins.IsSuccess)
{
// check migratePlugins.Error to figure out why
diff --git a/Remora.Plugins/Remora.Plugins.csproj b/Remora.Plugins/Remora.Plugins.csproj
index b68e9ad..77b1855 100644
--- a/Remora.Plugins/Remora.Plugins.csproj
+++ b/Remora.Plugins/Remora.Plugins.csproj
@@ -1,6 +1,8 @@
4.0.1
+ false
+ net8.0;net9.0
The default implementation of Remora.Plugins.Abstractions.
diff --git a/Remora.Plugins/Services/PluginService.cs b/Remora.Plugins/Services/PluginService.cs
index 98a8d0d..2187fc2 100644
--- a/Remora.Plugins/Services/PluginService.cs
+++ b/Remora.Plugins/Services/PluginService.cs
@@ -22,11 +22,12 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
-using Microsoft.Extensions.Options;
+using Microsoft.Extensions.DependencyInjection;
using Remora.Plugins.Abstractions;
using Remora.Plugins.Abstractions.Attributes;
using Remora.Plugins.Errors;
@@ -38,113 +39,119 @@ namespace Remora.Plugins.Services;
///
/// Serves functionality related to plugins.
///
+///
+/// Initializes a new instance of the class.
+///
+/// The service options.
[PublicAPI]
-public sealed class PluginService
+public sealed class PluginService(PluginServiceOptions? options = null)
{
- private readonly PluginServiceOptions _options;
+ private readonly PluginServiceOptions _options = options ?? PluginServiceOptions.Default;
- ///
- /// Initializes a new instance of the class.
- ///
- /// The service options.
- public PluginService(IOptions options)
- {
- _options = options.Value;
- }
+ private Dictionary>? _pluginsByAssembly;
///
/// Loads all available plugins into a tree structure, ordered by their topological dependencies. Effectively, this
/// means that will contain dependency-free plugins, with subsequent
/// dependents below them (recursively).
///
+ /// The service collection with which to register the plugin tree.
/// If provided, any plugins must match the defined predicate to be added to the .
/// The dependency tree.
[PublicAPI, Pure]
- public PluginTree LoadPluginTree(Predicate? filter = null)
+ public PluginTreeBuilder LoadPluginTree(IServiceCollection services, Predicate? filter = null)
{
- var pluginAssemblies = LoadAvailablePluginAssemblies().ToList();
- var pluginsWithDependencies = pluginAssemblies.ToDictionary
+ filter ??= _ => true;
+ LoadAvailablePluginAssemblies();
+
+ var pluginsWithDependencies = _pluginsByAssembly.Keys.ToDictionary
(
- a => a.PluginAssembly,
- a => a.PluginAssembly.GetReferencedAssemblies()
- .Where(ra => pluginAssemblies.Any(pa => pa.PluginAssembly.FullName == ra.FullName))
- .Select(ra => pluginAssemblies.First(pa => pa.PluginAssembly.FullName == ra.FullName))
- .Select(ra => ra.PluginAssembly)
+ a => a,
+ a => a.GetReferencedAssemblies()
+ .Where(ra => _pluginsByAssembly.Keys.Any(pa => pa.FullName == ra.FullName))
+ .Select(ra => _pluginsByAssembly.Keys.First(pa => pa.FullName == ra.FullName))
);
- bool IsDependency(Assembly assembly, Assembly other)
+ // Load plugin dependencies.
+ foreach (IEnumerable plugins in _pluginsByAssembly.Values)
{
- var dependencies = pluginsWithDependencies[assembly];
- foreach (var dependency in dependencies)
+ MethodInfo configureHelper = typeof(PluginService).GetMethod(nameof(ConfigurePlugin))
+ ?? throw new InvalidOperationException(); // This will never be null.
+
+ foreach (var plugin in plugins)
{
- if (dependency == other)
+ configureHelper.MakeGenericMethod(plugin).Invoke(null, [services]);
+ }
+ }
+
+ // Build and populate plugin tree.
+ var tree = new PluginTreeBuilder(filter);
+ var nodes = new Dictionary();
+
+ var sorted = pluginsWithDependencies.Keys.TopologicalSort(k => pluginsWithDependencies[k]).ToList();
+ while (sorted.Count > 0)
+ {
+ Assembly current = sorted[0];
+ IEnumerable pluginTypes = _pluginsByAssembly[current];
+
+ foreach (var type in pluginTypes)
+ {
+ var treeNodeBuilder = new PluginTreeNodeBuilder(current, type);
+ var dependencies = pluginsWithDependencies[current].ToList();
+
+ if (!dependencies.Any())
{
- return true;
+ tree.AddTreeNode(treeNodeBuilder);
}
- if (IsDependency(dependency, other))
+ foreach (var dependency in dependencies)
{
- return true;
+ if (!IsDirectDependency(current, dependency))
+ {
+ continue;
+ }
+
+ var dependencyNode = nodes[dependency];
+ dependencyNode.AddDependent(treeNodeBuilder);
}
- }
- return false;
+ nodes.Add(current, treeNodeBuilder);
+ sorted.Remove(current);
+ }
}
+ return tree;
+
bool IsDirectDependency(Assembly assembly, Assembly dependency)
{
var dependencies = pluginsWithDependencies[assembly];
return IsDependency(assembly, dependency) && dependencies.All(d => !IsDependency(d, dependency));
}
- var tree = new PluginTree();
- var nodes = new Dictionary();
-
- var sorted = pluginsWithDependencies.Keys.TopologicalSort(k => pluginsWithDependencies[k]).ToList();
- while (sorted.Count > 0)
+ bool IsDependency(Assembly assembly, Assembly other)
{
- var current = sorted[0];
- var loadDescriptorResult = LoadPluginDescriptor(current);
- if (!loadDescriptorResult.IsDefined(out IPluginDescriptor? pluginDescriptor))
- {
- continue;
- }
-
- if (!filter?.Invoke(pluginDescriptor) ?? false)
- {
- continue;
- }
-
- var node = new PluginTreeNode(pluginDescriptor);
-
- var dependencies = pluginsWithDependencies[current].ToList();
- if (!dependencies.Any())
- {
- // This is a root of a chain
- tree.AddBranch(node);
- }
-
+ var dependencies = pluginsWithDependencies[assembly];
foreach (var dependency in dependencies)
{
- if (!IsDirectDependency(current, dependency))
+ if (dependency == other)
{
- continue;
+ return true;
}
- var dependencyNode = nodes[dependency];
- dependencyNode.AddDependent(node);
+ if (IsDependency(dependency, other))
+ {
+ return true;
+ }
}
- nodes.Add(current, node);
- sorted.Remove(current);
+ return false;
}
-
- return tree;
}
///
/// Loads all available plugins into a flat list.
///
+ /// The service provider used to build the plugins.
///
/// This method should generally not be used for actually loading plugins into your application, since it may not
/// properly order plugins in more complex dependency graphs. Prefer using and its
@@ -152,78 +159,44 @@ bool IsDirectDependency(Assembly assembly, Assembly dependency)
///
/// The descriptors of the available plugins.
[Pure]
- public IEnumerable LoadPlugins()
+ public IEnumerable LoadPlugins(IServiceProvider services)
{
- var pluginAssemblies = LoadAvailablePluginAssemblies().ToList();
- var sorted = pluginAssemblies.TopologicalSort
+ LoadAvailablePluginAssemblies();
+ var pluginsWithDependencies = _pluginsByAssembly.Keys.ToDictionary
(
- a => a.PluginAssembly.GetReferencedAssemblies()
- .Where
- (
- n => pluginAssemblies.Any(pa => pa.PluginAssembly.GetName().FullName == n.FullName)
- )
- .Select
- (
- n => pluginAssemblies.First(pa => pa.PluginAssembly.GetName().FullName == n.FullName)
- )
+ a => a,
+ a => a.GetReferencedAssemblies()
+ .Where(ra => _pluginsByAssembly.Keys.Any(pa => pa.FullName == ra.FullName))
+ .Select(ra => _pluginsByAssembly.Keys.First(pa => pa.FullName == ra.FullName))
);
- foreach (var pluginAssembly in sorted)
+ foreach ((Assembly assembly, IEnumerable types) in pluginsWithDependencies)
{
- var descriptor = (IPluginDescriptor?)Activator.CreateInstance
- (
- pluginAssembly.PluginAttribute.PluginDescriptor
- );
+ var pluginTypes = _pluginsByAssembly[assembly].Concat(types.SelectMany(it => _pluginsByAssembly[it]));
- if (descriptor is null)
+ foreach (var pluginType in pluginTypes)
{
- continue;
+ yield return PluginTreeNodeBuilder.BuildPluginDescriptor(services, pluginType);
}
-
- yield return descriptor;
}
}
+ private static IServiceCollection ConfigurePlugin(IServiceCollection services)
+ where TPluginDescriptor : IPluginDescriptor
+ => TPluginDescriptor.ConfigureServices(services);
+
///
- /// Loads the plugin descriptor from the given assembly.
+ /// Loads the available plugin assemblies.
///
- /// The assembly.
- /// The plugin descriptor.
- [Pure]
- private static Result LoadPluginDescriptor(Assembly assembly)
+ /// If , this will empty and re-create the plugins.
+ [MemberNotNull(nameof(_pluginsByAssembly))]
+ private void LoadAvailablePluginAssemblies(bool reload = false)
{
- var pluginAttribute = assembly.GetCustomAttribute();
- if (pluginAttribute is null)
- {
- return new AssemblyIsNotPluginError();
- }
-
- IPluginDescriptor descriptor;
- try
- {
- var createdDescriptor = (IPluginDescriptor?)Activator.CreateInstance(pluginAttribute.PluginDescriptor);
- if (createdDescriptor is null)
- {
- return new InvalidPluginError();
- }
-
- descriptor = createdDescriptor;
- }
- catch (Exception e)
+ if (!reload && _pluginsByAssembly?.Count > 0)
{
- return e;
+ return;
}
- return Result.FromSuccess(descriptor);
- }
-
- ///
- /// Loads the available plugin assemblies.
- ///
- /// The available assemblies.
- [Pure]
- private IEnumerable<(RemoraPlugin PluginAttribute, Assembly PluginAssembly)> LoadAvailablePluginAssemblies()
- {
var searchPaths = new List();
if (_options.ScanAssemblyDirectory)
@@ -249,8 +222,11 @@ private static Result LoadPluginDescriptor(Assembly assembly)
"*.dll",
SearchOption.AllDirectories
)
- ).SelectMany(a => a);
+ )
+ .SelectMany(a => a)
+ .ToArray();
+ _pluginsByAssembly = new(assemblyPaths.Length);
foreach (var assemblyPath in assemblyPaths)
{
Assembly assembly;
@@ -263,13 +239,14 @@ private static Result LoadPluginDescriptor(Assembly assembly)
continue;
}
- var pluginAttribute = assembly.GetCustomAttribute();
- if (pluginAttribute is null)
+ if (assembly.GetCustomAttribute() is not null)
{
- continue;
+ _pluginsByAssembly[assembly] = assembly.GetExportedTypes().Where(IsPlugin);
}
-
- yield return (pluginAttribute, assembly);
}
+
+ static bool IsPlugin(Type type)
+ => typeof(IPluginDescriptor).IsAssignableFrom(type) &&
+ type is { IsAbstract: false, IsInterface: false };
}
}
diff --git a/Remora.Plugins/Services/PluginServiceOptions.cs b/Remora.Plugins/Services/PluginServiceOptions.cs
index 476a712..68eec09 100644
--- a/Remora.Plugins/Services/PluginServiceOptions.cs
+++ b/Remora.Plugins/Services/PluginServiceOptions.cs
@@ -35,4 +35,10 @@ public record PluginServiceOptions
(
IEnumerable PluginSearchPaths,
bool ScanAssemblyDirectory = true
-);
+)
+{
+ ///
+ /// Gets a default, empty instance of this which searches the assembly directory.
+ ///
+ public static PluginServiceOptions Default { get; } = new([]);
+}