I have created an AppDomain with a different base directory. However, I cannot seem to load the currently executing assembly into the other AppDomain without having a copy of the current executing assembly in the base directory. I've even tried to load it from the bytes.
I get no exception when I try to load, but when I try to use:
domain.DoCallBack(new CrossAppDomainDelegate(...
I get:
Could not load file or assembly ........... The system cannot find the file specified.
My code is as follows:
private static void SaveAssemblies(Assembly ass, List<byte[]> assemblyByteList)
{
AssemblyName[] assNames = ass.GetReferencedAssemblies();
foreach (AssemblyName assName in assNames)
{
Assembly referedAss = Assembly.Load(assName);
if (!referedAss.GlobalAssemblyCache)
{
SaveAssemblies(referedAss, assemblyByteList);
}
}
byte[] rawAssembly = File.ReadAllBytes(ass.Location);
assemblyByteList.Add(rawAssembly);
}
public static AppDomain CreateAppDomain(string dir, string name)
{
AppDomainSetup domainSetup = new AppDomainSetup();
domainSetup.ApplicationBase = dir;
domainSetup.ApplicationName = Path.GetFileName(dir);
domainSetup.PrivateBinPath = Path.Combine(dir, "Libs");
AppDomain domain = AppDomain.CreateDomain(name, null, domainSetup);
//Load system assemblies needed for the module
List<byte[]> assemblyByteList = new List<byte[]>();
SaveAssemblies(Assembly.GetExecutingAssembly(), assemblyByteList);
foreach (byte[] rawAssembly in assemblyByteList)
domain.Load(rawAssembly);
domain.DoCallBack(new CrossAppDomainDelegate(SetupLogging));
return domain;
}
Update:
It seems the assembly is loaded if i look in output i see this
'TaskExecuter.Terminal.vshost.exe' (Managed (v4.0.30319)): Loaded 'NLog'
'TaskExecuter.Terminal.vshost.exe' (Managed (v4.0.30319)): Loaded 'TaskExecuter', Symbols loaded.
but i still get the exception... i don't understand this
System.IO.FileNotFoundException was unhandled Message=Could not load
file or assembly 'TaskExecuter, Version=1.0.4244.31921,
Culture=neutral, PublicKeyToken=null' or one of its dependencies. The
system cannot find the file specified. Source=mscorlib
FileName=TaskExecuter, Version=1.0.4244.31921, Culture=neutral,
PublicKeyToken=null FusionLog==== Pre-bind state information ===
LOG: User = Peter-PC\Peter LOG: DisplayName = TaskExecuter,
Version=1.0.4244.31921, Culture=neutral, PublicKeyToken=null
(Fully-specified) LOG: Appbase =
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks LOG: Initial
PrivatePath = C:\ProgramData\TaskExecuter\TaskLib\uTorrentTasks\Libs
Calling assembly : (Unknown).
=== LOG: This bind starts in default load context. LOG: Using
application configuration file: d:\users\peter\documents\visual studio
2010\Projects\TaskExecuter\TaskExecuter.Terminal\bin\Release\TaskExecuter.Terminal.vshost.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:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter.DLL.
LOG: Attempting download of new URL
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter/TaskExecuter.DLL.
LOG: Attempting download of new URL
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter.DLL.
LOG: Attempting download of new URL
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter/TaskExecuter.DLL.
LOG: Attempting download of new URL
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter.EXE.
LOG: Attempting download of new URL
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/TaskExecuter/TaskExecuter.EXE.
LOG: Attempting download of new URL
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter.EXE.
LOG: Attempting download of new URL
file:///C:/ProgramData/TaskExecuter/TaskLib/uTorrentTasks/Libs/TaskExecuter/TaskExecuter.EXE.
StackTrace:
at System.Reflection.RuntimeAssembly._nLoad(AssemblyName
fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly
locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound,
Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.nLoad(AssemblyName
fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly
locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound,
Boolean forIntrospection, Boolean suppressSecurityChecks)
at
System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName
assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark,
Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.InternalLoad(String
assemblyString, Evidence assemblySecurity, StackCrawlMark& stackMark,
Boolean forIntrospection)
at System.Reflection.Assembly.Load(String assemblyString)
at
System.Runtime.Serialization.FormatterServices.LoadAssemblyFromString(String
assemblyName)
at
System.Reflection.MemberInfoSerializationHolder..ctor(SerializationInfo
info, StreamingContext context)
at System.AppDomain.DoCallBack(CrossAppDomainDelegate
callBackDelegate)
at TaskExecuter.AppDomainHelper.CreateAppDomain(String dir,
String name) in d:\users\peter\documents\visual studio
2010\Projects\TaskExecuter\TaskExecuter\AppDomainHelper.cs:line 50
at TaskExecuter.TaskManagment.TaskFinder.Probe() in
d:\users\peter\documents\visual studio
2010\Projects\TaskExecuter\TaskExecuter\TaskManagment\TaskFinder.cs:line
29
at TaskExecuter.TaskManagment.TaskManager.LoadTasks() in
d:\users\peter\documents\visual studio
2010\Projects\TaskExecuter\TaskExecuter\TaskManagment\TaskManager.cs:line
63
at TaskExecuter.TaskManagment.TaskManager.Start() in
d:\users\peter\documents\visual studio
2010\Projects\TaskExecuter\TaskExecuter\TaskManagment\TaskManager.cs:line
95
at TaskExecuter.Terminal.Program.Main(String[] args) in
d:\users\peter\documents\visual studio
2010\Projects\TaskExecuter\TaskExecuter.Terminal\Program.cs:line 16
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly,
String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile,
Evidence assemblySecurity, String[] args)
at
Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object
state)
at System.Threading.ExecutionContext.Run(ExecutionContext
executionContext, ContextCallback callback, Object state, Boolean
ignoreSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext
executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
InnerException:
I was able to recover the linked to blog post using archive.org and also come up with a working solution.
My goal was to dynamically compile an exe into a temporary location, and then have that exe shadow load all main dlls in a child appdomain so that the main application that spawned the exe can be updated easily. The basic approach is using childAppDomain.CreateInstanceFrom to create a type that in the constructor installs the assembly resolve event handler. My code looked like
var exportAppDomain = AppDomain.CreateDomain(
runnerName,
null,
appDomainSetup,
new PermissionSet(PermissionState.Unrestricted));
exportAppDomain.CreateInstanceFrom(
Assembly.GetExecutingAssembly().Location,
"ExportLauncher.AppDomainResolver",
true,
BindingFlags.Public | BindingFlags.Instance,
null,
new object[] { Assembly.GetExecutingAssembly().Location },
null,
null);
And the type that creates the needed AssemblyResolve handler (the blog post below describes why you need another type)
class AppDomainResolver
{
string _sourceExeLocation;
public AppDomainResolver(string sourceExeLocation)
{
_sourceExeLocation = sourceExeLocation;
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}
Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
if (args.Name.Contains("exporterLauncher")) // why does it not already know it has this assembly loaded? the seems to be required
return typeof(AppDomainResolver).Assembly;
else
return null;
}
}
Here is the original blog post:
Application Domains is hard…
Have you ever been working with Application Domain in .NET?, in the beginning it doesn’t seem all that difficult, but ones you get to know them you begin to realize all the little difficulties.
Everything works fine as long as you don’t move outside the Host AppDomains.BaseDirectory, but in our case we wanted to have Plug-ins deployed at say location “C:\My Plug-ins” while the host application would run at “C:\Program Files\My App”, since we might run into dependencies from the AppDomain to some of the Host Assemblies problems was apparently inevitable.
The Classic
Here is some simple code and our first attempt.
1: string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
2: AppDomainSetup setup = new AppDomainSetup
3: {
4: ApplicationName = name,
5: ApplicationBase = applicationBase,
6: PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
7: PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
8: ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
9: };
10:
11: Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
12: AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);
Seems very simple, but because “ApplicationBase” is different from “AppDomain.CurrentDomain.BaseDirectory” we ran into what seems to be a very well know exception.
System.IO.FileNotFoundException: Could not load file or assembly 'Host.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
If you have worked with any sort of dynamically loading assemblies I am fairly sure that is familiar to you. And the issue is that “Host.Services” was know within the Host Application Domain because it is stored in “C:\Program Files\My App”, and the Application Domain looking for it is looking in “C:\My Plug-ins”.
Well we Thought we instructed it to also look in “AppDomain.CurrentDomain.BaseDirectory” which would be “C:\Program Files\My App”, but that was not the case.
AppDomain.AssemblyResolve to the rescue?
Ok so we have been working with these quirks before, so we knew how we could use “AppDomain.AssemblyResolve” to manually resolve any assemblies that the AppDomain it self could not handle.
1: string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
2: AppDomainSetup setup = new AppDomainSetup
3: {
4: ApplicationName = name,
5: ApplicationBase = applicationBase,
6: PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
7: PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
8: ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
9: };
10:
11: Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
12: AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);
13: domain.AssemblyResolve += Resolve;
That should work right, well we thought so and ones again we were wrong, what happens now is that instead of actually getting as far as initializing the Application Domain and using it, instead it fails right where we hooked up the event handler for resolving assemblies.
Again the exception looks very much like the previous mentioned, but this time it can’t find the Assembly that contains the Type that has the “Resolve” handler we set up in the very last line in the above snippet.
AppDomain.Load then!
Ok, so obviously when hooking up the event handler, the Application Domain needs to know the Type of the object handling that event, that is actually fairly understandable when you think about it, so if the Application Domain can’t even find that one and load we can’t really handle anything.
So what is next? Our idea was to manually instruct the Application Domain to load a shallow assembly that didn’t have any other dependencies that what could be found in the GAC, and the hook an event handler up.
1: string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
2: AppDomainSetup setup = new AppDomainSetup
3: {
4: ApplicationName = name,
5: ApplicationBase = applicationBase,
6: PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
7: PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
8: ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
9: };
10:
11: Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
12: AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);
13: domain.Load(File.ReadAllBytes(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Host.AssemblyLoader.dll")));
14: domain.AssemblyResolve += new AssemblyLoader(AppDomain.CurrentDomain.BaseDirectory).Handle;
Using a very simple little class like the following, and don’t mind the odd Resolve behavior.
1: [Serializable]
2: public class AssemblyLoader
3: {
4: private string ApplicationBase { get; set; }
5:
6: public AssemblyLoader(string applicationBase)
7: {
8: ApplicationBase = applicationBase;
9: }
10:
11: public Assembly Resolve(object sender, ResolveEventArgs args)
12: {
13: AssemblyName assemblyName = new AssemblyName(args.Name);
14: string fileName = string.Format("{0}.dll", assemblyName.Name);
15: return Assembly.LoadFile(Path.Combine(ApplicationBase, fileName));
16: }
17: }
So yes or no?… NO!… same problem still.
Things are much more simple!
Actually things became much more simple in the end when we managed to make it work.
I Can’t say how exactly the .NET team has envisioned that this should work, we couldn't really find out any useable things that the “PrivateBinPath” and “PrivateBinPathProbe” was used for. Well we use them now, and made them work as we expected they would!
So we changed the “AssemblyLoader” class to look like this instead:
1: [Serializable]
2: public class AssemblyLoader : MarshalByRefObject
3: {
4: private string ApplicationBase { get; set; }
5:
6: public AssemblyLoader()
7: {
8: ApplicationBase = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;
9: AppDomain.CurrentDomain.AssemblyResolve += Resolve;
10: }
11:
12: private Assembly Resolve(object sender, ResolveEventArgs args)
13: {
14: AssemblyName assemblyName = new AssemblyName(args.Name);
15: string fileName = string.Format("{0}.dll", assemblyName.Name);
16: return Assembly.LoadFile(Path.Combine(ApplicationBase, fileName));
17: }
18: }
So rather than hooking up the event where we created the Application Domain, we let the class do it by it self, and to “CurrentDomain” instead.
Ok so wait, doesn’t that cause an issue when creating it in the factory since it is now loading for the wrong domain? Well thankfully you are able to create objects within domains from the outside.
So creating the domain is now done as follows:
1: string applicationBase = Path.GetDirectoryName(interOperabilityPackageType.AssemblyDescription.AssemblyPath);
2: AppDomainSetup setup = new AppDomainSetup
3: {
4: ApplicationName = name,
5: ApplicationBase = applicationBase,
6: PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory,
7: PrivateBinPathProbe = AppDomain.CurrentDomain.BaseDirectory,
8: ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
9: };
10:
11: Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
12: AppDomain domain = AppDomain.CreateDomain(name, evidence, setup);
13: domain.CreateInstanceFrom(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Host.AssemblyLoader.dll"),"Host.AssemblyLoader");
We don’t even care for maintaining a reference to the “AssemblyLoader” since it should pretty much be kept alive by hooking it self up to the Event.
Hopefully this can help some that has stumbled over the same problem, I see many workarounds where people then either just let Plug-ins be installed in same host directory, having all the necessary dependencies deployed along with the plug-in even though it isn’t something the plug-in knows it is dependant on and so forth.
The above at least enables us to install plug-ins away from our host application base which I think is nice.
If anyone have solved this differently, then please make a response, maybe we can find pros and cons in either way, or just discover a better solution.
If you have any questions or can’t get the above to work, then feel free to ask.
author: Jens Melgaard | posted # Thursday, July 01, 2010 3:08 PM | Feedback (0)
Is there any reason why you do not use the original assemblies ?
So unless your foreign appdomain uses credentials that prevent it from accessing the original assemblies, the method AppDomain.CreateInstanceFromAndUnwrap is capable of doing so.
I suggest you isolate your remotely executed code in a MarshalByRefObject class, using a class like this :
public class MyRemoteClass : MarshalByRefObject
{
public void SetupLogging()
{
// ...
}
}
And use it like this :
var assemblyPath = new Uri(typeof(MyRemoteClass).Assembly.CodeBase).LocalPath;
var remote = (MyRemoteClass)domain.CreateInstanceFromAndUnwrap(assemblyPath, "NameSpace.MyRemoteClass");
remote.SetupLogging();
This will avoid the unnecessary trouble of passing return values via appdomain state, as DoCallBack does not return values. This will also avoid mixing AppDomain plumbing code with your application logic.
Finally, you may need to intercept AppDomain.AssemblyResolve inside MyRemoteClass for other dependencies to load properly, though.
Found a solution after loading the assembly from byte setting the .GetName().CodeBase to null resolved the problem...
After looking around i found this page and it has a better solution then mine!
According to http://msdn.microsoft.com/en-us/library/aehss7y0.aspx the behaviour of AppDomain.CreateDomain has changed with .NET4 and you should use http://msdn.microsoft.com/en-us/library/ms130766.aspx and setup Evidence and grants "manually"...
If you need to load assembly yourself avoid loading from bytes... I'd recommend to use at least loading by full assembly path.
In general to investigate problems with loading assemblies serach for "fusion log viewer" ( http://www.bing.com/search?q=fussion+log+viewer ) and use the tool to see where code tries to load assemblies from.
My educated guess is that you missed an important part of the error message:
System.IO.FileNotFoundException was unhandled Message=Could not load file or
assembly 'TaskExecuter, Version=1.0.4244.31921, Culture=neutral, PublicKeyToken=null'
or one of its dependencies. The system cannot find the file specified. Source=mscorlib
Besides not being able to load your assembly from another location than under your ApplicationBase there is probably some dependend assembly missing from where it could be resolved and loaded.
By the way, if you start loading from bytes you should have a look at the assemblies loaded to your domain. The dependend assembly might be loaded already, but the dependency cannot be resolved automatically. If you have the same assembly loaded twice, its types will be incompatible. You'll get funny CastExceptions saying an object of YourClass cannot be cast to YourClass.
You can try to register an AssemblyResolve event handler to your domain, but with this you end up easily with some black magic conjuring stuff from .dll hell.
If everything else fails and you go to .dll hell yourself, meet me here:
Need to hookup AssemblyResolve event when DisallowApplicationBaseProbing = true
Related
I'm trying C# appdomain on win10 using vs2017, I've got this quick example. I've a directory called c:\git, I can create files under this directory with C# app, but when I tried app domain, it throws exception, my code as below:
class UseAppDomain
{
public static void Test()
{
var perm = new PermissionSet(PermissionState.None);
perm.AddPermission(
new SecurityPermission(SecurityPermissionFlag.Execution));
perm.AddPermission(
new FileIOPermission(FileIOPermissionAccess.NoAccess, #"c:\"));
var setup = new AppDomainSetup();
setup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
AppDomain secureDomain = AppDomain.CreateDomain("secure", null, setup, perm);
ThirdParty third = new ThirdParty();
Type thirdParty = typeof(ThirdParty);
secureDomain.
CreateInstanceAndUnwrap(thirdParty.Assembly.FullName,
thirdParty.FullName); //exception!!!!!!!!!!
AppDomain.Unload(secureDomain);
}
}
[Serializable]
class ThirdParty
{
public ThirdParty()
{
Console.WriteLine("3p loadling");
System.IO.File.Create(#"c:\git\test.txt");//Try to create file failed!
}
}
The exception message is:
Unhandled Exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.FileIOPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
at System.Security.CodeAccessSecurityEngine.Check(Object demand, StackCrawlMark& stackMark, Boolean isPermSet)
at System.Security.CodeAccessSecurityEngine.Check(CodeAccessPermission cap, StackCrawlMark& stackMark)
at System.Security.CodeAccessPermission.Demand()
... ...
I don't quite get what problem my program has, how to fix this issue?
Thanks.
If you want to create files from a partially trusted domain you need to use FileIOPermissionAccess.Write instead. Or FileIOPermissionAccess.AllAccess if you want to allow also reading and directory content discovery.
Side note:
You use the CreateInstanceAndUnwrap for a simple serializable class, which is not derived from MarshalByRefObject. Its effect is that the class will be serialized in the created domain and a copy will be deserialized in the main domain but as you omit the return value it will be dropped anyway.
So either do not unwrap the created object or derive it from the MarshalByRefObject class so its public members can be accessed from the main domain via remoting.
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
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.
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.
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.
I'm trying to make it so that we can generate certain reports daily and email them to a bunch of people in a list.
I've tested out Hangfire for recurring jobs and it works well. So that is not an issue. But I'm trying to create a report from my existing Crystal Report file (.rpt). Basically I want to make it so that when this job gets executed, the code would create the report, save it to the disk in a specified path as a PDF, and then I can email it to people as an attachment. So there is no need to be able to see the report on a web page. The idea is to literally just generate the report in the code behind, save it as a PDF, and email it from the code behind after it is saved.
The issue I'm having has to do with the actual generating and saving of the crystal report. Btw, I'm generating an excel file in the test but I'd change it to PDF for the actual report. This is what I have so far for generating the report:
string path = #"Save folder relative-path";
//"report" is declared at the class level and instantiated below.
report = new ReportDocument();
report.SetDatabaseLogon(ConfigurationManager.AppSettings["Username"], ConfigurationManager.AppSettings["Password"]);
report.Load(Server.MapPath("Relative path to the report"));
report.SetDataSource(GetDataSet()); //This gets the dataset filled with data for the report
try
{
ExportOptions options = new ExportOptions();
DiskFileDestinationOptions diskFileOptions = new DiskFileDestinationOptions();
ExcelFormatOptions excelOptions = new ExcelFormatOptions();
diskFileOptions.DiskFileName = path + "Test Report.xls";
options.ExportDestinationType = ExportDestinationType.DiskFile;
options.ExportFormatType = ExportFormatType.Excel;
options.ExportDestinationOptions = diskFileOptions;
options.ExportFormatOptions = excelOptions;
report.Export();
/*
This is where I would call a method to email the report to people
*/
}
catch (Exception ex)
{
Console.WriteLine("Error generating report: " + ex.Message);
}
This code is in a method which is being called at Application_Start in the global.asax file of the web application. When I run the application, the job fails and throws this error when I look under the failed jobs in the Hangfire dashboard even though I know the path in my code is correct:
System.IO.FileNotFoundException Could not load file or assembly
'App_global.asax.twm32qri, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null' or one of its dependencies. The system cannot
find the file specified.
System.IO.FileNotFoundException: Could not load file or assembly
'App_global.asax.twm32qri, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null' or one of its dependencies. The system cannot
find the file specified. File name: 'App_global.asax.twm32qri,
Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' at
System.RuntimeTypeHandle.GetTypeByName(String name, Boolean
throwOnError, Boolean ignoreCase, Boolean reflectionOnly,
StackCrawlMarkHandle stackMark, IntPtr pPrivHostBinder, Boolean
loadTypeFromPartialName, ObjectHandleOnStack type) at
System.RuntimeTypeHandle.GetTypeByName(String name, Boolean
throwOnError, Boolean ignoreCase, Boolean reflectionOnly,
StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean
loadTypeFromPartialName) at System.Type.GetType(String typeName,
Boolean throwOnError, Boolean ignoreCase) at
Hangfire.Storage.InvocationData.Deserialize()
WRN: Assembly binding logging is turned OFF. To enable assembly bind
failure logging, set the registry value
[HKLM\Software\Microsoft\Fusion!EnableLog] (DWORD) to 1. Note: There
is some performance penalty associated with assembly bind failure
logging. To turn this feature off, remove the registry value
[HKLM\Software\Microsoft\Fusion!EnableLog].
EDIT:
I have another error I'm getting too. This one has something to do
with loading the report file.
Failed An exception occurred during performance of the job.
CrystalDecisions.CrystalReports.Engine.LoadSaveReportException
Invalid report file path.
CrystalDecisions.CrystalReports.Engine.LoadSaveReportException:
Invalid report file path. at
CrystalDecisions.CrystalReports.Engine.ExceptionThrower.ThrowEngineException(String
messageID, EngineExceptionErrorID id) at
CrystalDecisions.CrystalReports.Engine.ReportDocument.Load(String
filename, OpenReportMethod openMethod, Int16 parentJob) at
CrystalDecisions.CrystalReports.Engine.ReportDocument.EnsureLoadReport()
at
CrystalDecisions.CrystalReports.Engine.ReportDocument.SetDatabaseLogon(String
user, String password) at Intranet.Global.GenerateReport() in
path\Global.asax.cs:line 98
Figured out the issue. I apparently needed to use a CrystalReportViewer object and set the ReportDocument object as its source. The CrystalReportViewer class is in the CrystalDecisions.Web namespace.
using (ReportDocument report = new ReportDocument())
{
using (CrystalReportViewer viewer = new CrystalReportViewer())
{
string path = System.Web.Hosting.HostingEnvironment.MapPath(#"Destination path here");
report.Load(System.Web.Hosting.HostingEnvironment.MapPath(#"Path to .rpt file here"));
report.SetDatabaseLogon(ConfigurationManager.AppSettings["Username"], ConfigurationManager.AppSettings["Password"]);
string file = path + "TestReport.xls";
//These two lines below are important. The report won't generate without them.
viewer.ReportSource = report;
viewer.RefreshReport();
//Just deleting the file if it exists.
if (File.Exists(file))
File.Delete(file);
report.ExportToDisk(ExportFormatType.Excel, diskFileOptions.DiskFileName);
}
}
What is controlling the Evidence of the current app domain?
var evidence = Thread.GetDomain().Evidence;
What controls if it is null or non-null, and what determines it contents?
When my application queries these host evidence objects from the domain evidence
var z = evidence.GetHostEvidence<Zone>
var p = evidence.GetHostEvidence<Publisher>
var s = evidence.GetHostEvidence<Site>
var n = evidence.GetHostEvidence<StrongName>
var u = evidence.GetHostEvidence<Url>
it appears as if they are sometimes all null when executing in some environments. The reason I believe this is an exception thrown inside IsolatedStorage._GetAccountingInfo(...), where by looking at the code in reflector it is clear that this exception will only be thrown if the domain evidence contains null for all of the above host evidence objects. This will cause isolated storage to fail to initialize.
Unfortunately I can't reproduce it on my own system. The Zone value for example will always be a proper value saying "My Computer", so I'm struggling to solve this.
What controls the contents of these values in the default app domain of a windows forms desktop application?
A similar situation occurs when your code is a COM object accessed by a native Win32 application (the Default AppDomain's Evidence is empty), or when it is loaded and run inside of the command line version of PowerShell.exe. I ran into this problem when using an OpenXML (specifically EPPlus) assembly that uses IsolatedStorage when the office document is over a certain file size.
Rather than spinning up another AppDomain inside of the default one and dealing with an additional level of Marshaling/Remoting, I prefer to use reflection to muck with the current AppDomain's evidence.
Here's the proof of concept in C#:
using System;
namespace AppDomainEvidence
{
class Program
{
static void Main(string[] args)
{
var initialAppDomainEvidence = System.Threading.Thread.GetDomain().Evidence; // Setting a breakpoint here will let you inspect the current AppDomain's evidence
try
{
var usfdAttempt1 = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForDomain(); // This will fail when the current AppDomain Evidence is instantiated via COM or in PowerShell
}
catch (Exception e)
{
// Set breakpoint here to inspect Exception "e"
}
// Create a new Evidence that include the MyComputer zone
var replacementEvidence = new System.Security.Policy.Evidence();
replacementEvidence.AddHostEvidence(new System.Security.Policy.Zone(System.Security.SecurityZone.MyComputer));
// Replace the current AppDomain's evidence using reflection
var currentAppDomain = System.Threading.Thread.GetDomain();
var securityIdentityField = currentAppDomain.GetType().GetField("_SecurityIdentity", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
securityIdentityField.SetValue(currentAppDomain,replacementEvidence);
var latestAppDomainEvidence = System.Threading.Thread.GetDomain().Evidence; // Setting a breakpoint here will let you inspect the current AppDomain's evidence
var usfdAttempt2 = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForDomain(); // This should work
}
}
}
And here's the workaround I implemented in PowerShell:
# This one will fail
$usfd = [System.IO.IsolatedStorage.IsolatedStorageFile]::GetUserStoreForDomain()
# Inspect the current AppDomain's Evidence
[threading.thread]::GetDomain().Evidence
# Modify the current AppDomain's Evidence
$evidence = new-object System.Security.Policy.Evidence
$zone = new-object System.Security.Policy.Zone('MyComputer')
$evidence.AddHost($zone)
$currentAppDomain = [threading.thread]::GetDomain()
$securityIdentityField=$currentAppDomain.GetType().GetField('_SecurityIdentity','Instance,NonPublic')
$securityIdentityField.SetValue($currentAppDomain, $evidence)
# Inspect the current AppDomain's Evidence
[threading.thread]::GetDomain().Evidence
# This one will succeed
$usfd = [System.IO.IsolatedStorage.IsolatedStorageFile]::GetUserStoreForDomain()
It turns out the culprit was indeed an "unusual deployment scenario" as was suggested by Hans in a comment on my question. We use encryption (enveloping) for a few assemblies, and apparently this tampers with the evidence required by isolated storage.
I think I've determined that even though I'm loading assemblies in a MarshalByRefObject in a new AppDomain that the assemblies are also getting loaded into the parent domain.
Here's my Assembly structure (arrows indicate dependency):
MainAssembly -> CommonInterfaceAssembly <- ExtensionAssembly
In the parent AppDomain I'm doing this:
var loader = (ExtensionLoader)extDomain.CreateInstanceAndUnwrap (Assembly.GetExecutingAssembly().FullName, "ExtensionLoader");
loader.loadExtensions (this);
and the Loader class is:
class ExtensionLoader : MarshalByRefObject
{
public List<IExtension> loadExtensions (ExtensionMgr mgr)
{
// Delegate to Addins to return the list of extensions
AddinManager.Initialize ();
AddinManager.Registry.Update ();
AddinManager.GetExtensionObjects<IExtension> ();
var extensions = new List<IExtension> (AddinManager.GetExtensionObjects<IExtension> ());
foreach (var ext in extensions) {
ext.Initialize (mgr);
}
return extensions;
}
}
I don't know if it's relevant to the question, but I am using Mono.Addins to load the extensions in the new AppDomain so I've left that code in. From what I can tell though things work fine up to the point where I invoke the Initialize method on each of the extensions.
So I have ran this scenario with the ExtensionAssembly in the same directory as the main executable and in a separate 'extensions' directory. What's curious to me is that when I invoke ext.Initialize either the ExtensionAssembly gets loaded in the parent AppDomain (if it exists in the same directory), or I get the below stack trace if not. Any ideas why?
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke (System.Runtime.Remoting.Proxies.RealProxy rp, IMessage msg, System.Exception& exc, System.Object[]& out_args) [0x001f0] in /home/tim/tmp/mono-2.10.8/mcs/class/corlib/System.Runtime.Remoting.Proxies/RealProxy.cs:247
Exception rethrown at [1]:
---> System.IO.FileNotFoundException: Could not load file or assembly 'Extensions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
File name: 'Extensions, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'
at System.AppDomain.Load (System.String assemblyString, System.Security.Policy.Evidence assemblySecurity, Boolean refonly) [0x00047] in /home/tim/tmp/mono-2.10.8/mcs/class/corlib/System/AppDomain.cs:785
at System.AppDomain.Load (System.String assemblyString) [0x00000] in /home/tim/tmp/mono-2.10.8/mcs/class/corlib/System/AppDomain.cs:762