Skip to content

Instantly share code, notes, and snippets.

@MrAliev
Last active June 10, 2021 11:37
Show Gist options
  • Save MrAliev/409b14993b2aa79541ce566ae0fed8c2 to your computer and use it in GitHub Desktop.
Save MrAliev/409b14993b2aa79541ce566ae0fed8c2 to your computer and use it in GitHub Desktop.
Dynamic DirectoryModuleCatalog for PRISM App
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
// ...
// ...
protected override IModuleCatalog CreateModuleCatalog()
{
const string dynamicDirectory = "DyrectoryNameForDynamicModules";
var dynamicDirectory = Path.Combine(Environment.CurrentDirectory, dynamicDirectory);
if (!Exists(dynamicDirectory))
{
_ = CreateDirectory(dynamicDirectory);
}
var catalog = new DynamicDirectoryModuleCatalog(Container.GetContainer(), dynamicDirectory);
return catalog;
}
// ...
// ...
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Policy;
using System.Threading;
using DryIoc;
using Prism.Modularity;
namespace ShellApp.Services
{
public class DynamicDirectoryModuleCatalog : ModuleCatalog
{
private readonly IContainer _container;
private readonly SynchronizationContext _context;
/// <summary>
/// Directory containing modules to search for.
/// </summary>
public string ModulePath { get; set; }
public DynamicDirectoryModuleCatalog(IContainer container, string modulePath)
{
_container = container;
_context = SynchronizationContext.Current;
ModulePath = modulePath;
// we need to watch our folder for newly added modules
var fileWatcher = new FileSystemWatcher(ModulePath, "*.dll");
fileWatcher.Created += FileWatcher_Created;
fileWatcher.EnableRaisingEvents = true;
}
/// <summary>
/// Rasied when a new file is added to the ModulePath directory
/// </summary>
private void FileWatcher_Created(object sender, FileSystemEventArgs e)
{
if (e.ChangeType == WatcherChangeTypes.Created)
{
LoadModuleCatalog(e.FullPath, true);
}
}
/// <summary>
/// Drives the main logic of building the child domain and searching for the assemblies.
/// </summary>
protected override void InnerLoad()
{
LoadModuleCatalog(ModulePath);
}
private void LoadModuleCatalog(string path, bool isFile = false)
{
if (string.IsNullOrEmpty(path))
throw new InvalidOperationException("Path cannot be null.");
if (isFile)
{
if (!File.Exists(path))
throw new InvalidOperationException($"File {path} could not be found.");
}
else
{
if (!Directory.Exists(path))
throw new InvalidOperationException($"Directory {path} could not be found.");
}
var childDomain = BuildChildDomain(AppDomain.CurrentDomain);
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => !(assembly is System.Reflection.Emit.AssemblyBuilder) &&
assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder" &&
!string.IsNullOrEmpty(assembly.Location))
.Select(assembly => assembly.Location);
try
{
var loadedAssemblies = new List<string>();
loadedAssemblies.AddRange(assemblies);
var loaderType = typeof(InnerModuleInfoLoader);
var loader = (InnerModuleInfoLoader) childDomain
.CreateInstanceFrom(loaderType.Assembly.Location,
loaderType.FullName ?? throw new ArgumentException($"{loaderType.FullName}")).Unwrap();
loader.LoadAssemblies(loadedAssemblies);
//get all the ModuleInfos
ModuleInfo[] modules = loader.GetModuleInfos(path, isFile);
//add modules to catalog
System.Collections.ObjectModel.CollectionExtensions.AddRange(this.Items, modules);
//we are dealing with a file from our file watcher, so let's notify that it needs to be loaded
if (isFile)
{
LoadModules(modules);
}
}
finally
{
AppDomain.Unload(childDomain);
}
}
/// <summary>
/// Uses the IModuleManager to load the modules into memory
/// </summary>
/// <param name="modules"></param>
private void LoadModules(ModuleInfo[] modules)
{
if (_context == null) return;
var manager = _container.Resolve<IModuleManager>();
_context.Send(delegate
{
foreach (var module in modules)
{
manager.LoadModule(module.ModuleName);
}
}, null);
}
/// <summary>
/// Creates a new child domain and copies the evidence from a parent domain.
/// </summary>
/// <param name="parentDomain">The parent domain.</param>
/// <returns>The new child domain.</returns>
/// <remarks>
/// Grabs the <paramref name="parentDomain"/> evidence and uses it to construct the new
/// <see cref="AppDomain"/> because in a ClickOnce execution environment, creating an
/// <see cref="AppDomain"/> will by default pick up the partial trust environment of
/// the AppLaunch.exe, which was the root executable. The AppLaunch.exe does a
/// create domain and applies the evidence from the ClickOnce manifests to
/// create the domain that the application is actually executing in. This will
/// need to be Full Trust for Composite Application Library applications.
/// </remarks>
/// <exception cref="ArgumentNullException">An <see cref="ArgumentNullException"/> is thrown if <paramref name="parentDomain"/> is null.</exception>
protected virtual AppDomain BuildChildDomain(AppDomain parentDomain)
{
if (parentDomain == null) throw new ArgumentNullException(nameof(parentDomain));
var evidence = new Evidence(parentDomain.Evidence);
var setup = parentDomain.SetupInformation;
return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);
}
private class InnerModuleInfoLoader : MarshalByRefObject
{
internal ModuleInfo[] GetModuleInfos(string path, bool isFile = false)
{
var moduleReflectionOnlyAssembly =
AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
asm => asm.FullName == typeof(IModule).Assembly.FullName);
var moduleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName ?? throw new ArgumentException());
FileSystemInfo info;
if (isFile)
info = new FileInfo(path);
else
info = new DirectoryInfo(path);
Assembly ResolveEventHandler(object sender, ResolveEventArgs args) => OnReflectionOnlyResolve(args, info);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ResolveEventHandler;
var modules = GetNotAllreadyLoadedModuleInfos(info, moduleType);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= ResolveEventHandler;
return modules.ToArray();
}
private static IEnumerable<ModuleInfo> GetNotAllreadyLoadedModuleInfos(FileSystemInfo info, Type moduleType)
{
var validAssemblies = new List<FileInfo>();
var alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
if (info is FileInfo fileInfo)
{
if (alreadyLoadedAssemblies.FirstOrDefault(assembly => string.Compare(Path.GetFileName(assembly.Location), fileInfo.Name, StringComparison.OrdinalIgnoreCase) == 0) == null)
{
var moduleInfos = Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName).GetExportedTypes()
.Where(moduleType.IsAssignableFrom)
.Where(t => t != moduleType)
.Where(t => !t.IsAbstract).Select(CreateModuleInfo);
return moduleInfos;
}
}
if (info is DirectoryInfo directory)
{
var files = directory.GetFiles("*.dll").Where(file => alreadyLoadedAssemblies.
FirstOrDefault(assembly => string.Compare(Path.GetFileName(assembly.Location), file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null);
foreach (var file in files)
{
try
{
Assembly.ReflectionOnlyLoadFrom(file.FullName);
validAssemblies.Add(file);
}
catch (BadImageFormatException)
{
// skip non-.NET Dlls
}
}
}
else
{
throw new ArgumentNullException(nameof(directory));
}
return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName)
.GetExportedTypes()
.Where(moduleType.IsAssignableFrom)
.Where(t => t != moduleType)
.Where(t => !t.IsAbstract)
.Select(CreateModuleInfo));
}
private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, FileSystemInfo info)
{
var loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
if (loadedAssembly != null)
{
return loadedAssembly;
}
if (!(info is DirectoryInfo directory)) return Assembly.ReflectionOnlyLoad(args.Name);
var assemblyName = new AssemblyName(args.Name);
var dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
return File.Exists(dependentAssemblyFilename) ?
Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename) :
Assembly.ReflectionOnlyLoad(args.Name);
}
internal void LoadAssemblies(IEnumerable<string> assemblies)
{
foreach (var assemblyPath in assemblies)
{
try
{
Assembly.ReflectionOnlyLoadFrom(assemblyPath);
}
catch (FileNotFoundException)
{
// Continue loading assemblies even if an assembly can not be loaded in the new AppDomain
}
}
}
private static ModuleInfo CreateModuleInfo(Type type)
{
var moduleName = type.Name;
var dependsOn = new List<string>();
var onDemand = false;
var moduleAttribute = CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(cad =>
cad.Constructor.DeclaringType != null && cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);
if (moduleAttribute?.NamedArguments != null)
foreach (var argument in moduleAttribute.NamedArguments)
{
var argumentName = argument.MemberInfo.Name;
switch (argumentName)
{
case "ModuleName":
moduleName = (string)argument.TypedValue.Value;
break;
case "OnDemand":
onDemand = (bool)argument.TypedValue.Value;
break;
case "StartupLoaded":
onDemand = !(bool)argument.TypedValue.Value;
break;
}
}
var moduleDependencyAttributes = CustomAttributeData.GetCustomAttributes(type).Where(cad =>
cad.Constructor.DeclaringType != null && cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);
foreach (var cad in moduleDependencyAttributes)
{
dependsOn.Add((string)cad.ConstructorArguments[0].Value);
}
var moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
{
InitializationMode =
onDemand
? InitializationMode.OnDemand
: InitializationMode.WhenAvailable,
Ref = type.Assembly.CodeBase,
};
moduleInfo.DependsOn.AddRange(dependsOn);
return moduleInfo;
}
}
}
/// <summary>
/// Class that provides extension methods to Collection
/// </summary>
public static class CollectionExtensions
{
/// <summary>
/// Add a range of items to a collection.
/// </summary>
/// <typeparam name="T">Type of objects within the collection.</typeparam>
/// <param name="collection">The collection to add items to.</param>
/// <param name="items">The items to add to the collection.</param>
/// <returns>The collection.</returns>
/// <exception cref="System.ArgumentNullException">An <see cref="System.ArgumentNullException"/> is thrown if <paramref name="collection"/> or <paramref name="items"/> is <see langword="null"/>.</exception>
public static Collection<T> AddRange<T>(this Collection<T> collection, IEnumerable<T> items)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (items == null) throw new ArgumentNullException(nameof(items));
foreach (var each in items)
{
collection.Add(each);
}
return collection;
}
}
}
@MrAliev
Copy link
Author

MrAliev commented Jun 10, 2021

ModuleCatalog для PRISM-приложений.

  • При запуске - сканирует папку с модулями, и подключает все найденные модули.
  • После запуска приложения - отслеживает появление новых модулей в папке, и автоматически подключает их к приложению без необходимости перезапуска приложения

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment