From a7a9bd057767748e0b9e9fba9e1352bfe1906a13 Mon Sep 17 00:00:00 2001 From: Foxtrek_64 Date: Sat, 31 May 2025 15:17:29 -0500 Subject: [PATCH 1/8] Add static plugin initializer, add tree builder and tree node builder --- .../Attributes/RemoraPlugin.cs | 17 +- .../IPluginDescriptor.cs | 11 +- .../PluginDescriptor.cs | 65 ----- .../Remora.Plugins.Abstractions.csproj | 2 + Remora.Plugins/PluginTree.cs | 45 +--- Remora.Plugins/PluginTreeBuilder.cs | 62 +++++ Remora.Plugins/PluginTreeNode.cs | 4 +- Remora.Plugins/PluginTreeNodeBuilder.cs | 91 +++++++ Remora.Plugins/README.md | 39 ++- Remora.Plugins/Remora.Plugins.csproj | 2 + Remora.Plugins/Services/PluginService.cs | 236 ++++++++++-------- .../Services/PluginServiceOptions.cs | 10 +- 12 files changed, 328 insertions(+), 256 deletions(-) delete mode 100644 Remora.Plugins.Abstractions/PluginDescriptor.cs create mode 100644 Remora.Plugins/PluginTreeBuilder.cs create mode 100644 Remora.Plugins/PluginTreeNodeBuilder.cs 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/IPluginDescriptor.cs b/Remora.Plugins.Abstractions/IPluginDescriptor.cs index 6b2d68e..8f5f7f8 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); + + /// + virtual 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..7cd57b6 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); diff --git a/Remora.Plugins/PluginTreeBuilder.cs b/Remora.Plugins/PluginTreeBuilder.cs new file mode 100644 index 0000000..bbf0635 --- /dev/null +++ b/Remora.Plugins/PluginTreeBuilder.cs @@ -0,0 +1,62 @@ +// +// 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 System.Diagnostics.Contracts; + +namespace Remora.Plugins +{ + /// + /// A type that facilitates the creation of a . + /// + public sealed class PluginTreeBuilder + { + 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] + public PluginTree Build(IServiceProvider serviceProvider) + { + var tree = new PluginTree(); + foreach (var node in _treeNodeBuilders) + { + var pluginTreeNode = node.Build(serviceProvider); + 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..7baf8f4 --- /dev/null +++ b/Remora.Plugins/PluginTreeNodeBuilder.cs @@ -0,0 +1,91 @@ +// +// 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 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. + public sealed class PluginTreeNodeBuilder(Assembly pluginAssembly, Type pluginType) + { + /// + /// Gets the plugin type. + /// + public Type PluginType { get; } = pluginType; + + /// + /// Gets the plugin assembly. + /// + public Assembly PluginAssembly { get; } = pluginAssembly; + + /// + /// Gets the dependents of this plugin node. + /// + public List Dependents { get; } = []; + + /// + /// Adds a dependent. + /// + /// The dependent to add. + public void AddDependent(PluginTreeNodeBuilder dependent) + { + Dependents.Add(dependent); + } + + /// + /// Builds the plugin tree node. + /// + /// The service provider used to construct the plugins. + /// A newly constructed . + public PluginTreeNode Build(IServiceProvider services) + { + var plugin = BuildPluginDescriptor(services, PluginType); + var node = new PluginTreeNode(plugin); + + foreach (var dependent in 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 . + 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..6166247 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,24 @@ 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 initializePlugins = await pluginTree.InitializeAsync(_services, ct); +var initializePlugins = await pluginTreeBuilder.InitializeAsync(_services, ct); if (!initializePlugins.IsSuccess) { // check initializePlugins.Error to figure out why return; } -var migratePlugins = await pluginTree.MigrateAsync(_services, ct); +var migratePlugins = await pluginTreeBuilder.MigrateAsync(_services, 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..7c86bbc 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 = null; /// /// 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 ((Assembly assembly, IEnumerable plugins) in _pluginsByAssembly) { - 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(); + 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,87 @@ 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; + static IEnumerable BuildPluginDescriptorsForAssembly(IServiceProvider services, IEnumerable pluginTypes) + { + foreach (var pluginType in pluginTypes) + { + yield return PluginTreeNodeBuilder.BuildPluginDescriptor(services, pluginType); + } } } + private static IServiceCollection ConfigurePlugin(IServiceCollection services) + where TPluginDescriptor : IPluginDescriptor + => TPluginDescriptor.ConfigureServices(services); + /// /// Loads the plugin descriptor from the given assembly. /// /// The assembly. /// The plugin descriptor. [Pure] - private static Result LoadPluginDescriptor(Assembly assembly) + private static Result> LoadPluginDescriptors(Assembly assembly, IEnumerable plugins) { - var pluginAttribute = assembly.GetCustomAttribute(); - if (pluginAttribute is null) - { - return new AssemblyIsNotPluginError(); - } + IPluginDescriptor[] pluginDescriptors = new IPluginDescriptor[plugins.Count()]; + int index = 0; - IPluginDescriptor descriptor; - try + foreach (var plugin in plugins) { - var createdDescriptor = (IPluginDescriptor?)Activator.CreateInstance(pluginAttribute.PluginDescriptor); - if (createdDescriptor is null) + try { - return new InvalidPluginError(); - } + // TODO: Wire up to service provider. + // ActivatorUtilities.CreateInstance(serviceProvider, type) + var descriptor = (IPluginDescriptor?)Activator.CreateInstance(plugin); + if (descriptor is null) + { + return new InvalidPluginError(); + } - descriptor = createdDescriptor; - } - catch (Exception e) - { - return e; + pluginDescriptors[index++] = descriptor; + } + catch (Exception e) + { + return e; + } } - return Result.FromSuccess(descriptor); + return pluginDescriptors; } /// /// Loads the available plugin assemblies. /// - /// The available assemblies. + /// If , this will empty and re-create the plugins. [Pure] - private IEnumerable<(RemoraPlugin PluginAttribute, Assembly PluginAssembly)> LoadAvailablePluginAssemblies() + [MemberNotNull(nameof(_pluginsByAssembly))] + private void LoadAvailablePluginAssemblies(bool reload = false) { + if (!reload && _pluginsByAssembly?.Count > 0) + { + return; + } + var searchPaths = new List(); if (_options.ScanAssemblyDirectory) @@ -249,8 +265,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 +282,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..2674a5e 100644 --- a/Remora.Plugins/Services/PluginServiceOptions.cs +++ b/Remora.Plugins/Services/PluginServiceOptions.cs @@ -20,7 +20,9 @@ // along with this program. If not, see . // +using System; using System.Collections.Generic; +using Remora.Plugins.Abstractions; namespace Remora.Plugins.Services; @@ -35,4 +37,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([]); +} From a6eebfb61a9c57e507feed0de33fddb2ff304069 Mon Sep 17 00:00:00 2001 From: Foxtrek_64 Date: Sun, 1 Jun 2025 10:48:40 -0500 Subject: [PATCH 2/8] Some code cleanup, actually pass through the plugin filter --- .../IPluginDescriptor.cs | 2 +- Remora.Plugins/PluginTreeBuilder.cs | 12 +++-- Remora.Plugins/PluginTreeNodeBuilder.cs | 10 ++-- Remora.Plugins/Services/PluginService.cs | 49 ++----------------- .../Services/PluginServiceOptions.cs | 2 - 5 files changed, 20 insertions(+), 55 deletions(-) diff --git a/Remora.Plugins.Abstractions/IPluginDescriptor.cs b/Remora.Plugins.Abstractions/IPluginDescriptor.cs index 8f5f7f8..4d997ef 100644 --- a/Remora.Plugins.Abstractions/IPluginDescriptor.cs +++ b/Remora.Plugins.Abstractions/IPluginDescriptor.cs @@ -66,5 +66,5 @@ public interface IPluginDescriptor ValueTask InitializeAsync(CancellationToken ct = default); /// - virtual string ToString() => this.Name; + string ToString() => this.Name; } diff --git a/Remora.Plugins/PluginTreeBuilder.cs b/Remora.Plugins/PluginTreeBuilder.cs index bbf0635..f627319 100644 --- a/Remora.Plugins/PluginTreeBuilder.cs +++ b/Remora.Plugins/PluginTreeBuilder.cs @@ -22,14 +22,17 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Contracts; +using JetBrains.Annotations; +using Remora.Plugins.Abstractions; namespace Remora.Plugins { /// /// A type that facilitates the creation of a . /// - public sealed class PluginTreeBuilder + /// A filter predicate which determines if the plugin should be loaded. + [PublicAPI] + public sealed class PluginTreeBuilder(Predicate filter) { private readonly List _treeNodeBuilders = []; @@ -54,7 +57,10 @@ public PluginTree Build(IServiceProvider serviceProvider) foreach (var node in _treeNodeBuilders) { var pluginTreeNode = node.Build(serviceProvider); - tree.AddBranch(pluginTreeNode); + if (filter.Invoke(pluginTreeNode.Plugin)) + { + tree.AddBranch(pluginTreeNode); + } } return tree; } diff --git a/Remora.Plugins/PluginTreeNodeBuilder.cs b/Remora.Plugins/PluginTreeNodeBuilder.cs index 7baf8f4..0c93d37 100644 --- a/Remora.Plugins/PluginTreeNodeBuilder.cs +++ b/Remora.Plugins/PluginTreeNodeBuilder.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Remora.Plugins.Abstractions; @@ -33,6 +34,7 @@ namespace Remora.Plugins /// /// The assembly the plugin belongs to. /// The type of the plugin to construct. + [PublicAPI] public sealed class PluginTreeNodeBuilder(Assembly pluginAssembly, Type pluginType) { /// @@ -56,7 +58,7 @@ public sealed class PluginTreeNodeBuilder(Assembly pluginAssembly, Type pluginTy /// The dependent to add. public void AddDependent(PluginTreeNodeBuilder dependent) { - Dependents.Add(dependent); + this.Dependents.Add(dependent); } /// @@ -64,12 +66,13 @@ public void AddDependent(PluginTreeNodeBuilder dependent) /// /// The service provider used to construct the plugins. /// A newly constructed . + [Pure] public PluginTreeNode Build(IServiceProvider services) { - var plugin = BuildPluginDescriptor(services, PluginType); + var plugin = PluginTreeNodeBuilder.BuildPluginDescriptor(services, PluginType); var node = new PluginTreeNode(plugin); - foreach (var dependent in Dependents) + foreach (var dependent in this.Dependents) { var dependentPlugin = BuildPluginDescriptor(services, dependent.PluginType); var dependentNode = new PluginTreeNode(dependentPlugin); @@ -85,6 +88,7 @@ public PluginTreeNode Build(IServiceProvider services) /// 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/Services/PluginService.cs b/Remora.Plugins/Services/PluginService.cs index 7c86bbc..2187fc2 100644 --- a/Remora.Plugins/Services/PluginService.cs +++ b/Remora.Plugins/Services/PluginService.cs @@ -48,7 +48,7 @@ public sealed class PluginService(PluginServiceOptions? options = null) { private readonly PluginServiceOptions _options = options ?? PluginServiceOptions.Default; - private Dictionary>? _pluginsByAssembly = null; + private Dictionary>? _pluginsByAssembly; /// /// Loads all available plugins into a tree structure, ordered by their topological dependencies. Effectively, this @@ -73,7 +73,7 @@ public PluginTreeBuilder LoadPluginTree(IServiceCollection services, Predicate plugins) in _pluginsByAssembly) + foreach (IEnumerable plugins in _pluginsByAssembly.Values) { MethodInfo configureHelper = typeof(PluginService).GetMethod(nameof(ConfigurePlugin)) ?? throw new InvalidOperationException(); // This will never be null. @@ -85,7 +85,7 @@ public PluginTreeBuilder LoadPluginTree(IServiceCollection services, Predicate(); var sorted = pluginsWithDependencies.Keys.TopologicalSort(k => pluginsWithDependencies[k]).ToList(); @@ -179,59 +179,16 @@ public IEnumerable LoadPlugins(IServiceProvider services) yield return PluginTreeNodeBuilder.BuildPluginDescriptor(services, pluginType); } } - - static IEnumerable BuildPluginDescriptorsForAssembly(IServiceProvider services, IEnumerable pluginTypes) - { - foreach (var pluginType in pluginTypes) - { - yield return PluginTreeNodeBuilder.BuildPluginDescriptor(services, pluginType); - } - } } private static IServiceCollection ConfigurePlugin(IServiceCollection services) where TPluginDescriptor : IPluginDescriptor => TPluginDescriptor.ConfigureServices(services); - /// - /// Loads the plugin descriptor from the given assembly. - /// - /// The assembly. - /// The plugin descriptor. - [Pure] - private static Result> LoadPluginDescriptors(Assembly assembly, IEnumerable plugins) - { - IPluginDescriptor[] pluginDescriptors = new IPluginDescriptor[plugins.Count()]; - int index = 0; - - foreach (var plugin in plugins) - { - try - { - // TODO: Wire up to service provider. - // ActivatorUtilities.CreateInstance(serviceProvider, type) - var descriptor = (IPluginDescriptor?)Activator.CreateInstance(plugin); - if (descriptor is null) - { - return new InvalidPluginError(); - } - - pluginDescriptors[index++] = descriptor; - } - catch (Exception e) - { - return e; - } - } - - return pluginDescriptors; - } - /// /// Loads the available plugin assemblies. /// /// If , this will empty and re-create the plugins. - [Pure] [MemberNotNull(nameof(_pluginsByAssembly))] private void LoadAvailablePluginAssemblies(bool reload = false) { diff --git a/Remora.Plugins/Services/PluginServiceOptions.cs b/Remora.Plugins/Services/PluginServiceOptions.cs index 2674a5e..68eec09 100644 --- a/Remora.Plugins/Services/PluginServiceOptions.cs +++ b/Remora.Plugins/Services/PluginServiceOptions.cs @@ -20,9 +20,7 @@ // along with this program. If not, see . // -using System; using System.Collections.Generic; -using Remora.Plugins.Abstractions; namespace Remora.Plugins.Services; From 82b80276cab131233c9fadb83b5419a11a414c4a Mon Sep 17 00:00:00 2001 From: Foxtrek_64 Date: Sun, 1 Jun 2025 10:51:31 -0500 Subject: [PATCH 3/8] Some more attributes added --- Remora.Plugins/PluginTreeBuilder.cs | 1 + Remora.Plugins/PluginTreeNodeBuilder.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Remora.Plugins/PluginTreeBuilder.cs b/Remora.Plugins/PluginTreeBuilder.cs index f627319..814373c 100644 --- a/Remora.Plugins/PluginTreeBuilder.cs +++ b/Remora.Plugins/PluginTreeBuilder.cs @@ -51,6 +51,7 @@ public void AddTreeNode(PluginTreeNodeBuilder node) /// The service provider. /// A new . [Pure] + [PublicAPI] public PluginTree Build(IServiceProvider serviceProvider) { var tree = new PluginTree(); diff --git a/Remora.Plugins/PluginTreeNodeBuilder.cs b/Remora.Plugins/PluginTreeNodeBuilder.cs index 0c93d37..72ee021 100644 --- a/Remora.Plugins/PluginTreeNodeBuilder.cs +++ b/Remora.Plugins/PluginTreeNodeBuilder.cs @@ -40,16 +40,19 @@ public sealed class PluginTreeNodeBuilder(Assembly pluginAssembly, Type pluginTy /// /// 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; } = []; /// From fcf1c9577c9f4b56ab7b2a590a0a9742ff1fdada Mon Sep 17 00:00:00 2001 From: Foxtrek_64 Date: Sun, 1 Jun 2025 10:52:48 -0500 Subject: [PATCH 4/8] Add missing 'this' qualifier --- Remora.Plugins/PluginTreeNodeBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Remora.Plugins/PluginTreeNodeBuilder.cs b/Remora.Plugins/PluginTreeNodeBuilder.cs index 72ee021..86254c8 100644 --- a/Remora.Plugins/PluginTreeNodeBuilder.cs +++ b/Remora.Plugins/PluginTreeNodeBuilder.cs @@ -72,7 +72,7 @@ public void AddDependent(PluginTreeNodeBuilder dependent) [Pure] public PluginTreeNode Build(IServiceProvider services) { - var plugin = PluginTreeNodeBuilder.BuildPluginDescriptor(services, PluginType); + var plugin = BuildPluginDescriptor(services, this.PluginType); var node = new PluginTreeNode(plugin); foreach (var dependent in this.Dependents) From aa8f0607fc68d876d74dc7c61693414583cbaee3 Mon Sep 17 00:00:00 2001 From: Foxtrek_64 Date: Sun, 1 Jun 2025 13:05:13 -0500 Subject: [PATCH 5/8] Update readme, remove service provider parameter from IMigratablePlugin. --- Remora.Plugins.Abstractions/IMigratablePlugin.cs | 3 +-- Remora.Plugins/PluginTree.cs | 5 ++--- Remora.Plugins/README.md | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) 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/PluginTree.cs b/Remora.Plugins/PluginTree.cs index 7cd57b6..cb493e6 100644 --- a/Remora.Plugins/PluginTree.cs +++ b/Remora.Plugins/PluginTree.cs @@ -77,10 +77,9 @@ public async Task InitializeAsync(CancellationToken ct = default) /// /// 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 ( @@ -96,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/README.md b/Remora.Plugins/README.md index 6166247..4aed304 100644 --- a/Remora.Plugins/README.md +++ b/Remora.Plugins/README.md @@ -62,15 +62,16 @@ var serviceCollection = new ServiceCollection(); var pluginTreeBuilder = pluginService.LoadPluginTree(serviceCollection, filter: plugin => plugin.Name.Length > 3); _services = serviceCollection.BuildServiceProvider(); +var pluginTree = pluginTreeBuilder.Build(_services); -var initializePlugins = await pluginTreeBuilder.InitializeAsync(_services, ct); +var initializePlugins = await pluginTree.InitializeAsync(ct); if (!initializePlugins.IsSuccess) { // check initializePlugins.Error to figure out why return; } -var migratePlugins = await pluginTreeBuilder.MigrateAsync(_services, ct); +var migratePlugins = await pluginTree.MigrateAsync(ct); if (!migratePlugins.IsSuccess) { // check migratePlugins.Error to figure out why From c52663df2b6fc9cea73d55f8444b807bbc69f7a0 Mon Sep 17 00:00:00 2001 From: Foxtrek_64 Date: Mon, 2 Jun 2025 09:54:33 -0500 Subject: [PATCH 6/8] Update dotnet.yml Attempted fix for blob issue. --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4909085..cbb53c1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -43,7 +43,7 @@ jobs: - name: Coverage uses: irongut/CodeCoverageSummary@v1.3.0 with: - filename: coverage/**/coverage.cobertura.xml + filename: coverage.cobertura.xml badge: true format: markdown indicators: true From 131f52eb6a0170c5273b3389a5dfb0fd0d9ce517 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Mon, 2 Jun 2025 10:01:25 -0500 Subject: [PATCH 7/8] Revert "Update dotnet.yml" This reverts commit c52663df2b6fc9cea73d55f8444b807bbc69f7a0. --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index cbb53c1..4909085 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -43,7 +43,7 @@ jobs: - name: Coverage uses: irongut/CodeCoverageSummary@v1.3.0 with: - filename: coverage.cobertura.xml + filename: coverage/**/coverage.cobertura.xml badge: true format: markdown indicators: true From 8c173708e3f1554cf73db90056a90550de6cf0e3 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Wed, 11 Jun 2025 08:50:02 -0500 Subject: [PATCH 8/8] Trigger checks