Reputation: 3734
Here's a problem which cost me a whole day. I figured I'd post the solution in case anyone else hits it.
I have a .NET Office plug-in which calls an ASMX web service. It gets a valid XML response, but the XmlSerializationReader can't deserialize the response properly. It fails to find one of my DLLs, and throws a FileNotFoundException.
The basic structure is this:
Utility Assembly -- contains some commonly-used utility classes which DTO's depend on.
namespace MyCompany.CommonUtilities
{
[Serializable]
public class UtilityClass
{
// Various fields and properties..
}
}
DTO Assembly -- contains serializable objects for web service calls
using MyCompany.CommonUtilities;
namespace MyCompany.DtoObjects
{
[Serializable]
public class WebResponseDto
{
// Various other fields and properties..
public UtilityClass Property1 { get; set; }
}
}
This all worked great under .NET 3.5, and still does. If I run our old plugin, built against 3.5, everything works.
What happened though, is that in .NET 4.5 they completely reworked the way Xml serializers are generated at runtime. See https://stackoverflow.com/a/14699617/3183464 and the README for .NET 4.5. The end result is that the Xml Serializer can't find the correct DLL for my Utilities project.
When I compile to .NET 4.5 the web service returns a properly serialized object, but it can't be deserialized on the client side. Instead I just get
Exception: There is an error in XML document (1, 997).
System.IO.FileNotFoundException: Could not load file or assembly 'MyCompany.CommonUtilities, Version=6.0.0.23028, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderMyCompanyService.Read101_WebResponseDto(Boolean isNullable, Boolean checkType)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderMyCompanyService.Read258_WebMethodResponse()
at Microsoft.Xml.Serialization.GeneratedAssembly.ArrayOfObjectSerializer285.Deserialize(XmlSerializationReader reader)
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
Which was confusing, since the Utilities DLL was already loaded before the webservice was even called.
Looking into the Fusion logs, I saw two distinct load events for the utility assembly. The first was when my Visio plugin first loaded, since it depends on the utility classes:
LOG: DisplayName = MyCompany.CommonUtilities, Version=6.0.0.23028, Culture=neutral, PublicKeyToken=null (Fully-specified)
LOG: Appbase = file:///C:/Program Files (x86)/Microsoft Office/Office14/
Calling assembly : MyCompany.VisioAddIn, Version=6.0.0.23028, Culture=neutral, PublicKeyToken=null.
LOG: This bind starts in LoadFrom load context.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities.DLL.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities/MyCompany.CommonUtilities.DLL.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities.EXE.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities/MyCompany.CommonUtilities.EXE.
LOG: Attempting download of new URL file:///C:/Users/MyUser/Documents/DevProjects/VisioAddIn/bin/Debug/MyCompany.CommonUtilities.DLL.
LOG: Assembly download was successful. Attempting setup of file: C:\Users\MyUser\Documents\DevProjects\VisioAddIn\bin\Debug\MyCompany.CommonUtilities.dll
Notice the context: It calls Assembly.LoadFrom()
, and it finds the DLL.
But then later, the serialization assembly generated at run-time tries to load the same assembly in order to deserialize a DTO, and can't find it:
LOG: DisplayName = MyCompany.CommonUtilities, Version=6.0.0.23028, Culture=neutral, PublicKeyToken=null (Fully-specified)
LOG: Appbase = file:///C:/Program Files (x86)/Microsoft Office/Office14/
Calling assembly : (Unknown).
LOG: This bind starts in default load context.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities.DLL.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities/MyCompany.CommonUtilities.DLL.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities.EXE.
LOG: Attempting download of new URL file:///C:/Program Files (x86)/Microsoft Office/Office14/MyCompany.CommonUtilities/MyCompany.CommonUtilities.EXE.
LOG: All probing URLs attempted and failed
See what happened?
The assembly generated at runtime doesn't realize it needs to look in the folder where my plug-in lives. It just calls Assembly.Load()
, which uses the default load context.
Since it's running as part of Visio.EXE, Fusion searches the obvious place: C:\Program Files (x86)\Office
, and doesn't find anything. Result: File not found.
Upvotes: 1
Views: 315
Reputation: 3734
Configure .NET to use legacy serialization generation.
<xmlSerializer useLegacySerializerGeneration="true"/>
This didn't seem to do anything. Also, the only way to do this would have been to add a Visio.exe.config file to the Program Files folder, and this has all sorts of Group Policy and permissions implications. Regardless, it didn't work.
Project Properties > Build > "Generate serialization assembly" set to "On"
This generated a serialization DLL, but the DLL wasn't found by Fusion just like the utility DLL wasn't found. .NET's serialization engine isn't looking in the right place.
Attaching an event handler to the AppDomain and catching exceptions when Assembly.Load()
fails.
I check to see if the assembly is one of mine. If so, I look to see if it's already loaded -- yes, Fusion can fail to load an assembly which is already loaded. If it's not loaded, I look in my plugin's folder to see if I can find the right DLL.
Result: Everything works again.
public void PlugInStartup()
{
// (other stuff...)
// This allows dynamically-generated DLLs (Xml serializers) to find our DLLs.
// Because they're not in C:\Program Files\Office, they often can't be found.
AppDomain.CurrentDomain.AssemblyResolve += FindUnresolvedAssembly;
}
private Assembly FindUnresolvedAssembly(object sender, ResolveEventArgs args)
{
string fileName = args.Name.Split(',')[0] + ".dll";
if (!fileName.Contains("MyCompany"))
// Not our problem
return null;
Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
Assembly alreadyLoadedAssembly = loadedAssemblies
.FirstOrDefault(a => a.FullName == args.Name);
if (alreadyLoadedAssembly != null)
return alreadyLoadedAssembly;
try
{
string
pluginDir = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
assemblyPath = System.IO.Path.Combine(pluginDir, fileName);
if (File.Exists(assemblyPath))
return Assembly.LoadFrom(assemblyPath);
}
catch
{ }
return null;
}
Upvotes: 1