NanoXLSX.Core 3.2.1
Loading...
Searching...
No Matches
PluginLoader.cs
1/*
2 * NanoXLSX is a small .NET library to generate and read XLSX (Microsoft Excel 2007 or newer) files in an easy and native way
3 * Copyright Raphael Stoeckli © 2026
4 * This library is licensed under the MIT License.
5 * You find a copy of the license in project folder or on: http://opensource.org/licenses/MIT
6 */
7
8using System;
9using System.Collections.Generic;
10using System.Diagnostics.CodeAnalysis;
11using System.IO;
12using System.Linq;
13using System.Reflection;
17
18namespace NanoXLSX.Registry
19{
23 public static class PlugInLoader
24 {
25 private static bool initialized;
26 private static readonly object _lock = new object();
27
28 private static readonly Dictionary<string, PlugInInstance> plugInClasses = new Dictionary<string, PlugInInstance>();
29 private static readonly Dictionary<string, List<PlugInInstance>> queuePlugInClasses = new Dictionary<string, List<PlugInInstance>>();
30
35 public static bool Initialize()
36 {
37
38 if (initialized)
39 {
40 return false;
41 }
42 lock (_lock)
43 {
44 LoadReferencedAssemblies();
45 initialized = true;
46 return initialized;
47 }
48 }
49
53 [ExcludeFromCodeCoverage] // Indirectly tested by InjectPlugins
54 private static void LoadReferencedAssemblies()
55 {
56 Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
57
58 // 1. Register plugins from already-loaded assemblies
59 foreach (Assembly assembly in loadedAssemblies)
60 {
61 if (!assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location))
62 {
63 try
64 {
65 RegisterPlugIns(assembly);
66 }
67 catch (Exception ex)
68 {
69 System.Diagnostics.Debug.WriteLine($"Failed to register plugins from {assembly.Location}: {ex.Message}");
70 }
71 }
72 }
73
74 // 2. Load any remaining DLLs that haven't been loaded yet
75 IEnumerable<string> allLoadedPaths = loadedAssemblies
76 .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
77 .Select(a => a.Location);
78 HashSet<string> loadedPaths = new HashSet<string>(allLoadedPaths, StringComparer.InvariantCultureIgnoreCase);
79
80 // Guard the assembly-directory enumeration. Directory.GetFiles can throw (e.g. IOException
81 // "The parameter is incorrect" when BaseDirectory contains an entry the native enumerator
82 // cannot stat). Phase 1 above already registered plugins from loaded assemblies,
83 // so on enumeration failure, the already processed result is returned rather than crashing.
84 List<string> referencedPaths;
85 try
86 {
87 referencedPaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
88 .Where(path => !loadedPaths.Contains(path))
89 .ToList();
90 }
91 catch (Exception ex)
92 {
93 System.Diagnostics.Debug.WriteLine($"Failed to enumerate assemblies in {AppDomain.CurrentDomain.BaseDirectory}: {ex.Message}");
94 return;
95 }
96
97 foreach (string path in referencedPaths)
98 {
99 try
100 {
101 Assembly assembly = Assembly.LoadFrom(path);
102 RegisterPlugIns(assembly);
103 }
104 catch (Exception ex)
105 {
106 System.Diagnostics.Debug.WriteLine($"Failed to load assembly: {path} - {ex.Message}");
107 }
108 }
109 }
110
116 internal static void InjectPlugins(List<Type> pluginTypes)
117 {
118 lock (_lock)
119 {
120 // Collect all types decorated with the NanoXlsxPlugInAttribute.
121 IEnumerable<Type> replacingPluginTypes = pluginTypes
122 .Where(t => t.GetCustomAttribute<NanoXlsxPlugInAttribute>() != null);
123
124 // Collect all types decorated with the NanoXlsxQueuePlugInAttribute.
125 IEnumerable<Type> queuePluginTypes = pluginTypes
126 .Where(t => t.GetCustomAttributes<NanoXlsxQueuePlugInAttribute>().Any());
127
128 // Pass the collected types to the appropriate handlers.
129 HandleReplacingPlugIns(replacingPluginTypes);
130 HandleQueuePlugIns(queuePluginTypes);
131 initialized = true;
132 }
133 }
134
139 internal static void DisposePlugins()
140 {
141 lock (_lock)
142 {
143 plugInClasses.Clear();
144 queuePlugInClasses.Clear();
145 initialized = false;
146 }
147 }
148
153 [ExcludeFromCodeCoverage] // Indirectly tested by InjectPlugins
154 private static void RegisterPlugIns(Assembly assembly)
155 {
156 IEnumerable<Type> replacingPlugInTypes = GetAssemblyPlugInsByType(assembly, typeof(NanoXlsxPlugInAttribute));
157 IEnumerable<Type> queuePlugInTypes = GetAssemblyPlugInsByType(assembly, typeof(NanoXlsxQueuePlugInAttribute));
158 HandleReplacingPlugIns(replacingPlugInTypes);
159 HandleQueuePlugIns(queuePlugInTypes);
160 }
161
162
169 [ExcludeFromCodeCoverage] // Indirectly tested by InjectPlugins
170 private static List<Type> GetAssemblyPlugInsByType(Assembly assembly, Type attributeType)
171 {
172 List<Type> plugInTypes = new List<Type>();
173 Type plugInInterface = typeof(IPlugin);
174 Type[] allTypes = assembly.GetTypes();
175
176 for (int i = 0; i < allTypes.Length; i++)
177 {
178 Type type = allTypes[i];
179 if (type.IsClass && !type.IsAbstract &&
180 plugInInterface.IsAssignableFrom(type) &&
181 type.GetCustomAttribute(attributeType) != null)
182 {
183 plugInTypes.Add(type);
184 }
185 }
186 return plugInTypes;
187 }
188
193 [ExcludeFromCodeCoverage] // Indirectly tested by InjectPlugins
194 private static void HandleReplacingPlugIns(IEnumerable<Type> plugInTypes)
195 {
196 foreach (Type plugInType in plugInTypes)
197 {
198 IEnumerable<NanoXlsxPlugInAttribute> attributes = plugInType.GetCustomAttributes<NanoXlsxPlugInAttribute>();
199 foreach (NanoXlsxPlugInAttribute attribute in attributes)
200 {
201 if (plugInClasses.ContainsKey(attribute.PlugInUUID))
202 {
203 if (attribute.PlugInOrder >= plugInClasses[attribute.PlugInUUID].Order)
204 {
205 // Skip duplicates with lower order numbers
206 plugInClasses[attribute.PlugInUUID] = new PlugInInstance(attribute.PlugInUUID, attribute.PlugInOrder, plugInType);
207 }
208 }
209 else if (!plugInClasses.ContainsKey(attribute.PlugInUUID))
210 {
211 plugInClasses.Add(attribute.PlugInUUID, new PlugInInstance(attribute.PlugInUUID, attribute.PlugInOrder, plugInType));
212 }
213 }
214 }
215 }
216
221 private static void HandleQueuePlugIns(IEnumerable<Type> queuePlugInTypes)
222 {
223 foreach (Type plugInType in queuePlugInTypes)
224 {
225 IEnumerable<NanoXlsxQueuePlugInAttribute> attributes = plugInType.GetCustomAttributes<NanoXlsxQueuePlugInAttribute>();
226 foreach (var attribute in attributes)
227 {
228 if (!queuePlugInClasses.TryGetValue(attribute.QueueUUID, out var value))
229 {
230 value = new List<PlugInInstance>();
231 queuePlugInClasses.Add(attribute.QueueUUID, value);
232 }
233
234 value.Add(new PlugInInstance(attribute.PlugInUUID, attribute.PlugInOrder, plugInType));
235 }
236 }
237 // Sort each list based on PlugInOrder (ascending)
238 foreach (KeyValuePair<string, List<PlugInInstance>> entry in queuePlugInClasses)
239 {
240 entry.Value.Sort((a, b) => a.Order.CompareTo(b.Order));
241 }
242 }
243
253 internal static T GetPlugIn<T>(string plugInUUID, T fallBackInstance)
254 {
255 if (plugInClasses.TryGetValue(plugInUUID, out var plugIn))
256 {
257 return (T)Activator.CreateInstance(plugIn.Type, true);
258 }
259 else
260 {
261 return fallBackInstance;
262 }
263 }
264
270 internal static bool HasQueuePlugins(string queueUUID)
271 {
272 return queuePlugInClasses.TryGetValue(queueUUID, out var list) && list.Count > 0;
273 }
274
285 internal static T GetNextQueuePlugIn<T>(string queueUUID, string lastPlugInUUID, out string currentPlugInUUID)
286 where T : class, IPlugin
287 {
288 return GetNextQueuePlugIn<T>(queueUUID, lastPlugInUUID, out currentPlugInUUID, null);
289 }
290
300 internal static T GetNextQueuePlugIn<T>(string queueUUID, string lastPlugInUUID, out string currentPlugInUUID, IBaseWriter baseWriter)
301 where T : class, IPlugin
302 {
303 currentPlugInUUID = null;
304 if (!queuePlugInClasses.TryGetValue(queueUUID, out var plugInList) || plugInList.Count == 0)
305 {
306 return default;
307 }
308
309 int startIndex = 0;
310 if (lastPlugInUUID != null)
311 {
312 int lastIndex = plugInList.FindIndex(p => p.UUID == lastPlugInUUID);
313 if (lastIndex < 0)
314 {
315 return default;
316 }
317
318 startIndex = lastIndex + 1;
319 }
320
321 Type requestedType = typeof(T);
322 for (int i = startIndex; i < plugInList.Count; i++)
323 {
324 PlugInInstance plugIn = plugInList[i];
325 if (!requestedType.IsAssignableFrom(plugIn.Type))
326 {
327 continue;
328 }
329
330 object instance = null;
331 if (baseWriter != null)
332 {
333 ConstructorInfo contextualConstructor = plugIn.Type
334 .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
335 .FirstOrDefault(constructor =>
336 {
337 ParameterInfo[] parameters = constructor.GetParameters();
338 return parameters.Length == 1 && parameters[0].ParameterType == typeof(IBaseWriter);
339 });
340 if (contextualConstructor != null)
341 {
342 instance = contextualConstructor.Invoke(new object[] { baseWriter });
343 }
344 }
345
346 if (instance == null)
347 {
348 instance = Activator.CreateInstance(plugIn.Type, true);
349 }
350
351 currentPlugInUUID = plugIn.UUID;
352 return (T)instance;
353 }
354
355 return default;
356 }
357
361 private sealed class PlugInInstance
362 {
366 public string UUID { get; private set; }
370 public int Order { get; private set; }
375 public Type Type { get; private set; }
376
383 internal PlugInInstance(string uuid, int order, Type type)
384 {
385 this.UUID = uuid;
386 this.Order = order;
387 this.Type = type;
388 }
389 }
390 }
391}
int PlugInOrder
Order how the annotated plug-ins are registered in case of duplicate UIDs. The higher number will ove...
Class to register plug-in classes that extends the functionality of NanoXLSX (Core or any other packa...
static bool Initialize()
Initializes the plug-in loader process. If already initialized, the method returns without action.