2

I have the following scenario:

One host application, that hosts (win)forms that are defined in other projects of the same solution. The hosted forms all implement the same interface. Since the host application references all those other projects dynamic loading (assembly-resolving) is not involved (and not wanted).

Now I thought that I could iterate AppDomain.CurrentDomain.GetAssemblies(), getting all types and filter which of the types implement my interface. But since I never explicitly use the other projects from the host-project the assemblies won't be loaded in the host-app and thereby AppDomain.CurrentDomain.GetAssemblies() won't list them.

I think in most cases this behavior is good, but in my case (reflection usage) it's a problem. Is there a way to force loading (specific) assemblies that are referenced but not explicitly used? I'm looking for visual studio/project-settings way to do this - I don't like the idea to write code to get assemblies loaded that are already referenced :(

Possible solutions:

  • Call Assembly.Load for each of the assemblies. con: code-change in the host app
  • Call a known class/method from the forms-project(s) to force loading. con: code change in the host app and knowledge of class/method to call
Michael
  • 1,931
  • 2
  • 8
  • 22
  • 2
    So you want to do that without writing any code, right? – Evk Nov 22 '17 at 12:42
  • https://stackoverflow.com/questions/2384592/is-there-a-way-to-force-all-referenced-assemblies-to-be-loaded-into-the-app-doma – CodeCaster Nov 22 '17 at 12:43
  • @Evk I don't want to add code each time a new project is added. If the code is able to deal with newly added project-references (ignoring other DLLs that are not referenced) without modifications, this would be fine. – Michael Nov 22 '17 at 13:01
  • Ok, then see link above by CodeCaster which does exactly that I think. – Evk Nov 22 '17 at 13:02
  • 1
    That answer is 7 years old mind you. There could be better options now. – KinSlayerUY Nov 22 '17 at 13:13
  • @Evk Yes, the link is good. (Already had this) But the examples of the link just list all .dll files in the host-app directory and then attempt to load those that are not already loaded. What happens if some unknown dll will be moved into the host-app-directory? – Michael Nov 22 '17 at 13:21
  • First answer there (one by Jon Skeet) uses `Assembly.GetReferencedAssemblies`, not lists any dlls in directories. So you call `Assembly.GetReferencedAssemblies` on your host dll (with `Assembly.GetEntryAssembly().GetReferencedAssemblies()` for example) and load those which are not loaded yet. – Evk Nov 22 '17 at 13:23
  • @Evk I've seen Jon Skeets answer with `GetReferencedAssemblies` and tried it. Calling `Assembly.GetExecutingAssembly().GetReferencedAssemblies()` returns a list of assemblies, but this list does not contain any of my "missing" DDLs/executables - only if I create an instance of some object of the referenced project before - and thats what I want to prevent. May be its a problem that my referenced projects produces executables and not libraries? Can't find any restrictions on that on MSDN/GetReferencedAssemblies... – Michael Nov 22 '17 at 14:32
  • Yes you are right. Assemblies which are not used will not be added to assembly metadata by compiler (even if you add them to "References" in visual studio), and so will not appear in `GetReferencesAssemblies`. For that reason I think what you want to do is impossible and you have to resort to inspecting files unfortunately, because information about assemblies that are "referenced" in VS but not used is just not available. – Evk Nov 22 '17 at 14:37
  • 2
    I'm not sure if this is what you are seeking, but you could use a T4 template to enumerate all projects in the solution and their referenced assemblies and output a code file with the information. – TnTinMn Nov 22 '17 at 15:51

1 Answers1

2

Answer based on the comment from TnTinMn. Thanks for the suggestion!

  • Change the static Program class (Entry point of the host app) to be a partial class
  • Create a TextTemplate/T4 file named Program.AssemblyLoader.tt
  • Add the following content:

Program.AssemblyLoader.tt:

<#@ template language="C#" hostSpecific="true" debug="True" #>
<#@ output extension="cs" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDte" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System" #>
<#

    var visualStudio = (EnvDTE.DTE)(Host as IServiceProvider).GetService(typeof(EnvDTE.DTE));
    var solution = (EnvDTE.Solution)visualStudio.Solution;
    var projects = solution.Projects;

    // Assume that the first item is the main project, the host app.
    // Index is **NOT** zero-based!
    var main = (EnvDTE.Project)projects.Item(1);
    var p = main.Properties.Item("DefaultNamespace");
    var ns = p.Value.ToString();

#>
using System;
using System.Linq;
using System.IO;
using System.Reflection;

namespace <# WriteLine(ns); #>
{
    static partial class Program
    {
        static Program()
        {
            //! Generated file. Do not edit. Check the .tt file instead!
<#
foreach(EnvDTE.Project proj in projects)
{
    if(proj.Properties == null || proj.Properties.Item("OutputFileName") == null)
        continue;

    EnvDTE.Property p2 = proj.Properties.Item("OutputFileName") as EnvDTE.Property;
    WriteLine("            LoadAssemblyIfNecessary(\"" + p2.Value + "\");");
}
#>
        }

        private static readonly AssemblyName[] REFS = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
        private static readonly string APP = Path.GetFileName(System.Windows.Forms.Application.ExecutablePath);

        private static void LoadAssemblyIfNecessary(string name)
        {
            if (string.Equals(name, APP, StringComparison.InvariantCultureIgnoreCase)) return;

            if (!REFS.Any(x => x.Name.StartsWith(Path.GetFileNameWithoutExtension(name), StringComparison.InvariantCultureIgnoreCase)))
            {
                AppDomain.CurrentDomain.Load(Path.GetFileNameWithoutExtension(name));
            }
        }
    }
}

Example for the generated Program.AssemblyLoader.cs:

using System;
using System.Linq;
using System.IO;
using System.Reflection;

namespace Your.Name.Space
{
    static partial class Program
    {
        static Program()
        {
            //! Generated file. Do not edit. Check the .tt file instead!
            LoadAssemblyIfNecessary("HostApp.dll");
            LoadAssemblyIfNecessary("Dependency1.dll");
            LoadAssemblyIfNecessary("Dependency2.dll");
            LoadAssemblyIfNecessary("Modul1.exe");
        }

        private static readonly AssemblyName[] REFS = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
        private static readonly string APP = Path.GetFileName(System.Windows.Forms.Application.ExecutablePath);

        private static void LoadAssemblyIfNecessary(string name)
        {
            if (string.Equals(name, APP, StringComparison.InvariantCultureIgnoreCase)) return;

            if (!REFS.Any(x => x.Name.StartsWith(Path.GetFileNameWithoutExtension(name), StringComparison.InvariantCultureIgnoreCase)))
            {
                AppDomain.CurrentDomain.Load(Path.GetFileNameWithoutExtension(name));
            }
        }
    }
}
Michael
  • 1,931
  • 2
  • 8
  • 22
  • +1 I am glad to see someone that can take a comment and research/implement it to satisfy their needs. It is such a rare and wonderful experience in these days of _give me a C&P answer_. – TnTinMn Nov 24 '17 at 20:26
  • But in this example, the USER has to make his own entry point, the Program class, partial. This is a requirement the user has to fullfill. In an scenario where the assembly is third party and the user is just consument, he have to make this EXPLICIT. This annoys me for so long. I come to the conclusion that you could inject a module initializer to the customer's project assembly output. In c# 9 it should be one stop easier to just add a c# file decorated by `[module: ModuleInitializer(typeof(ForeignAssembly.MyModuleInitializer))]` – Teneko Jul 28 '20 at 13:37