Monday, 23 March 2015

NuGetDownloadedModuleCatalog: Initialize modules downloaded from nuget



The problem: In my WP app, I don't want to deploy my entire application through clickonce. Instead I would like to install he shell and download all other modules from a nuget repository and then dynamically discover and initialize modules.


Prerequisite:
Nuget Package Explorer
Local NuGet Repository e.g. Artifactory



1. Create your nuget package and put all files as a content.
2. Upload to the repository.
3. Get the URL of the package.
4. Create a file called NuGetDownloadedModuleCatalog.xml and put inside your Shell application. The format has to be:-

<?xml version="1.0" encoding="utf-8" ?>
<Modules>
  <Module name="Module1" url="http://.....1.0.0.nupkg"/>
  <Module name="Module2" url="http://.....1.0.0.nupkg"/>
</Modules>


5.  Copy paste given code in the same app.

    public class NuGetDownloadedModuleCatalog : ModuleCatalog
    {
        private const string NugetCatalogFileName = "NugetModules.catalog";

        private readonly Dictionary<string, string> _packagedModules = new Dictionary<string, string>();


        readonly SynchronizationContext _context;

        /// <summary>
        /// Directory containing modules to search for.
        /// </summary>
        public string NugetModulesDirectory { get; set; }

        public NuGetDownloadedModuleCatalog(string nugetModulesDirectory)
        {

            _context = SynchronizationContext.Current;
            NugetModulesDirectory = nugetModulesDirectory;

            GetPackagedModules();
            StartPackageDownload();

            // we need to watch our folder for newly added modules
            var fileWatcher = new FileSystemWatcher(NugetModulesDirectory);
            fileWatcher.Created += FileWatcher_Created;
            fileWatcher.EnableRaisingEvents = true;
        }


        private void GetPackagedModules()
        {
            XDocument doc = XDocument.Load("NuGetDownloadedModuleCatalog.xml");
            if (doc.Root != null)
            {
                var moduleNodes = doc.Root.Elements().Where(e => e.HasAttributes);
                moduleNodes.ForEach(m =>
                {
                    var name = m.Attributes().FirstOrDefault(a => a.Name == "name");
                    var url = m.Attributes().FirstOrDefault(a => a.Name == "url");

                    if (name != null && url != null)
                        _packagedModules[name.Value] = url.Value;
                });
            }
        }

        private void StartPackageDownload()
        {
            if (!Directory.Exists(NugetModulesDirectory))
                Directory.CreateDirectory(NugetModulesDirectory);

            Task.Factory.StartNew(() =>
            {
                using (var client = new WebClient())
                {
                    try
                    {
                        foreach (var packagedModule in _packagedModules)
                        {
                            var moduleDestinationDir = Path.Combine(NugetModulesDirectory, packagedModule.Key);

                            if(ModuleDownloaded(moduleDestinationDir)) continue;

                            var zipFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip");
                            var unarchivedPath = Path.Combine(Path.GetTempPath(), packagedModule.Key);
                            client.DownloadFile(packagedModule.Value, zipFile);

                            if (Directory.Exists(unarchivedPath))
                                Directory.Delete(unarchivedPath, true);

                            Directory.CreateDirectory(unarchivedPath);

                            ArchiveManager.UnArchive(zipFile, unarchivedPath);

                            var contentPath = Path.Combine(unarchivedPath, "content");

                            CopyDir(contentPath, moduleDestinationDir);

                        }

                    }
                    // ReSharper disable EmptyGeneralCatchClause
                    catch (Exception) { }
                    // ReSharper restore EmptyGeneralCatchClause
                }
            });
        }

        public static void CopyDir(string source, string target)
        {
            if (!Directory.Exists(target)) Directory.CreateDirectory(target);
            string[] sysEntries = Directory.GetFileSystemEntries(source);

            foreach (string sysEntry in sysEntries)
            {
                string fileName = Path.GetFileName(sysEntry);
                if(fileName==null) continue;
                string targetPath = Path.Combine(target, fileName);
                if (Directory.Exists(sysEntry))
                    CopyDir(sysEntry, targetPath);
                else
                {
                    File.Copy(sysEntry, targetPath, true);
                }
            }

            UpdateCatalog(target);
        }

        private static readonly object SynchObject = new object();
        private static void UpdateCatalog(string target)
        {
            lock (SynchObject)
            {
                if (!File.Exists(NugetCatalogFileName))
                    File.WriteAllText(NugetCatalogFileName, string.Empty);

                var text = File.ReadAllText(NugetCatalogFileName);
                text += Environment.NewLine;
                text += target;
                File.WriteAllText(NugetCatalogFileName, text);
            }
        }

        private bool ModuleDownloaded(string target)
        {
            lock (SynchObject)
            {
                if (!File.Exists(NugetCatalogFileName))
                    File.WriteAllText(NugetCatalogFileName,string.Empty);


                var text = File.ReadAllText(NugetCatalogFileName);
                return text.Contains(target);
            }
        }


        /// <summary>
        /// Raised when a new file is added to the ModulePath directory
        /// </summary>
        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(NugetModulesDirectory);
        }

        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(string.Format("File {0} could not be found.", path));
            }
            else
            {
                if (!Directory.Exists(path))
                    throw new InvalidOperationException(string.Format("Directory {0} could not be found.", path));
            }

            AppDomain childDomain = BuildChildDomain(AppDomain.CurrentDomain);

            try
            {
                var loadedAssemblies = new List<string>();

                var assemblies = (
                    from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
                    where !(assembly is System.Reflection.Emit.AssemblyBuilder)
                          && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
                          && !String.IsNullOrEmpty(assembly.Location)
                    select assembly.Location
                    );

                loadedAssemblies.AddRange(assemblies);

                Type loaderType = typeof(InnerModuleInfoLoader);

                var loader =
                    (InnerModuleInfoLoader)
                        childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
                loader.LoadAssemblies(loadedAssemblies);

                //get all the ModuleInfos
                ModuleInfo[] modules = loader.GetModuleInfos(path, isFile);

                //add modules to catalog
                Items.AddRange(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(IEnumerable<ModuleInfo> modules)
        {
            if (_context == null)
                return;

            var moduleManager = ServiceLocator.Current.GetInstance<IModuleManager>();
            var modulecatalog = ServiceLocator.Current.GetInstance<IModuleCatalog>();

            _context.Send(delegate
            {
                foreach (var module in modules)
                {
                    if (moduleManager != null && modulecatalog != null && modulecatalog.Modules.Any(m => m != null && m.ModuleName == module.ModuleName))
                        moduleManager.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("parentDomain");

            var evidence = new Evidence(parentDomain.Evidence);
            AppDomainSetup setup = parentDomain.SetupInformation;
            return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);
        }

        private class InnerModuleInfoLoader : MarshalByRefObject
        {
            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
            internal ModuleInfo[] GetModuleInfos(string path, bool isFile = false)
            {
                Assembly moduleReflectionOnlyAssembly =
                    AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
                        asm => asm.FullName == typeof(IModule).Assembly.FullName);

                var moduleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName);

                FileSystemInfo info;
                if (isFile)
                    info = new FileInfo(path);
                else
                    info = new DirectoryInfo(path);

                ResolveEventHandler resolveEventHandler = delegate(object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, info); };
                AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
                IEnumerable<ModuleInfo> 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>();
                Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();

                var fileInfo = info as FileInfo;
                if (fileInfo != null)
                {
                    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;
                    }
                }

                var directory = info as DirectoryInfo;

                if (directory == null)
                    return Enumerable.Empty<ModuleInfo>();

                var files = directory.GetFiles("*.dll").Where(file => alreadyLoadedAssemblies.
                    FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null);

                foreach (FileInfo file in files)
                {
                    try
                    {
                        Assembly.ReflectionOnlyLoadFrom(file.FullName);
                        validAssemblies.Add(file);
                    }
                    catch (BadImageFormatException)
                    {
                        // skip non-.NET Dlls
                    }
                }

                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)
            {
                Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
                    asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
                if (loadedAssembly != null)
                {
                    return loadedAssembly;
                }

                var directory = info as DirectoryInfo;
                if (directory != null)
                {
                    var assemblyName = new AssemblyName(args.Name);
                    string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
                    if (File.Exists(dependentAssemblyFilename))
                    {
                        return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
                    }
                }

                return Assembly.ReflectionOnlyLoad(args.Name);
            }

            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
            internal void LoadAssemblies(IEnumerable<string> assemblies)
            {
                foreach (string 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)
            {
                string moduleName = type.Name;
                bool onDemand = false;
                var moduleAttribute = CustomAttributeData.GetCustomAttributes(type)
                    .Where(cad=>cad.Constructor!=null && cad.Constructor.DeclaringType!=null)
                    .FirstOrDefault(cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);

                if (moduleAttribute != null && moduleAttribute.NamedArguments!=null)
                {
                    foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
                    {
                        string 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!=null && cad.Constructor.DeclaringType!=null 
                        && cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);

                var dependsOn = moduleDependencyAttributes.Select(cad => (string) cad.ConstructorArguments[0].Value).ToList();

                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("collection");
            if (items == null) throw new ArgumentNullException("items");

            foreach (var each in items)
            {
                collection.Add(each);
            }

            return collection;
        }
    }


6.  In your bootstrapper, add following code:-

   protected override IModuleCatalog CreateModuleCatalog()
        {
            var catalog = new NuGetDownloadedModuleCatalog(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NuGetModules"));
            return catalog;
        }

Done!

Enjoy.