3

I'm trying to use the new long path support at my app. In order to use it, without forcing clients to have the newest .NET 4.6.2 version instelled on their machines, one should only add those elements to his app.config (see the link for more info https://blogs.msdn.microsoft.com/dotnet/2016/08/02/announcing-net-framework-4-6-2/) :

<startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/>
</startup>
<runtime>
    <AppContextSwitchOverrides value="Switch.System.IO.UseLegacyPathHandling=false" />
</runtime>

When I use it in my execution project it works perfectly. The problem is in my testing projects (which use Nunit). I've added app.config to my test project in the same way I've added it to the execution project.

Using the ConfigurationManager class I've managed to ensure that app config indeed loaded (in short: using an app setting which i was able to retrieve in a unit test).

Using ConfigurationManager.GetSection("runtime"), I even managed to ensure the runtime element has been loaded properly (_rawXml value is the same as in app.config).

But (!) for some reason the app config runtime element is not influencing the UseLegacyPathHandling variable and therefore all of my calls with long path fail.

I guess the problem is somehow relates to the fact that testing projects become dll's that are loaded using the Nunit engine, which is the execution entry point.

I'm facing the exact same problem in another project I have, which is a dll loaded by Office Word application. I believe the problem is the same in both cases and derived from the fact that the projects are not meant to be an execution entry point.

It's important to understand that I've no access to the executions their self (Word Office or Nunit) and therefore I can't configure them myself.

Is there an option to somehow make the AppContextSwitchOverrides get loaded from scratch dynamically? Other ideas will be most welcome.

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Ofir H
  • 31
  • 5

1 Answers1

5

I've been having the same issue, and have noted the same lack of loading of that particular setting.

So far, what I've got is that the caching of settings is at least partly to blame. If you check out how it's implemented, disabling the cache has no effect on future calls to values (i.e. if caching is enabled and something is accessed during that time, then it will always be cached).

https://referencesource.microsoft.com/#mscorlib/system/AppContext/AppContext.cs

This doesn't seem to be an issue for most of the settings, but for some reason the UseLegacyPathHandling and BlockLongPaths settings are getting cached by the time I can first step into the code.

At this time, I don't have a good answer, but if you need something in the interim, I've got a highly suspect temporary fix for you. Using reflection, you can fix the setting in the assembly init. It writes to private variables by name, and uses the specific value of 0 to invalidate the cache, so it's a very delicate fix and not appropriate for a long term solution.

That being said, if you need something that 'just works' for the time being, you can check the settings, and apply the hack as needed. Here's a simple example code. This would be a method you'll need in your test class.

    [AssemblyInitialize]
    public static void AssemblyInit(TestContext context)
    {
        // Check to see if we're using legacy paths
        bool stillUsingLegacyPaths;
        if (AppContext.TryGetSwitch("Switch.System.IO.UseLegacyPathHandling", out stillUsingLegacyPaths) && stillUsingLegacyPaths)
        {
            // Here's where we trash the private cached field to get this to ACTUALLY work.
            var switchType = Type.GetType("System.AppContextSwitches"); // <- internal class, bad idea.
            if (switchType != null)
            {
                AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);   // <- Should disable legacy path handling

                // Get the private field that is used for caching the path handling value (bad idea).
                var legacyField = switchType.GetField("_useLegacyPathHandling", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
                legacyField?.SetValue(null, (Int32)0); // <- caching uses 0 to indicate no value, -1 for false, 1 for true.

                // Ensure the value is set.  This changes the backing field, but we're stuck with the cached value for now.
                AppContext.TryGetSwitch("Switch.System.IO.UseLegacyPathHandling", out stillUsingLegacyPaths);
                TestAssert.False(stillUsingLegacyPaths, "Testing will fail if we are using legacy path handling.");
            }
        }
    }
Ben Healy
  • 59
  • 1
  • 3
  • Also of note, if you check out the style sheet used for the app.config, the rutime section has the processContents="skip" property. I'm wondering if this is preventing processing of this section at all. – Ben Healy Mar 13 '17 at 19:17
  • Further investigation shows that there are more issues. AppContextDefaults are set before any user code is executed, so without the ability to load the config properly, there is no way to reconfigure these options without reflection. Also, apparently the test engines will use the .config file for the test engine (i.e. vstest.executionengine.x86.exe.Config in the Unit Test Sessions window), not the one specified for the assembly. This explains why the runtime section is ignored (not needed since one is already loaded). Tragically, I haven't found a workaround for this yet. – Ben Healy Mar 15 '17 at 20:23
  • Also, if you read my last comment and wonder why I'm not just loading the config at runtime, check out how caching is implemented in AppContextSwitches (once set, they can't be modified if your config specifies .NET4.6.1 or earlier). So if you're setting any of the 5 'quirks' defaulted in AppContextDefaultValues.Defaults.cs, you're not going to be able to write tests against those changes. – Ben Healy Mar 15 '17 at 20:30