< Summary

Information
Class: MyNet.Utilities.Plugins.PluginService
Assembly: MyNet.Utilities
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Plugins/PluginService.cs
Tag: 323_28699572109
Line coverage
0%
Covered lines: 0
Uncovered lines: 32
Coverable lines: 32
Total lines: 151
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 24
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
GetTypes(...)0%110100%
FindType(...)0%4260%
CreateInstance(...)0%2040%
LoadAssembly(...)0%2040%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Plugins/PluginService.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="PluginService.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Concurrent;
 9using System.Collections.Generic;
 10using System.IO;
 11using System.Linq;
 12using System.Reflection;
 13
 14namespace 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>
 27public 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>
 035    private static readonly ConcurrentDictionary<string, Assembly> AssemblyCache =
 036        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    {
 053        if (!Directory.Exists(pluginsDirectory)) return [];
 54
 055        var result = new List<Type>();
 56
 057        foreach (var subdirectory in new DirectoryInfo(pluginsDirectory).GetDirectories())
 58        {
 059            var dllPath = Path.Combine(subdirectory.FullName, $"{subdirectory.Name}.dll");
 60
 061            if (!File.Exists(dllPath)) continue;
 62
 063            var assembly = LoadAssembly(dllPath);
 064            if (assembly is null) continue;
 65
 066            var matchingTypes = assembly.GetTypes()
 067                                        .Where(t => t is { IsAbstract: false, IsInterface: false }
 068                                                    && t.IsAssignableTo(typeof(T)));
 069            result.AddRange(matchingTypes);
 70        }
 71
 072        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    {
 087        if (string.IsNullOrEmpty(pluginPath)) return null;
 88
 089        var assembly = LoadAssembly(pluginPath);
 090        return assembly?.GetTypes()
 091                        .FirstOrDefault(t => t is { IsAbstract: false, IsInterface: false }
 092                                             && 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    {
 0111        if (string.IsNullOrEmpty(pluginPath)) return default;
 112
 0113        var type = FindType<T>(pluginPath);
 0114        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    {
 0129        if (!File.Exists(dllPath)) return null;
 130
 0131        if (AssemblyCache.TryGetValue(dllPath, out var cached)) return cached;
 132
 133        try
 134        {
 0135            var assemblyName = Path.GetFileNameWithoutExtension(dllPath);
 0136            var loadContext = new PluginLoadContext(dllPath);
 0137            var assembly = loadContext.LoadFromAssemblyName(new(assemblyName));
 138
 139            // Only cache on success so a broken DLL can be replaced and retried.
 0140            AssemblyCache.TryAdd(dllPath, assembly);
 0141            return assembly;
 142        }
 0143        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.
 0147            return null;
 148        }
 0149    }
 150}
 151