skip to Main Content

I am working on an application with two supporting DLLs in the Project. ClassLibrary1.dll and ClassLibrary2.dll. The ClassLibrary2.dll is optional and only used by the ClassLibrary1.dll. The application has a feature to import the DLL explicitly. Everything worked fine if I import both DLLs into the application.

The problem arises when I don’t import the optional ClassLibrary2.dll.

Here on application start, I am checking whether assembly exists or not:

var assemblyName = "ClassLibrary2.dll, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var assembly = (from a in assemblies
                where a.FullName == assemblyName
                select a).SingleOrDefault();

// this IsClassLibrary2Exists property become true when the DLL exists
if (assembly != null)
{
    Props.IsClassLibrary2Exists = true;          
}

Here is How I am calling the ClassLibrary2 method in ClassLibrary1

if(Props.IsClassLibrary2Exists){
      ClassLibrary2.SpecialProduct.GetSpecialProduct(Id);
}

I am getting an error when the assembly does not exist:

"System.IO.FileNotFoundException: ‘Could not load file or assembly ‘ClassLibrary2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null’ or one of its dependencies. The system cannot find the file specified.’"

2

Answers


  1. To have any success in preventing one assembly to require the other one you need all access to the "assembly 2" to be done via methods that are not executed. Protecting access with if is not enough because whole method need to be JITed first and that requires loading the other assembly.

    What likely would work:

       ... 
       if (Props.IsClassLibrary2Exists)
           DoThingsWithLibrary2();
       ...
    
       void DoThingsWithLibrary2()
       {
         // access to Library2 feature must be scoped to methods
         // that are not executed
         ClassLibrary2.SpecialProduct.GetSpecialProduct(Id);
       }  
    

    Note that it is extremely easy to use "the other library" in a way that is hard/impossible to fix at run-time – static properties/constructors can be called at any time and hence referencing anything from the other library there will fail, having fields/properties/method parameters/return to use types from the other library would fail even via reflection… and like many other cases.

    If possible prefer to load the assembly dynamically and have it implement shared interfaces so you can limit reflection to instantiation part, but use those classes via strongly typed interfaces.

    Login or Signup to reply.
  2. I agree with the comment by Dai that the underlying issue is that ClassLibrary1 has a hardcoded dependency on ClassLibrary2 so the matter falls to how this can be decoupled.

    One approach that has worked for me is to have an Interface class library consisting only of plugin interfaces that the application may attempt to use (but which, at the same time, might not have instances attached). It will be referenced by plugin servers and clients alike.


    Interface class library

    namespace Interface
    {
        /// <summary>
        /// To be a plugin for this app requires IPlugin at minimum .
        /// </summary>
        public interface IPlugin { }
        public interface ISpecialProduct : IPlugin
        {
            string GetSpecialProduct(string id);
        }
    }
    

    ClassLibrary1

    The SpecialProduct member is decoupled from ClassLibrary2 because it’s an interface not a class.

    using Interface; // But 'not' ClassLibrary2
    namespace ClassLibrary1
    {
        public class Product
        {
            public ISpecialProduct SpecialProduct { get; set; }
        }
    }
    

    ClassLibrary2

    using Interface; // The only dependency
    namespace ClassLibrary2
    {
        public class SpecialProduct : ISpecialProduct
        {
            public string GetSpecialProduct(string id) => Guid.NewGuid().ToString();
        }
    }
    

    Test (proof of concept using console app)

    Available plugins are located in the Plugins subfolder of the application’s run directory. ClassLibrary2 is not initially referenced or loaded.

    using Interface;
    using ClassLibrary1; // But 'not' ClassLibrary2
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.Title = "Test optional lib";
            Assembly assySpecial;
            Product product = new Product(); // In ClassLibrary1
    
            #region N O T    L O A D E D
            // Try get assy (not strong named or picky about version)
            assySpecial =
                AppDomain
                .CurrentDomain
                .GetAssemblies()
                .FirstOrDefault(_ => _.GetName().Name.Equals("ClassLibrary2"));
            Debug.Assert(assySpecial == null, "Expecting assy not loaded");
            Debug.Assert(product.SpecialProduct == null, "Expecting null SpecialProduct");
            if(product.SpecialProduct == null)
            {
                Console.WriteLine("SpecialProduct is not loaded yet.");
            }
            #endregion N O T    L O A D E D
    
            #region L O A D
            var pluginPath = Path.Combine(
                AppDomain.CurrentDomain.BaseDirectory,
                "Plugins",
                "ClassLibrary2.dll");
            if(File.Exists(pluginPath)) 
            {
                Assembly.LoadFrom(pluginPath);
            }
            #endregion L O A D
    
            #region L O A D E D
            assySpecial =
                AppDomain
                .CurrentDomain
                .GetAssemblies()
                .FirstOrDefault(_ => _.GetName().Name.Equals("ClassLibrary2"));
    
            Debug.Assert(assySpecial != null, "Expecting assy loaded");
    
            if (assySpecial != null)
            {
                product.SpecialProduct = 
                    (ISpecialProduct)
                    Activator.CreateInstance(
                        assySpecial
                        .GetTypes()
                        .First(_ => _.Name.Equals("SpecialProduct")));
            }
    
            Console.WriteLine($"SpecialProduct: {product.SpecialProduct?.GetSpecialProduct("123")}");
    
            Console.ReadKey();
            #endregion L O A D E D
        }
    }
    

    console output

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search