| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="PluginService.cs" company="Stéphane ANDRE"> |
| | | 3 | | // Copyright (c) Stéphane ANDRE. All rights reserved. |
| | | 4 | | // </copyright> |
| | | 5 | | // ----------------------------------------------------------------------- |
| | | 6 | | |
| | | 7 | | using System; |
| | | 8 | | using System.Collections.Concurrent; |
| | | 9 | | using System.Collections.Generic; |
| | | 10 | | using System.IO; |
| | | 11 | | using System.Linq; |
| | | 12 | | using System.Reflection; |
| | | 13 | | |
| | | 14 | | namespace MyNet.Utilities.Plugins; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Low-level, stateless helpers for discovering, loading, and instantiating plugin types. |
| | | 18 | | /// <para> |
| | | 19 | | /// Convention: a plugin lives in a subdirectory whose name matches the DLL file it contains. |
| | | 20 | | /// For example, a plugin directory <c>plugins/MyPlugin/</c> must contain <c>MyPlugin.dll</c>. |
| | | 21 | | /// </para> |
| | | 22 | | /// <para> |
| | | 23 | | /// Loaded assemblies are cached by their full path so the same DLL is never loaded into |
| | | 24 | | /// more than one <see cref="PluginLoadContext"/>, preserving type identity across calls. |
| | | 25 | | /// </para> |
| | | 26 | | /// </summary> |
| | | 27 | | public static class PluginService |
| | | 28 | | { |
| | | 29 | | /// <summary> |
| | | 30 | | /// Process-wide cache of successfully loaded plugin assemblies, keyed by the |
| | | 31 | | /// normalised (lower-case, full) DLL path. Prevents loading the same file into |
| | | 32 | | /// multiple <see cref="PluginLoadContext"/> instances, which would make types from |
| | | 33 | | /// different contexts incompatible with each other even when they come from the same DLL. |
| | | 34 | | /// </summary> |
| | 0 | 35 | | private static readonly ConcurrentDictionary<string, Assembly> AssemblyCache = |
| | 0 | 36 | | new(StringComparer.OrdinalIgnoreCase); |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// Scans every subdirectory of <paramref name="pluginsDirectory"/> for a conventionally |
| | | 40 | | /// named DLL and returns all concrete (non-abstract, non-interface) types that |
| | | 41 | | /// implement or inherit <typeparamref name="T"/>. |
| | | 42 | | /// </summary> |
| | | 43 | | /// <typeparam name="T">The contract type (base class or interface) to look for.</typeparam> |
| | | 44 | | /// <param name="pluginsDirectory"> |
| | | 45 | | /// The root directory whose immediate subdirectories are treated as plugin packages. |
| | | 46 | | /// </param> |
| | | 47 | | /// <returns> |
| | | 48 | | /// A flat, deduplicated sequence of matching concrete types across all loaded plugins. |
| | | 49 | | /// Returns an empty sequence when the directory does not exist or contains no valid plugins. |
| | | 50 | | /// </returns> |
| | | 51 | | public static IEnumerable<Type> GetTypes<T>(string pluginsDirectory) |
| | | 52 | | { |
| | 0 | 53 | | if (!Directory.Exists(pluginsDirectory)) return []; |
| | | 54 | | |
| | 0 | 55 | | var result = new List<Type>(); |
| | | 56 | | |
| | 0 | 57 | | foreach (var subdirectory in new DirectoryInfo(pluginsDirectory).GetDirectories()) |
| | | 58 | | { |
| | 0 | 59 | | var dllPath = Path.Combine(subdirectory.FullName, $"{subdirectory.Name}.dll"); |
| | | 60 | | |
| | 0 | 61 | | if (!File.Exists(dllPath)) continue; |
| | | 62 | | |
| | 0 | 63 | | var assembly = LoadAssembly(dllPath); |
| | 0 | 64 | | if (assembly is null) continue; |
| | | 65 | | |
| | 0 | 66 | | var matchingTypes = assembly.GetTypes() |
| | 0 | 67 | | .Where(t => t is { IsAbstract: false, IsInterface: false } |
| | 0 | 68 | | && t.IsAssignableTo(typeof(T))); |
| | 0 | 69 | | result.AddRange(matchingTypes); |
| | | 70 | | } |
| | | 71 | | |
| | 0 | 72 | | return result; |
| | | 73 | | } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Loads the assembly located at <paramref name="pluginPath"/> and returns the first |
| | | 77 | | /// concrete type that implements or inherits <typeparamref name="T"/>. |
| | | 78 | | /// </summary> |
| | | 79 | | /// <typeparam name="T">The contract type (base class or interface) to look for.</typeparam> |
| | | 80 | | /// <param name="pluginPath">The full path to the plugin DLL file.</param> |
| | | 81 | | /// <returns> |
| | | 82 | | /// The first matching concrete <see cref="Type"/>, or <c>null</c> when the file does |
| | | 83 | | /// not exist, cannot be loaded, or contains no suitable type. |
| | | 84 | | /// </returns> |
| | | 85 | | public static Type? FindType<T>(string pluginPath) |
| | | 86 | | { |
| | 0 | 87 | | if (string.IsNullOrEmpty(pluginPath)) return null; |
| | | 88 | | |
| | 0 | 89 | | var assembly = LoadAssembly(pluginPath); |
| | 0 | 90 | | return assembly?.GetTypes() |
| | 0 | 91 | | .FirstOrDefault(t => t is { IsAbstract: false, IsInterface: false } |
| | 0 | 92 | | && t.IsAssignableTo(typeof(T))); |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// Loads the assembly at <paramref name="pluginPath"/>, locates the first concrete type |
| | | 97 | | /// implementing <typeparamref name="T"/>, and creates an instance of it using |
| | | 98 | | /// <see cref="Activator.CreateInstance(Type, object[])"/>. |
| | | 99 | | /// </summary> |
| | | 100 | | /// <typeparam name="T">The expected type of the created instance.</typeparam> |
| | | 101 | | /// <param name="pluginPath">The full path to the plugin DLL file.</param> |
| | | 102 | | /// <param name="constructorParameters"> |
| | | 103 | | /// Optional arguments forwarded to the constructor of the discovered type. |
| | | 104 | | /// </param> |
| | | 105 | | /// <returns> |
| | | 106 | | /// A new instance of <typeparamref name="T"/>, or <c>null</c> if no matching type |
| | | 107 | | /// was found or instantiation failed. |
| | | 108 | | /// </returns> |
| | | 109 | | public static T? CreateInstance<T>(string pluginPath, params object[] constructorParameters) |
| | | 110 | | { |
| | 0 | 111 | | if (string.IsNullOrEmpty(pluginPath)) return default; |
| | | 112 | | |
| | 0 | 113 | | var type = FindType<T>(pluginPath); |
| | 0 | 114 | | return type is null ? default : (T?)Activator.CreateInstance(type, constructorParameters); |
| | | 115 | | } |
| | | 116 | | |
| | | 117 | | /// <summary> |
| | | 118 | | /// Returns the cached <see cref="Assembly"/> for <paramref name="dllPath"/>, loading it |
| | | 119 | | /// on first access via a dedicated <see cref="PluginLoadContext"/>. |
| | | 120 | | /// </summary> |
| | | 121 | | /// <param name="dllPath">The full, absolute path to the DLL file.</param> |
| | | 122 | | /// <returns> |
| | | 123 | | /// The loaded <see cref="Assembly"/>, or <c>null</c> when the file is missing or the |
| | | 124 | | /// load operation throws (the exception is swallowed and the entry is not cached so |
| | | 125 | | /// that a corrected DLL can be picked up on the next call). |
| | | 126 | | /// </returns> |
| | | 127 | | private static Assembly? LoadAssembly(string dllPath) |
| | | 128 | | { |
| | 0 | 129 | | if (!File.Exists(dllPath)) return null; |
| | | 130 | | |
| | 0 | 131 | | if (AssemblyCache.TryGetValue(dllPath, out var cached)) return cached; |
| | | 132 | | |
| | | 133 | | try |
| | | 134 | | { |
| | 0 | 135 | | var assemblyName = Path.GetFileNameWithoutExtension(dllPath); |
| | 0 | 136 | | var loadContext = new PluginLoadContext(dllPath); |
| | 0 | 137 | | var assembly = loadContext.LoadFromAssemblyName(new(assemblyName)); |
| | | 138 | | |
| | | 139 | | // Only cache on success so a broken DLL can be replaced and retried. |
| | 0 | 140 | | AssemblyCache.TryAdd(dllPath, assembly); |
| | 0 | 141 | | return assembly; |
| | | 142 | | } |
| | 0 | 143 | | catch (Exception ex) when (ex is FileLoadException or BadImageFormatException or FileNotFoundException) |
| | | 144 | | { |
| | | 145 | | // The DLL exists on disk but could not be loaded (wrong architecture, missing |
| | | 146 | | // dependencies, corrupted file …). Log-worthy but not fatal for the host. |
| | 0 | 147 | | return null; |
| | | 148 | | } |
| | 0 | 149 | | } |
| | | 150 | | } |
| | | 151 | | |