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([]); +}