613 lines
24 KiB
C#
613 lines
24 KiB
C#
// <copyright file="PlugInManager.cs" company="MUnique">
|
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
// </copyright>
|
|
|
|
namespace MUnique.OpenMU.PlugIns;
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.ComponentModel.Design;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Nito.Disposables.Internals;
|
|
|
|
/// <summary>
|
|
/// The manager for plugins.
|
|
/// </summary>
|
|
public class PlugInManager
|
|
{
|
|
private readonly ILogger<PlugInManager> _logger;
|
|
private readonly ServiceContainer _serviceContainer;
|
|
private readonly IDictionary<Type, object> _plugInPoints = new Dictionary<Type, object>();
|
|
private readonly IDictionary<Guid, Type> _knownPlugIns = new ConcurrentDictionary<Guid, Type>();
|
|
private readonly ConcurrentDictionary<Type, ISet<Type>> _knownPlugInsPerInterfaceType = new();
|
|
private readonly ConcurrentDictionary<Guid, Type> _activePlugIns = new();
|
|
private object? _lastCreatedPlugIn;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PlugInManager" /> class.
|
|
/// </summary>
|
|
/// <param name="configurations">The configurations.</param>
|
|
/// <param name="loggerFactory">The logger factory.</param>
|
|
/// <param name="serviceProvider">The service provider.</param>
|
|
/// <param name="customConfigReferenceHandler">The reference handler for references in custom plugin configurations.</param>
|
|
public PlugInManager(ICollection<PlugInConfiguration>? configurations, ILoggerFactory loggerFactory, IServiceProvider? serviceProvider, ReferenceHandler? customConfigReferenceHandler)
|
|
{
|
|
_ = typeof(Nito.AsyncEx.AsyncReaderWriterLock); // Ensure Nito.AsyncEx.Coordination is loaded so it will be available in proxy generation.
|
|
|
|
this._logger = loggerFactory.CreateLogger<PlugInManager>();
|
|
this._serviceContainer = new ServiceContainer(serviceProvider);
|
|
this._serviceContainer.AddService(typeof(PlugInManager), this);
|
|
this._serviceContainer.AddService(typeof(ILoggerFactory), loggerFactory);
|
|
|
|
this.CustomConfigReferenceHandler = customConfigReferenceHandler;
|
|
|
|
if (configurations is not null)
|
|
{
|
|
this.DiscoverAndRegisterPlugIns();
|
|
var loadedAssemblies = new HashSet<string>();
|
|
foreach (var configuration in configurations)
|
|
{
|
|
this.ReadConfiguration(configuration, loadedAssemblies);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when a plugin got deactivated.
|
|
/// </summary>
|
|
public event EventHandler<PlugInEventArgs>? PlugInDeactivated;
|
|
|
|
/// <summary>
|
|
/// Occurs when a plugin got activated.
|
|
/// </summary>
|
|
public event EventHandler<PlugInEventArgs>? PlugInActivated;
|
|
|
|
/// <summary>
|
|
/// Occurs when the <see cref="PlugInConfiguration.CustomConfiguration"/> has been changed.
|
|
/// </summary>
|
|
public event EventHandler<PlugInConfigurationChangedEventArgs>? PlugInConfigurationChanged;
|
|
|
|
/// <summary>
|
|
/// Gets the known plugin types.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The known plugin types.
|
|
/// </value>
|
|
public IEnumerable<Type> KnownPlugInTypes => this._knownPlugIns.Values;
|
|
|
|
/// <summary>
|
|
/// Gets the reference handler for references in custom plugin configurations.
|
|
/// </summary>
|
|
public ReferenceHandler? CustomConfigReferenceHandler { get; }
|
|
|
|
/// <summary>
|
|
/// Discovers and registers all plugins of all loaded assemblies.
|
|
/// </summary>
|
|
public void DiscoverAndRegisterPlugIns()
|
|
{
|
|
var plugIns = this.DiscoverNewPlugIns();
|
|
this.ValidateNoDuplicateGuids(plugIns);
|
|
this.RegisterPlugIns(plugIns);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Discovers and registers plugins of the specified assembly.
|
|
/// </summary>
|
|
/// <param name="assembly">The assembly.</param>
|
|
public void DiscoverAndRegisterPlugIns(Assembly assembly)
|
|
{
|
|
var plugIns = this.DiscoverNewPlugIns(this.DiscoverPlugIns(assembly));
|
|
this.ValidateNoDuplicateGuids(plugIns);
|
|
this.RegisterPlugIns(plugIns);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Discovers and register plugins of type <typeparamref name="T"/>.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of the plugins that should be discovered.</typeparam>
|
|
public void DiscoverAndRegisterPlugInsOf<T>()
|
|
{
|
|
var plugIns = this.DiscoverAllPlugIns().Where(type => typeof(T).IsAssignableFrom(type));
|
|
this.RegisterPlugIns(plugIns);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the known plugins of the given interface type.
|
|
/// </summary>
|
|
/// <typeparam name="T">The plugin interface type. Type parameter of a <see cref="CustomPlugInContainerBase{TPlugIn}"/>.</typeparam>
|
|
/// <returns>The known plugins of the given interface type.</returns>
|
|
public IEnumerable<Type> GetKnownPlugInsOf<T>()
|
|
{
|
|
if (this._knownPlugInsPerInterfaceType.TryGetValue(typeof(T), out var result))
|
|
{
|
|
return result;
|
|
}
|
|
|
|
return this._knownPlugIns.Values.Where(p => typeof(T).IsAssignableFrom(p));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the active plugins of the specified type.
|
|
/// </summary>
|
|
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
|
/// <returns>The active plugins of the specified type.</returns>
|
|
public IEnumerable<TPlugIn> GetActivePlugInsOf<TPlugIn>()
|
|
{
|
|
if (this._plugInPoints.TryGetValue(typeof(TPlugIn), out var point) && point is IPlugInContainer<TPlugIn> container)
|
|
{
|
|
return container.ActivePlugIns;
|
|
}
|
|
|
|
return Enumerable.Empty<TPlugIn>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deactivates the plugin of type <typeparamref name="TPlugIn"/>.
|
|
/// </summary>
|
|
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
|
public void DeactivatePlugIn<TPlugIn>() => this.DeactivatePlugIn(typeof(TPlugIn));
|
|
|
|
/// <summary>
|
|
/// Deactivates the plugin of the specified type.
|
|
/// </summary>
|
|
/// <param name="plugIn">The plugin.</param>
|
|
public void DeactivatePlugIn(Type plugIn) => this.DeactivatePlugIn(plugIn.GUID);
|
|
|
|
/// <summary>
|
|
/// Deactivates the plugin with the specified type id.
|
|
/// </summary>
|
|
/// <param name="plugInId">The plugin identifier.</param>
|
|
public void DeactivatePlugIn(Guid plugInId)
|
|
{
|
|
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
|
|
{
|
|
this._activePlugIns.TryRemove(plugInId, out _);
|
|
this.PlugInDeactivated?.Invoke(this, new PlugInEventArgs(plugInType));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Activates the plugin of type <typeparamref name="TPlugIn"/>.
|
|
/// </summary>
|
|
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
|
public void ActivatePlugIn<TPlugIn>() => this.ActivatePlugIn(typeof(TPlugIn));
|
|
|
|
/// <summary>
|
|
/// Activates the plugin of the specified type.
|
|
/// </summary>
|
|
/// <param name="plugIn">The plugin.</param>
|
|
public void ActivatePlugIn(Type plugIn) => this.ActivatePlugIn(plugIn.GUID);
|
|
|
|
/// <summary>
|
|
/// Activates the plugin with the specified type id.
|
|
/// </summary>
|
|
/// <param name="plugInId">The plugin identifier.</param>
|
|
public void ActivatePlugIn(Guid plugInId)
|
|
{
|
|
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
|
|
{
|
|
this._activePlugIns.TryAdd(plugInId, plugInType);
|
|
this.PlugInActivated?.Invoke(this, new PlugInEventArgs(plugInType));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the plugin point which implements <typeparamref name="TPlugIn"/>.
|
|
/// </summary>
|
|
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
|
/// <returns>The plugin point which implements <typeparamref name="TPlugIn"/>, if available; Otherwise, <c>null</c>.</returns>
|
|
public TPlugIn? GetPlugInPoint<TPlugIn>()
|
|
where TPlugIn : class
|
|
{
|
|
if (this._plugInPoints.TryGetValue(typeof(TPlugIn), out var obj) && obj is TPlugIn plugIn)
|
|
{
|
|
return plugIn;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the strategy plugin.
|
|
/// </summary>
|
|
/// <typeparam name="TKey">The type of the key.</typeparam>
|
|
/// <typeparam name="TStrategy">The type of the strategy.</typeparam>
|
|
/// <returns>The strategy plugin.</returns>
|
|
public IStrategyPlugInProvider<TKey, TStrategy>? GetStrategyProvider<TKey, TStrategy>()
|
|
where TStrategy : class, IStrategyPlugIn<TKey>
|
|
{
|
|
if (this._plugInPoints.TryGetValue(typeof(TStrategy), out var obj) && obj is IStrategyPlugInProvider<TKey, TStrategy> plugIn)
|
|
{
|
|
return plugIn;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the strategy plugin.
|
|
/// </summary>
|
|
/// <typeparam name="TKey">The type of the key.</typeparam>
|
|
/// <typeparam name="TStrategy">The type of the strategy.</typeparam>
|
|
/// <param name="key">The key.</param>
|
|
/// <returns>The strategy plugin of the specified key, if available; Otherwise, <c>null</c>.</returns>
|
|
public TStrategy? GetStrategy<TKey, TStrategy>(TKey key)
|
|
where TStrategy : class, IStrategyPlugIn<TKey>
|
|
{
|
|
return this.GetStrategyProvider<TKey, TStrategy>()?[key];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the strategy plugin.
|
|
/// </summary>
|
|
/// <typeparam name="TStrategy">The type of the strategy.</typeparam>
|
|
/// <param name="key">The key.</param>
|
|
/// <returns>The strategy plugin of the specified key, if available; Otherwise, <c>null</c>.</returns>
|
|
public TStrategy? GetStrategy<TStrategy>(string key)
|
|
where TStrategy : class, IStrategyPlugIn<string>
|
|
{
|
|
return this.GetStrategy<string, TStrategy>(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the specified plugin type is configured as active.
|
|
/// </summary>
|
|
/// <param name="plugInType">Type of the plugin.</param>
|
|
/// <returns>
|
|
/// <c>true</c> if the specified plugin type is configured as active; otherwise, <c>false</c>.
|
|
/// </returns>
|
|
public bool IsPlugInActive(Type plugInType)
|
|
{
|
|
return this.IsPlugInActive(plugInType.GUID);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the specified plugin type is configured as active.
|
|
/// </summary>
|
|
/// <param name="plugInTypeId">Identifier of the type of the plugin.</param>
|
|
/// <returns>
|
|
/// <c>true</c> if the specified plugin type is configured as active; otherwise, <c>false</c>.
|
|
/// </returns>
|
|
public bool IsPlugInActive(Guid plugInTypeId)
|
|
{
|
|
return this._activePlugIns.ContainsKey(plugInTypeId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the plugin class for the specified plugin interface.
|
|
/// </summary>
|
|
/// <typeparam name="TPlugInInterface">The type of the plugin interface.</typeparam>
|
|
/// <typeparam name="TPlugInClass">The type of the plugin class.</typeparam>
|
|
public void RegisterPlugIn<TPlugInInterface, TPlugInClass>()
|
|
where TPlugInInterface : class
|
|
where TPlugInClass : class, TPlugInInterface
|
|
{
|
|
this.RegisterPlugInType(typeof(TPlugInClass));
|
|
if (typeof(TPlugInInterface).GetCustomAttribute(typeof(PlugInPointAttribute)) != null)
|
|
{
|
|
var plugIn = this._lastCreatedPlugIn as TPlugInClass ?? this.CreatePlugInInstance<TPlugInClass>();
|
|
this._lastCreatedPlugIn = plugIn;
|
|
this.RegisterPlugInAtPlugInPoint<TPlugInInterface>(plugIn);
|
|
}
|
|
else if (this.GetCustomPlugInPointType(typeof(TPlugInInterface)) is { } customPlugInPointType)
|
|
{
|
|
if (!this._knownPlugInsPerInterfaceType.TryGetValue(customPlugInPointType, out var plugInList))
|
|
{
|
|
plugInList = new HashSet<Type>();
|
|
this._knownPlugInsPerInterfaceType.TryAdd(customPlugInPointType, plugInList);
|
|
}
|
|
|
|
plugInList.Add(typeof(TPlugInClass));
|
|
this.PlugInActivated?.Invoke(this, new PlugInEventArgs(typeof(TPlugInClass)));
|
|
}
|
|
else
|
|
{
|
|
this._logger.LogWarning("Plugin {PlugInClass} wasn't registered, because it's not an implementation of an interface which is marked with PlugInPointAttribute or CustomPlugInContainerAttribute.", typeof(TPlugInClass));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the plugin instance for the specified plugin point interface.
|
|
/// </summary>
|
|
/// <typeparam name="TPlugInInterface">The type of the plugin interface.</typeparam>
|
|
/// <param name="instance">The instance.</param>
|
|
/// <exception cref="ArgumentException">Plugin Type {instance.GetType()} - instance.</exception>
|
|
public void RegisterPlugInAtPlugInPoint<TPlugInInterface>(TPlugInInterface instance)
|
|
where TPlugInInterface : class
|
|
{
|
|
if (instance.GetType().GetCustomAttribute<GuidAttribute>() is null)
|
|
{
|
|
throw new ArgumentException($"Plugin Type {instance.GetType()} is missing a {typeof(GuidAttribute)}. It's required to identify the plugin in the configuration. Otherwise, the plugin would get a different Guid every time it gets compiled.", nameof(instance));
|
|
}
|
|
|
|
if (!this._plugInPoints.TryGetValue(typeof(TPlugInInterface), out var point))
|
|
{
|
|
var proxy = this.CreateProxy<TPlugInInterface>();
|
|
proxy.AddPlugIn(instance, true);
|
|
this._plugInPoints.Add(typeof(TPlugInInterface), proxy);
|
|
}
|
|
else if (point is IPlugInContainer<TPlugInInterface> proxy)
|
|
{
|
|
proxy.AddPlugIn(instance, true);
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidPlugInProxyException(point.GetType(), typeof(IPlugInContainer<TPlugInInterface>));
|
|
}
|
|
|
|
this.RegisterPlugInType(instance.GetType());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures the plugin.
|
|
/// </summary>
|
|
/// <param name="plugInId">The plugin identifier.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
public void ConfigurePlugIn(Guid plugInId, PlugInConfiguration configuration)
|
|
{
|
|
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
|
|
{
|
|
this.ConfigurePlugIn(plugInType, configuration);
|
|
}
|
|
}
|
|
|
|
private void ValidateNoDuplicateGuids(IEnumerable<Type> plugIns)
|
|
{
|
|
var allPlugIns = plugIns.Concat(this._knownPlugIns.Values).Distinct();
|
|
var duplicates = allPlugIns.GroupBy(t => t.GUID).Where(g => g.Count() > 1).ToList();
|
|
if (duplicates.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var message = string.Join("; ", duplicates.Select(g =>
|
|
$"{g.Key} used by {string.Join(", ", g.Select(t => t.FullName ?? t.Name))}"));
|
|
throw new DuplicatePlugInGuidException(message);
|
|
}
|
|
|
|
private Type? GetCustomPlugInPointType(Type interfaceType)
|
|
{
|
|
if (interfaceType.GetCustomAttribute<CustomPlugInContainerAttribute>() != null)
|
|
{
|
|
return interfaceType;
|
|
}
|
|
|
|
return interfaceType.GetInterfaces().FirstOrDefault(i => i.GetCustomAttribute<CustomPlugInContainerAttribute>() != null);
|
|
}
|
|
|
|
private void RegisterPlugInType(Type plugInType)
|
|
{
|
|
var plugInTypeId = plugInType.GUID;
|
|
if (!this._knownPlugIns.ContainsKey(plugInTypeId))
|
|
{
|
|
this._knownPlugIns.Add(plugInTypeId, plugInType);
|
|
this._activePlugIns.TryAdd(plugInTypeId, plugInType); // registered plugins are by default active
|
|
this._logger.LogDebug("Added known plugin {0}, {1}", plugInTypeId, plugInType);
|
|
}
|
|
}
|
|
|
|
private IPlugInContainer<TPlugInInterface> CreateProxy<TPlugInInterface>()
|
|
where TPlugInInterface : class
|
|
{
|
|
IPlugInContainer<TPlugInInterface> proxy;
|
|
var strategyPlugInInterface = typeof(TPlugInInterface).GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IStrategyPlugIn<>));
|
|
if (strategyPlugInInterface != null)
|
|
{
|
|
var keyType = strategyPlugInInterface.GetGenericArguments()[0];
|
|
var providerType = typeof(StrategyPlugInProvider<,>).MakeGenericType(keyType, typeof(TPlugInInterface));
|
|
proxy = (IPlugInContainer<TPlugInInterface>)ActivatorUtilities.CreateInstance(this._serviceContainer, providerType);
|
|
}
|
|
else
|
|
{
|
|
var proxyGenerator = new PlugInProxyTypeGenerator();
|
|
proxy = proxyGenerator.GenerateProxy<TPlugInInterface>(this);
|
|
}
|
|
|
|
return proxy;
|
|
}
|
|
|
|
private TPlugInClass CreatePlugInInstance<TPlugInClass>()
|
|
{
|
|
return ActivatorUtilities.CreateInstance<TPlugInClass>(this._serviceContainer);
|
|
}
|
|
|
|
private void ReadConfiguration(PlugInConfiguration configuration, HashSet<string> loadedAssemblies)
|
|
{
|
|
if (!this._knownPlugIns.ContainsKey(configuration.TypeId))
|
|
{
|
|
if (!string.IsNullOrEmpty(configuration.ExternalAssemblyName)
|
|
&& !loadedAssemblies.Contains(configuration.ExternalAssemblyName.ToLower()))
|
|
{
|
|
loadedAssemblies.Add(configuration.ExternalAssemblyName.ToLower());
|
|
|
|
try
|
|
{
|
|
var assembly = Assembly.LoadFile("plugins\\" + configuration.ExternalAssemblyName);
|
|
this.DiscoverAndRegisterPlugIns(assembly);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
this._logger.LogError(e, "Error while loading external plugin assembly {ExternalAssemblyName} for plugin {TypeId}.", configuration.ExternalAssemblyName, configuration.TypeId);
|
|
return;
|
|
}
|
|
}
|
|
else if (!string.IsNullOrEmpty(configuration.CustomPlugInSource))
|
|
{
|
|
this._logger.LogWarning("Custom plugin source found at plugin configuration: {Configuration}", configuration);
|
|
/* TODO: Implement code signing, if we really need this feature.
|
|
Assembly customPlugInAssembly = this.CompileCustomPlugInAssembly(configuration);
|
|
this.DiscoverAndRegisterPlugIns(customPlugInAssembly);*/
|
|
}
|
|
else
|
|
{
|
|
// nothing we can do
|
|
}
|
|
}
|
|
|
|
if (this._knownPlugIns.TryGetValue(configuration.TypeId, out var plugInType))
|
|
{
|
|
if (!configuration.IsActive)
|
|
{
|
|
this.DeactivatePlugIn(plugInType);
|
|
}
|
|
|
|
this.ConfigurePlugIn(plugInType, configuration);
|
|
|
|
// When the IsActive property changed, we activate/deactivate accordingly.
|
|
// Currently, property changes are only fired for IsActive, so we don't need to check it.
|
|
configuration.PropertyChanged += (sender, args) => this.OnConfigurationChanged(configuration, plugInType, args.PropertyName);
|
|
}
|
|
else
|
|
{
|
|
this._logger.LogWarning("Unknown plugin type for id {TypeId}", configuration.TypeId);
|
|
}
|
|
}
|
|
|
|
private void OnConfigurationChanged(PlugInConfiguration configuration, Type plugInType, string? propertyName)
|
|
{
|
|
if (propertyName == nameof(PlugInConfiguration.IsActive))
|
|
{
|
|
if (configuration.IsActive)
|
|
{
|
|
this.ActivatePlugIn(plugInType);
|
|
this.ConfigurePlugIn(plugInType, configuration);
|
|
}
|
|
else
|
|
{
|
|
this.DeactivatePlugIn(plugInType);
|
|
}
|
|
}
|
|
|
|
if (propertyName == nameof(PlugInConfiguration.CustomConfiguration))
|
|
{
|
|
this.ConfigurePlugIn(plugInType, configuration);
|
|
}
|
|
}
|
|
|
|
private void ConfigurePlugIn(Type plugInType, PlugInConfiguration configuration)
|
|
{
|
|
this.PlugInConfigurationChanged?.Invoke(this, new PlugInConfigurationChangedEventArgs(plugInType, configuration));
|
|
}
|
|
|
|
private IEnumerable<Type> DiscoverNewPlugIns()
|
|
{
|
|
return this.DiscoverNewPlugIns(this.DiscoverAllPlugIns());
|
|
}
|
|
|
|
private IEnumerable<Type> DiscoverNewPlugIns(IEnumerable<Type> allPlugIns)
|
|
{
|
|
var newPlugIns = allPlugIns.Where(plugIn => !this._knownPlugIns.ContainsKey(plugIn.GUID)).ToList();
|
|
return newPlugIns;
|
|
}
|
|
|
|
private IEnumerable<Type> DiscoverAllPlugIns()
|
|
{
|
|
return AppDomain.CurrentDomain.GetAssemblies()
|
|
.Where(assembly => assembly.FullName is not null)
|
|
.Where(assembly => !assembly.FullName!.StartsWith("System"))
|
|
.Where(assembly => !assembly.FullName!.StartsWith("Microsoft"))
|
|
.Where(assembly => !assembly.FullName!.StartsWith("Nito"))
|
|
.Where(assembly => !assembly.FullName!.StartsWith("Blazor"))
|
|
.SelectMany(assembly =>
|
|
{
|
|
try
|
|
{
|
|
return assembly.DefinedTypes.Where(type => type.GetCustomAttribute<PlugInAttribute>() != null);
|
|
}
|
|
catch (ReflectionTypeLoadException ex)
|
|
{
|
|
return ex.Types.WhereNotNull();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return Enumerable.Empty<Type>();
|
|
}
|
|
});
|
|
}
|
|
|
|
private IEnumerable<Type> DiscoverPlugIns(Assembly assembly)
|
|
{
|
|
return assembly.DefinedTypes.Where(type => type.GetCustomAttribute<PlugInAttribute>() != null);
|
|
}
|
|
|
|
private void RegisterPlugIns(IEnumerable<Type> plugIns)
|
|
{
|
|
foreach (var plugIn in plugIns)
|
|
{
|
|
try
|
|
{
|
|
// A plugin usually should be small, but it should be possible that one plugin can implement more than one plugin interface.
|
|
// In this case, we need to register it at every plugin point
|
|
var plugInInterfaces = plugIn.GetInterfaces().Where(t => t.IsInterface && (t.GetCustomAttribute<PlugInPointAttribute>() != null || t.GetCustomAttribute<CustomPlugInContainerAttribute>() != null));
|
|
foreach (var plugInInterface in plugInInterfaces)
|
|
{
|
|
var genericMethod = this.GetType().GetMethods().FirstOrDefault(mi => mi.IsGenericMethod && mi.Name == nameof(this.RegisterPlugIn))?.MakeGenericMethod(plugInInterface, plugIn);
|
|
genericMethod?.Invoke(this, []);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
this._logger.LogError(e, "Couldn't register plugin type {PlugIn}", plugIn);
|
|
this._logger.LogError("TODO: Use ServiceContainer");
|
|
}
|
|
|
|
this._lastCreatedPlugIn = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exception that is thrown when two different plugin types share the same <see cref="GuidAttribute"/>.
|
|
/// </summary>
|
|
public class DuplicatePlugInGuidException : InvalidOperationException
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DuplicatePlugInGuidException"/> class.
|
|
/// </summary>
|
|
/// <param name="message">The message.</param>
|
|
public DuplicatePlugInGuidException(string message)
|
|
: base(message)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exception that occurs when the created proxy doesn't implement the expected interface <see cref="IPlugInContainer{TPlugIn}"/>.
|
|
/// </summary>
|
|
/// <seealso cref="System.Exception" />
|
|
public class InvalidPlugInProxyException : Exception
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="InvalidPlugInProxyException"/> class.
|
|
/// </summary>
|
|
/// <param name="type">The actual class type of the proxy.</param>
|
|
/// <param name="expectedType">The expected interface type of the proxy.</param>
|
|
public InvalidPlugInProxyException(Type type, Type expectedType)
|
|
{
|
|
this.Type = type;
|
|
this.ExpectedType = expectedType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the actual class type of the proxy.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The type.
|
|
/// </value>
|
|
public Type Type { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the expected interface type of the proxy.
|
|
/// </summary>
|
|
public Type ExpectedType { get; }
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString()
|
|
{
|
|
return $"Unexpected plugin proxy type {this.Type}. Expected one of {this.ExpectedType}";
|
|
}
|
|
}
|
|
} |