0

I have a netstandard20 class library and a console application which has no direct reference to that class library. Instead I need to load that library at runtime so that types could be resolved correctly. For now my console application targets net8.0 and I do it the following way:

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

namespace AssemblyResolveTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //if I uncomment this line typeSecondAttempt would be null below
            //var typeFirstAttempt = Type.GetType("DataClasses.TestDataClass");

            var path = Path.Combine(Directory.GetCurrentDirectory(), "DataClasses.dll");
            var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);

            var typeSecondAttempt = Type.GetType("DataClasses.TestDataClass, DataClasses");
        }
}

My first problem is - why if I uncomment typeFirstAttempt line typeSecondAttempt becomes null (cannot be resolved)?

My second problem is that now I need to move library load logic to a netstandard20 class library and I cannot use System.Runtime.Loader.AssemblyLoadContext there. There is a nuget but it is working when called only from .NET (but I need to support .NET Framework as well). The only approach I know is AppDomain.CurrentDomain.AssemblyResolve, like that:

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

namespace AssemblyResolveTestClassLibrary
{
    public static class AssemblyResolver
    {
        static AssemblyResolver()
        {
            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        }

        private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            if (new AssemblyName(args.Name).Name == "DataClasses")
            {
                var path = Path.Combine(Directory.GetCurrentDirectory(), "DataClasses.dll");
                return Assembly.LoadFile(path);
            }
            else
                return null;
        }

        public static Type GetTestDataClassType()
        {
            return Type.GetType("DataClasses.TestDataClass, DataClasses");
        }
    }
}

But Assembly.LoadFile(path) executes every time I call AssemblyResolver.GetTestDataClassType(). My goal is to load assembly one time - how to achieve that? Maybe some other AppDomain.CurrentDomain function? Thank you.

UPDATE - clarification about my project real structure (simplified):

Interfaces - netstandard20 library. Let's say it has:

public interface ISampleResult
{
    Guid Id { get; }
}
public interface ISampleService
{
    ISampleResult GetSampleResult();
}

DataClasses - netstandard20 library, references Interfaces library. Let's say it has:

public class SampleResult(Guid id) : ISampleResult
{
    public Guid Id { get; } = id;
}

Server - net8.0 class library that actually performs operations, declared in interfaces. It references Interfaces and DataClasses libraries. Let's say it has:

public class SampleService : ISampleService
{
    public ISampleResult GetSampleResult()
    {
       return new SampleResult(Guid.Empty);
    }
}

IPC - netstandard20 library, references Interfaces library and NewtonSoft.Json nuget. It is a library for inter-process communication: plugins (can be net472 or net8.0+, that reference Interfaces library) sends a Func<T> request to be actually executed on a server and get response with result. Requests/responses are sended as JSON. Let's say plugin requests ISampleService.GetSampleResult() - when NewtonSoft.Json tries to deserialize response from server it fails, because IPC knows nothing about DataClasses. Thats why I need to detect such situations and load DataClasses library at runtime (but only once).

Feel free to ask questions if I didn't clarified enough.

4
  • 1
    Can't you just do something like var assembly = Assembly.LoadFile(path); var testDataType = assembly.GetType("DataClasses.TestDataClass") ? Commented Aug 25 at 15:00
  • No, I cannot. netstandard20 library for which I need a working solution serializes/deserializes objects from JSON via NewtonSoft.JSON library. I will add this clarification to my question. Commented Aug 25 at 18:14
  • It doesn't sounds correct. If a plugin requests a GetSampleResult it should get the interface value back, not a concrete class, why would it even try to reference DataClasses, nor how does it even know that interface implementation is there? In c++ COM, you would get some sort of proxy interface implementation back, which calls back to the server for real values. Are your interfaces pure data objects, or do they have methods? Commented Sep 1 at 14:59
  • If you step through Assembly.LoadFrom, it registers the dll path into a s_loadFromAssemblyList list and then calls AssemblyLoadContext.Default.LoadFromAssemblyPath, much like you. Difference is s_loadFromAssemblyList, which is used by LoadFromResolveHandler which seems like the default ResolveHandler. Commented Sep 25 at 12:33

1 Answer 1

0

Here's a working example:

internal class Program
{
    static AssemblyDependencyResolver _resolver;

    private static Assembly CustomEventHandlerForAppDomain(object sender, ResolveEventArgs args)
    {
        Console.WriteLine($"Resolving {args.Name}...");

        string assemblyPath = _resolver.ResolveAssemblyToPath(new AssemblyName(args.Name));
        if (assemblyPath != null)
        {
            return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
        }

        return null;
    }

    static void Main(string[] args)
    {
        //string assembly_path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DataClasses.dll");
        //assembly_path doesn't need to point right next to the executable. But it does use the same path to resolve other dependencies
        string assembly_path = @"full_path_to\\DataClasses.dll";
        _resolver = new AssemblyDependencyResolver(assembly_path);

        //if I uncomment this line typeSecondAttempt would be null below
        var typeFirstAttempt = Type.GetType("DataClasses.SampleResult");
        Console.WriteLine("FirstAttempt(DataClasses.SampleResult): "+typeFirstAttempt?.ToString());


        AppDomain.CurrentDomain.AssemblyResolve += CustomEventHandlerForAppDomain;

        var typeFirstSortAttempt = Type.GetType("AssemblyLoadTest.ISort");
        Console.WriteLine($"FirstAttempt(AssemblyLoadTest.ISort): {typeFirstSortAttempt?.ToString()}");

        var typeSecondAttempt = Type.GetType("DataClasses.SampleResult, DataClasses");
        Console.WriteLine($"SecondAttempt(DataClasses.SampleResult): {typeSecondAttempt?.ToString()}");

        var typeThirdAttempt = Type.GetType("DataClasses.SampleResult, DataClasses");
        Console.WriteLine($"ThirdAttempt(DataClasses.SampleResult): {typeThirdAttempt?.ToString()}");
    }

The output is:

FirstAttempt(DataClasses.SampleResult):
FirstAttempt(AssemblyLoadTest.ISort): AssemblyLoadTest.ISort
Resolving DataClasses, Culture=neutral, PublicKeyToken=null...
Resolving Interfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null...
SecondAttempt(DataClasses.SampleResult): DataClasses.SampleResult
ThirdAttempt(DataClasses.SampleResult): DataClasses.SampleResult

I recreated your project setup with two netstandard2.0 libs. DataClasses uses Interfaces as a project ref so their dlls are effectively together in DataClasses ${OutDir}. The console project is .net8.0. To show the resolutions from the current AppDomain, I have consecutive resolutions for the DataClasses type and also an interface residing in the console app project (ISort).

Using AssemblyDependencyResolver to create a resolver for the DataClasses and Interfaces libs. Provided they are together in the same directory (be it next to the executable or in a separate folder as the tutorial mentioned in the PS. If the current AppDomain cannot resolve the type, it will raise the Resolving event which will try to resolve the type to a path and load it.

You'll notice that the first attempt cannot resolve at all and prints nothing. The ISort prints without calling the custom resolver and the Second Attempt resolves through the AssemblyDependencyResolver. The ThirdAttempt does not because it's already "cached" in the AppDomain.

PS: I didn't try to look into the Newtonsoft issue but the tutorial on adding plugins has a section that refers to the Newtonsoft.Json package and talks about a specific property to add to the csproj.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.