WPF AppDomain AssemblyResolve being called when it shouldn't - by ShiverCube

Status : 

  By Design<br /><br />
		The product team believes this item works according to its intended design.<br /><br />
		A more detailed explanation for the resolution of this particular item may have been provided in the comments section.


4
0
Sign in
to vote
ID 526836 Comments
Status Closed Workarounds
Type Bug Repros 4
Opened 1/24/2010 10:40:57 PM
Access Restriction Public

Description

The information located at http://msdn.microsoft.com/en-us/library/sb6a8618(VS.100).aspx is incorrect: "The default resource is the only resource that is compiled with the main assembly. Unless you specify a satellite assembly using the NeutralResourcesLanguageAttribute, it is the ultimate fallback (final parent)." In .NET 4.0 the default has changed to the satellite assembly.

The behaviour in .NET 4.0 regarding the AssemblyResolve event is different than the behaviour implemented in .NET 3.5, causing existing .NET 3.5 applications to break. In .NET 4.0 the AssemblyResolve event is raised for both missing assemblies and for missing resources. In .NET 3.5, AssemblyResolve is only raised when an assembly is missing. When a satellite resource is missing, a MissingSatelliteAssemblyException is thrown (causing the application to crash or for the DispatcherUnhandledException event to be raise), and AssemblyResolve is never called. This should be the same behaviour for .NET 4.0.
Sign in to post a comment.
Posted by Daniele Mancini on 7/20/2010 at 2:21 AM
Thanks Michael for your answer. I can understand that a situation such as the one you are describing, could indeed require that satellite assemblies are resolved as standard assemblies.

Regarding the advice of using the AssemblyName, I performed some tests and verified that creating the AssemblyName object is too costly.
My test is quite simple: checking 80 assembly names (taken from my current scenario) for N times (where N is the number of repetitions used to retrieve a consistent mean value... I used 1000 and 1000000 repetitions).
The result of such a test shows that using AssemblyName is 30 times slower than performing the proposed string split/parse method. I even created a faster version of the function, that avoids to split the string and relies on the assumption that the assembly name is always well formed (which is always true when handling the AssemblyResolve event); such a function is 100 times faster than using the AssemblyName.

In case you or someone else wants to verify this, the three versions of the function are shown below:
//---------------------------------------------------------------------------------------------------
//Current method: Split & Parse
public static bool SplitParseIsSatelliteAssembly(string assemblyName)
{
    string[] fields = assemblyName.Split(',');
    string name = fields[0];
    string culture = fields[2];
    if (name.EndsWith(".resources") && !culture.EndsWith("neutral"))
        return true;

    return false;
}

//New method: Using AssemblyName
public static bool AssemblyNameIsSatelliteAssembly(string assemblyName)
{
    AssemblyName name = new AssemblyName(assemblyName);
    if (name.Name.EndsWith(".resources") && name.CultureInfo.Name.Length > 0)
        return true;

    return false;
}

//Modified method: Fast Parse
public static bool FastParseIsSatelliteAssembly(string assemblyName)
{
    int firstComma = assemblyName.IndexOf(',');

    if (string.Equals(assemblyName.Substring(firstComma - 10, 10), ".resources"))
    {
        int cultureStart = assemblyName.IndexOf("Culture=", firstComma) + 8;
        int cultureEnd = assemblyName.IndexOf(',', cultureStart);

        if (!string.Equals(assemblyName.Substring(cultureStart, cultureEnd - cultureStart), "neutral"))
            return true;
    }

    return false;
}
//---------------------------------------------------------------------------------------------------

The computed (mean) execution times for the functions described above are:
- Split & parse method: 1.3 us
- AssemblyName method: 37.0 us
- Fast Parse method: 0.3 us

So, to avoid a (noticeable, at least in my application) performance hit, in my opinion it is better to stick with a 'dumb' and fast implementaion of the filter function, relying on the fact that the Assembly name provided by the AssemblyResolve event is always well formed.
Posted by Microsoft on 7/9/2010 at 4:01 PM
Hi Daniele.
Sorry to hear that the workaround did not work for you.
To answer your question - an example where such functionality was required was a scenario where assemblies were loaded from memory (via byte arrays) from a package/archive. We could not find a decent way to enable it without this change.
In hindsight, we could make this new functionality opt-in, but we underestimated the impact and it is too late for that now.
The way I read it, in your case the extesion functionality you implement using the resolve event does not need to support a similar extensibility for resource files, in which case the code you added seems the right approach, although I would probably recommend using AssemblyName class instead of parsing the name yourself.

Cheers,
Michael Rayhelson [MSFT]
Posted by Daniele Mancini on 5/7/2010 at 4:09 AM
The proposed workaround doesn't solve the problem.

I am using an Italian Windows Vista OS, using the following test code in the App class:
    public partial class App : Application
    {
        private int m_Count;

        public App()
        {
            AppDomain.CurrentDomain.AssemblyResolve += (s, e) =>
            {
                m_Count++;
                MessageBox.Show(e.Name + "\nRaised so far " + m_Count + " times");
                return null;
            };
        }
    }

- If NeutralResourcesLanguage attribute is not set or is set to
[assembly: NeutralResourcesLanguageAttribute("en-US", UltimateResourceFallbackLocation.MainAssembly)]
the event is fired 8 times (4 for my assembly, and 4 for PresentationFramework.Aero), searching for the italian version of the satellite assemblies
- If NeutralResourcesLanguage is set to
[assembly: NeutralResourcesLanguageAttribute("it-IT", UltimateResourceFallbackLocation.MainAssembly)]
the event is raised 4 times (just for the PresentationFramework.Aero assembly, which is probably not localized)

This means that other than using the NeutralResourcesLanguageAttribute, Culture and UICulture must be set properly on startup, to achieve exactly the same behaviour we got on .NET 3.5, as in the following example
    public partial class App : Application
    {
        private int m_Count;

        public App()
        {
            System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
            AppDomain.CurrentDomain.AssemblyResolve += (s, e) =>
            {
                m_Count++;
                MessageBox.Show(e.Name + "\nRaised so far " + m_Count + " times");
                return null;
            };
        }
    }

Using this code, no MessageBox appear (thus no event is fired).

Unfortunally, setting the culture on the Dispatcher/Main thread is not enough: ThreadPool threads and user-spawned threads will still be created with the OS/startup culture, so it is still possible that the application will try to resolve a satellite assembly regardless .

To truly avoid compatibility problems (even probably as a thumb rule), when it comes to the AssemblyResolve event, the best thing to do is filter-out satellite assemblies.

I still think that such assemblies should not be reported in the AssemblyResolve event, since I really cannot foresee a possible scenario where you want to resolve a satellite assembly (tipially customizing resource manager is enough for localization purposes)... an example, maybe would be useful.
Anyway, if such feature is so important, maybe it would be worth adding a parameter in the event arguments, used to signal that such assembly is just a satellite one.
At the moment I am forced to check it in a rough (but I suppose proper) way:

private static Assembly OnDomainAssemblyResolve(object sender, ResolveEventArgs args)
{
    string[] fields = args.Name.Split(',');
    string name = fields[0];
    string culture = fields[2];
    if (name.EndsWith(".resources") && !culture.EndsWith("neutral")) //A satellite assembly ends with .resources
                                                    //and uses a specific culture
        return null;

         //If the assembly name does not qualify a satellite assembly, try to resolve it...
}
Posted by ShiverCube on 2/8/2010 at 1:05 PM
Yes, it will definitely clear up the issue if it is correctly documented on MSDN. At the moment the description about how the event and assembly loading works is incorrect and confusing. If I am provided with this extra information then I will be able to port an existing .NET 3.5 application over to .NET 4.0.
Posted by Microsoft on 2/8/2010 at 12:57 PM
Oh, and I will also request documenting the event in a more clear way...
Posted by Microsoft on 2/8/2010 at 12:56 PM
What I'm hearing is that the way AssemblyResolve is designed is unclear, so the way it is getting affected by this change is confusing and unexpected, so it will be good to clarify those in MSDN. I will work with our tech writers to make sure we have clarifying articles on MSDN on the change. Will this address your concerns?
Posted by ShiverCube on 2/6/2010 at 3:20 AM
The issue is that the AssemblyResolve event has a different behaviour in .NET 4.0 than it does in .NET 3.5. Sure, if you return null in the handler you can suppress the event, but this doesn't make any sense from an abstraction point of view. The AssemblyResolve should only be called when a resource cannot be found, and returning null should throw an Exception (as it does in .NET 3.5). My main concern is that the even though the behaviour appears to be different in the new version of .NET, I am unable to find any documentation about the matter. I would expect that if Microsoft decided to make a radical change such as this then they would at least have written something about it on MSDN.
Posted by Microsoft on 2/5/2010 at 9:19 PM
I will happy to suggest the soltion, but for that I understand more about the problem.
It is expected that AssemblyResolve handlers will simply ignore the assemblies they do not understand, and event handlers that do not follow this guidance can interfere with other runtime/framework functionality. Could you describe in more detail why raising extra events is problematic?
Posted by ShiverCube on 2/3/2010 at 1:59 AM
Since Microsoft is so determined to classify this annoyance as feature rather than a bug, can someone please explain the solution to the problem?
Posted by ShiverCube on 2/3/2010 at 1:58 AM
Since Microsoft is so determined to classify this annoyance as feature rather than a bug, can someone please explain the solution to the problem?
Posted by Microsoft on 2/2/2010 at 3:55 PM
I think it's important to differentiate between 'default' fallback (from NeutralResourcesLanguageAttribute ) and 'utlimate' fallback (from the MSDN page).
The former is "what to use if not specified, that is, override the lookup chain" and the latter is "make it the last one in the lookup chain", and for the latter one it is natural for some othe fallbacks to be occuring prior to it, triggering the resolve event. In fact, if the are cultures available for the standard fallback chain, using 'default' and 'utilmate' could resukt in a different language.
Posted by ShiverCube on 2/2/2010 at 2:34 AM
No matter how the new framework is implemented, the issue is still classified as a bug, and is not a feature. The so called new design breaks existing .NET 3.5 applications, and completely changes how developers should handle resources. The new behaviour is also undocumented. For these reasons, I am reopening the case so that it can be reevaluated.
Posted by Microsoft on 2/1/2010 at 2:54 PM
There were two things that could have affected this. One is that we got closer integration with the OS as far as the fallback order is concerned and another, more likely to be at play here, is that we started raising the event for satellite assemblies (we were not doing that before) to extend the range of supported localized scenarios.
Posted by Microsoft on 1/26/2010 at 8:54 PM
We are routing this issue to the appropriate group within the Visual Studio Product Team for triage and resolution.These specialized experts will follow-up with your issue.
Posted by ShiverCube on 1/26/2010 at 7:58 PM
The behaviour in .NET 4.0 beta is different to what I have tested in .NET 3.5. In .NET 4.0 the AssemblyResolve event is raised for both missing assemblies and for missing resources. In .NET 3.5, AssemblyResolve is only raised when an assembly is missing. When a satellite resource is missing, a MissingSatelliteAssemblyException is thrown (causing the application to crash or for the DispatcherUnhandledException event to be raise), and AssemblyResolve is never called.
Posted by Microsoft on 1/26/2010 at 7:10 PM
Thank you for your feedback, we are currently reviewing the issue you have submitted. If this issue is urgent, please contact support directly(http://support.microsoft.com)
Posted by ShiverCube on 1/26/2010 at 5:51 PM
Ok thanks. Setting NeutralResourcesLanguageAttribute in my project to "en-US" and to use the main assembly as the default fallback solves the issue. But should this not be the default behaviour anyway? MSDN (http://msdn.microsoft.com/en-us/library/sb6a8618(VS.100).aspx) states: "The default resource is the only resource that is compiled with the main assembly. Unless you specify a satellite assembly using the NeutralResourcesLanguageAttribute, it is the ultimate fallback (final parent)."

Has the default value been changed in .NET 4.0 (and not yet in the documentation), or is it going to default back to the main assembly when the final version is released?
Posted by David A Nelson on 1/26/2010 at 3:07 PM
The problem is not that AssemblyResolve is being called for assemblies that already exist. The problem is that WPF is looking for a resources assembly when it shouldn't be. You can test this by removing the string literal from your handler and using e.Name instead. In that case, no popup is shown. It is the lookup of the string itself, which it expects to be in a satellite assembly, which is causing the problem.

In .NET 4.0, the default for resource lookup appears to have been changed to look for a satellite assembly first. This is substantiated by the fact that adding a NeutralResourcesLanguage attribute to the assembly, specifying UltimateResourceFallbackLocation.MainAssembly (which is supposed to be the default, according to http://msdn.microsoft.com/en-us/library/system.resources.ultimateresourcefallbacklocation%28VS.100%29.aspx), eliminates the problem.