0

I have a console app that resides in C:\MyApp.

I have several libraries that are NOT referenced by the app. I use an Activator.CreateInstance() to use them. They reside in C:\MyLibrary\Job001, C:\MyLibrary\Job002, etc. Each of these libraries have multiple dependencies and can be different versions of dependencies already found in the main app.

When I try to run this I am seeing this error: Could not load file or assembly 'Persistence.Database, Version=1.7.2.67, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified. This is one of the common dependencies for most jobs. I checked the directory and it does exist with the library.

How do I activate the library AND have it use the references as found in it's own directory?

I am using the following (extension) code to activate a library:

public static IJob ConcreteJob(this JobInfoPayload src)
{
    if (src.AssemblyFile.IsNullOrEmpty())
        throw new Exception("AssemblyFile cannot be empty or null!");
    if (src.AssemblyName.IsNullOrEmpty())
        throw new Exception("AssemblyName cannot be empty or null!");

    try
    {
        var assembly = Assembly.LoadFile(src.AssemblyFile);
        var assemblyType = assembly.GetType(src.AssemblyName);
        var job = Activator.CreateInstance(assemblyType) as IJob;
        return job;
    }
    catch (Exception ex)
    {
        Serilog.Log.Logger.Fatal(ex, "JOB was not able to be created!!");
        throw; // bubble this up to the top...
    }
}

I am looking at system.appdomain.assemblyresolve but am not making sense of how to use this in the library project.

Thoughts?


ADDITIONAL INFO (29 NOV 2016)

Server App References:

  • Library.Infrastructure
  • QueueApp.Core
  • Hangfire
  • OWIN

Job Library References:

  • Library.Infrastructure
  • Library.Persistence
  • Library.SQL.Database01
  • Library.SQL.Database02
  • QueueApp.Job.Core
  • EntityFramework

We have several Jobs that follow the same pattern BUT can be built with different versions of the Job Library References. This is due to a slow creep over time. If a job written last year is still working why would we take the time to open up that solution, update all the references, recompile, then spend a month going back through QA and acceptance when we can just leave it alone?

The challenge I am running into is the JOB cannot find the referenced files, expecting them to be in the Server App directory. Instead, they are in that Job's directory. Using Fuslogvw.exe just confirms that it is NOT looking in the DLL's directory but rather the hosting app's directory.

** I currently get the same behavior whether I use Assembly.LoadFrom() or Assembly.LoadFile().

FUSLOGVW log results:

*** Assembly Binder Log Entry  (11/29/2016 @ 10:20:21 AM) ***

The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
Running under executable  D:\Dev\QueueApp\Source\QueueApp\bin\Debug\QueueApp.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: DisplayName = Job.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null (Fully-specified)
LOG: Appbase = file:///D:/Dev/QueueApp/Source/QueueApp/bin/Debug/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = QueueApp.exe
Calling assembly : Job.AgileExport, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: D:\Dev\QueueApp\Source\QueueApp\bin\Debug\QueueApp.exe.Config
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
LOG: Attempting download of new URL file:///D:/Dev/QueueApp/Source/QueueApp/bin/Debug/Job.Core.DLL.
LOG: Attempting download of new URL file:///D:/Dev/QueueApp/Source/QueueApp/bin/Debug/Job.Core/Job.Core.DLL.
LOG: Attempting download of new URL file:///D:/Dev/QueueApp/Source/QueueApp/bin/Debug/Job.Core.EXE.
LOG: Attempting download of new URL file:///D:/Dev/QueueApp/Source/QueueApp/bin/Debug/Job.Core/Job.Core.EXE.
LOG: All probing URLs attempted and failed.

The APP is looking for all files in:
D:\Dev\QueueApp\Source\QueueApp\bin\Debug
The JOB exists in:
D:\Dev\QueueApp\Source\Job.AgileExport\bin\Debug

Keith Barrows
  • 24,802
  • 26
  • 88
  • 134

3 Answers3

1

I think there are two solutions available.

One solution is to create a new AppDomain to host your dynamically loaded assembly. When you create a new AppDomain you have the option of providing a settings object for the AppDomain, and in that object you get to provide the paths that AppDomain will use to resolve assemblies. You can't alter the paths in your existing AppDomain because it already exists.

Another solution is to handle your current AppDomain's AssemblyResolve event, which will be raised in the case that the normal assembly resolution fails. You can then take custom steps to help resolve the assembly.

There is a feature/bug in .NET where handling this event is required when .NET is hosted in various containers (such as IE, COM+, and more) and BinaryFormatter is used to deserialize types that should be available, but actually aren't found.

I have an example of hooking and resolving the AssemblyResolve event here: https://github.com/MarimerLLC/csla/blob/V1-5-x/cslacs10/NetRun/Launcher.cs

In your case you can probably alter my ResolveEventHandler method to look for the "missing" assemblies in the folder where you originally loaded the dynamic assembly.

Rockford Lhotka
  • 842
  • 6
  • 9
1

Using Assembly.LoadFrom, it is not possible to load multiple versions of the same assembly, in the same AppDomain.

Thus, if Job001 needs LibraryA, 1.0.0.0 (and can't use newer version at runtime) and Job002 needs LibraryA, 2.0.0.0, you'll have to load Job001 and Job002 each in its own AppDomain.

Notice that the order in which you dynamically load assemblies is very important:

  • When you load Job001 it will automatically load LibraryA, 1.0.0.0 if it finds it, and if you load Job002 after that, it won't be able to load LibraryA, 2.0.0.0 and LibraryA, 1.0.0.0 will remain in the domain.

  • Likewise, When you load Job002 it will automatically load LibraryA, 2.0.0.0 if it finds it, and if you load Job001 after that, it won't be able to load LibraryA, 1.0.0.0 and LibraryA, 2.0.0.0 will remain in the domain.

You best bet is to either use Assembly.LoadFile + AppDomain.AssemblyResolve to load the dependencies yourself (and then you can have multiple versions of the same assembly in the same AppDomain), or you create a separate AppDomain for each JobXXX assembly, and let the dependencies be loaded automatically.

C. Augusto Proiete
  • 24,684
  • 2
  • 63
  • 91
  • Makes sense - too much sense. I fear my knowledge of AppDomains is limited. My first reaction is to create a new AppDomain per job. Now, to figure out, with the plethora of conflicting advice, which is the best/easiest way to do this. :) – Keith Barrows Nov 30 '16 at 20:41
0

This is what I came up with so far. These classes are in the main server app, not found in any of the JOBs. We have several different types of JOBs, Ad Hoc being one of the types. By placing the code in the base class, all JOB handlers now inherit it.

public class JobAdHocHandler : BaseHandler, IJobHandler
{
    public MinimumResultModel Handle(MinimumCommandModel message)
    {
        var result = new MinimumResultModel {Id = "-1", PayloadAsString = message.FullPayloadString};

        try
        {
            var info = message.MinimumPayload.JobInfo;

            SetupInstance(info); // <<-- SOLUTION (in BaseHandler)
            var job = JobHandler.GetJob(info); // <<-- SOLUTION (in BaseHandler)

            result.Id = BackgroundJob.Enqueue(() => job.Execute(null, message.FullPayloadString, JobCancellationToken.Null));
        }
        catch (Exception ex)
        {
            Log.Logger.Fatal(ex, ex.Message);
            result.Exception = ex;
        }

        AppDomain.Unload(JobAppDomain);
        return result;
    }
    public bool AppliesTo(JobType jobType) => jobType == JobType.AdHoc;
}

public class BaseHandler : MarshalByRefObject
{
    protected internal AppDomain JobAppDomain;
    protected internal BaseHandler JobHandler;

    protected internal void SetupInstance(JobInfoPayload info)
    {
        var ads = new AppDomainSetup
        {
            ApplicationBase = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName,
            DisallowBindingRedirects = false,
            DisallowCodeDownload = true,
            PrivateBinPath = info.JobClassName,
            ApplicationName = info.JobName,
        };
        JobAppDomain = AppDomain.CreateDomain(info.JobName, null, ads);
        JobHandler = (BaseHandler)JobAppDomain.CreateInstanceAndUnwrap(typeof(BaseHandler).Assembly.FullName, typeof(BaseHandler).FullName);
    }

    protected internal IJob GetJob(JobInfoPayload info)
    {
        var assembly = Assembly.LoadFrom(info.JobClassName + @"\" + info.JobClassName + ".dll");
        var assemblyType = assembly.GetType(info.AssemblyName);
        var job = Activator.CreateInstance(assemblyType) as IJob;
        if (job == null)
            throw new Exception("Unable to create job: " + info.JobClassName);
        return job;
    }
}

Seems to work well so far.

Keith Barrows
  • 24,802
  • 26
  • 88
  • 134