Tuesday, November 12, 2013

InternalGetSatelliteAssembly - Finding resources in .NET v4.x

I recently worked through an issue with resource loading in an ASP.NET v4.0 web application. The specific issue I dealt with was related to a third party component called Syncfusion XlsIO. I was working with XlsIO v9.403.0.62, which is a .NET v3.5 assembly. However, I believe the same issue would occur regardless of the specific assembly.

The underlying exception that we needed to resolve is:

System.TypeInitializationException: The type initializer for 'Syncfusion.XlsIO.Implementation.Shapes.ShapeFillImpl' threw an exception. ---> System.IO.FileNotFoundException: Could not find file 'Syncfusion.XlsIO.Base.resources'.
   at System.Reflection.RuntimeAssembly.InternalGetSatelliteAssembly(String name, CultureInfo culture, Version version, Boolean throwOnFileNotFound, StackCrawlMark& stackMark)
   at System.Resources.ManifestBasedResourceGroveler.GetSatelliteAssembly(CultureInfo lookForCulture, StackCrawlMark& stackMark)
   at System.Resources.ManifestBasedResourceGroveler.GrovelForResourceSet(CultureInfo culture, Dictionary`2 localResourceSets, Boolean tryParents, Boolean createIfNotExists, StackCrawlMark& stackMark)
   at System.Resources.ResourceManager.InternalGetResourceSet(CultureInfo requestedCulture, Boolean createIfNotExists, Boolean tryParents, StackCrawlMark& stackMark)
   at System.Resources.ResourceManager.InternalGetResourceSet(CultureInfo culture, Boolean createIfNotExists, Boolean tryParents)
   at System.Resources.ResourceManager.GetObject(String name, CultureInfo culture, Boolean wrapUnmanagedMemStream)
   at Syncfusion.XlsIO.Implementation.Shapes.ShapeFillImpl.GetResData(String strID)
   at Syncfusion.XlsIO.Implementation.Shapes.ShapeFillImpl..cctor()
   --- End of inner exception stack trace ---
   at Syncfusion.XlsIO.Implementation.Shapes.ShapeFillImpl..ctor(IApplication application, Object parent)
   at Syncfusion.XlsIO.Implementation.Charts.ChartBorderImpl..ctor(IApplication application, Object parent)
   at Syncfusion.XlsIO.Implementation.Charts.ChartFrameFormatImpl.SetDefaultValues(Boolean bAutoSize, Boolean bIsInteriorGray)
   at Syncfusion.XlsIO.Implementation.Charts.ChartTextAreaImpl.CreateFrameFormat()
   at Syncfusion.XlsIO.Implementation.Charts.ChartTextAreaImpl.InitFrameFormat()
   at Syncfusion.XlsIO.Implementation.Charts.ChartImpl.CreateChartTitle()
   at Syncfusion.XlsIO.Implementation.Charts.ChartImpl..ctor(IApplication application, Object parent)
   at Syncfusion.XlsIO.Implementation.Collections.ShapesCollection.AddChart()
   at Syncfusion.XlsIO.Implementation.Collections.WorksheetChartsCollection.Add()

Each .NET assembly may contain resources (strings, images, etc). The resources may be embedded in the main assembly, or they may be compiled into satellite assemblies. At run-time, the System.Resources.ResourceManager class needs to find the assembly that contains the resources for the current culture. If no culture-specific resources are found, then ResourceManager needs to find the neutral resources. Each .NET assembly may contain the NeutralResourcesLanguage attribute, which defines the language and location of the neutral resources. If an assembly does not explicitly define the NeutralResourcesLanaguage attribute, then .NET will look in the default fallback location. In .NET v2.0/v3.5, the default fallback location for neutral resources was assumed to be in the main assembly. In .NET v4.0, the default fallback location for neutral resources is assumed to be in the satellite assembly.

In this particular case, we have an ASP.NET v4.0 Application Pool process (w3wp.exe) that is loading a .NET v3.5 DLL (Syncfusion.XlsIO.Base.dll). Syncfusion.XlsIO.Base.dll contains the neutral langauge resources embedded within that main assembly. Because Syncfusion.XlsIO.Base.dll is a .NET v3.5 assembly, there was no need to explicitly set the default fallback location to be the main assembly (since that was the default fallback location in .NET v3.5). However, since the Application Pool process is running in .NET v4.0, it assumes that the default fallback location is the satellite assembly, so it looks for an assembly called Syncfusion.XlsIO.Base.Resources.dll. It cannot find that file, and throws the exception.

We ran into a similar issue not long ago. In that case, we modified the .NET v3.5 assemblies to include the following attribute:

<Assembly: System.Resources.NeutralResourcesLanguage("en-US", System.Resources.UltimateResourceFallbackLocation.MainAssembly)>

By explicitly specifying UltimateResourceFallbackLocation.MainAssembly, we can tell the .NET v4.0 run-time to look in the main assembly for the neutral resources. That worked previously, because we own the assemblies being loaded, so we were able to recompile those assemblies with the NeutralResourcesLanguage attribute as shown above. However, in this recent case, the assembly generating the error is Syncfusion.XlsIO.Base.dll. That is not our assembly, so we cannot recompile it with the NeutralResourcesLanguage attribute. So, we needed a different solution - a solution that does not require modifying the "off the shelf" assembly.

After some trial and error (and some prayer), I figured out a solution:

1. Use ILSpy (Ildasm would probably work) to open the "off the shelf" Syncfusion.XlsIO.Base.dll assembly

2. Save each of the embedded resources into a separate *.resources file:
     Syncfusion.XlsIO.Localization.sr.resources
     Syncfusion.XlsIO.PresetGradients.resources
     Syncfusion.XlsIO.TexturePatternGradient.resources
     Syncfusion.XlsIO.ToolBoxIcons.XlsIO.bmp
     Syncfusion.XlsIO.VMLPresetGradientFills.resources

3. Use Assembly Linker to package the *.resources files into a satellite assembly (called Syncfusion.XlsIO.Base.Resources.dll). I ended up creating a *.bat file that contains the following command:

    "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\al.exe" /embed:Syncfusion.XlsIO.Localization.sr.resources

/embed:Syncfusion.XlsIO.PresetGradients.resources /embed:Syncfusion.XlsIO.TexturePatternGradient.resources

/embed:Syncfusion.XlsIO.ToolBoxIcons.XlsIO.bmp /embed:Syncfusion.XlsIO.VMLPresetGradientFills.resources /culture:en

/out:Syncfusion.XlsIO.Base.Resources.dll /version:9.403.0.62

4. Copy the newly created Syncfusion.XlsIO.Base.Resources.dll into the ASP.NET v4.0 application's ./bin folder

With the Syncfusion.XlsIO.Base.Resources.dll in the ./bin folder, the w3wp.exe process is able to load the resources correctly (from this new satellite assembly) and the exception no longer occurs.


1 comment:

  1. If a calling assembly is signed with a SNK, the satellite assembly has to be signed with the same SNK. This means that you were very fortunate that `Syncfusion.XlsIO.Base.dll` wasn't signed, because obviously you wouldn't have had access to that company's SNK for your "fake satellite".

    ReplyDelete