Why is AppDomain.AppendPrivatePath Obsolete?

This is the first in a series of posts where we discuss the reasoning behind “obsoleting” specific APIs.

If you use AppDomain.AppendPrivatePath, or look at MSDN, you’ll notice it’s obsolete.  This frustrates people because the alternative suggested (AppDomainSetup.PrivateBinPath) requires you to do something entirely different (spin up a new AppDomain).  It also doesn’t shed any light on why the API is obsolete. 

You can use this general rule to determine whether you should continue to call an obsolete method:

If a method is obsolete, stop using it!

Generally, the reason we obsolete methods is because we’ve identified something problematic about the method that isn’t fixable.  In this case, we leave the method in place for compatibility. But remember, there’s still something wrong with it!

So, what’s so bad about AppendPrivatePath?  The short answer is that it lets you get into situations that introduce load order dependencies.  In other words, your app can become a time bomb that can crash under seemingly random circumstances.  Load order can be affected by anything from local, machine specific factors (CPU speed, # of processor cores), to external factors like transient network latency.  So, if you are dependent on load order, you can end up with “straightforward” problems like loading unexpected assemblies, or failing to load an assembly.

Consider the following scenario:

  1. AppendPrivatePath is called to add “c:\foo” to the probing paths
  2. A reference to “bar, Version=1.0.0.0, Culture=neutral, PublicKeyToken=asdfghjkl” is resolved due to JIT compiling a method, and the assembly is found in c:\foo\bar.dll

Now, an optimization is made by the JIT team, and bar.dll is now aggressively loaded due to some inlining that occurs in the JIT compiler.  Now, the order of the 2 steps is reversed and bar.dll fails to load because c:\foo wasn’t yet added to the probing path.

In addition to the straightforward (easy to explain) problems, there are a number of unexpected (hard to explain) issues that can arise.  Consider this scenario (same as above, but with step 3 added):

  1. AppendPrivatePath is called to add “c:\foo” to the probing paths
  2. A reference to “bar, Version=1.0.0.0, Culture=neutral, PublicKeyToken=asdfghjkl” is resolved due to JIT compiling a method, and the assembly is found in c:\foo\bar.dll
  3. Assembly.LoadFrom(“c:\foo\bar.dll) is called to load the specific assembly, and is loaded successfully, and its types are equal to the ones loaded from the reference.

Now something occurs that disrupts the load order, and Assembly.LoadFrom occurs first (perhaps it’s called by a component that is loaded earlier due to user interaction).  Can you guess the result?

The reference to “bar, Version=1.0.0.0, Culture=neutral, PublicKeyToken=asdfghjkl” will fail to load, despite the fact that it is available in the probing paths at the time the reference is resolved.  Why does this happen?

When you do Assembly.LoadFrom, we attempt to do something called “context propagation”.  Load contexts (covered on Suzanne Cook’s blog) are another mechanism meant to avoid load order dependencies (among other things).  For LoadFrom, in order to determine whether an assembly gets propagated to the load context, we actually attempt a load.  In this case, that load fails because the assembly is in “c:\foo”, which is not yet in the probing path, so the assembly is not propagated to the load context.  By the time the reference is resolved, “c:\foo” is in the probing path, but we fail.  Can you guess why?

We already tried the same load (during LoadFrom) and it failed, so it’s using the cache result of the previous bind!

What do you do instead?

So, hopefully you agree not to call this API anymore.  If you’re looking for an alternative, here are the scenarios you might fall into:

I just want to edit the probing path

Great, just use a config file to set the path or AppDomainSetup.PrivateBinPath as suggested.

But, I need to do this dynamically (after the AppDomain has been created)

You can use the assembly resolve event.  Hook up a handler that probes wherever you want via Assembly.LoadFrom.  These will be visible to the load context (because they originated from Load) and will keep you from tripping over loader context problems.

I hope this helps people who might otherwise use the obsolete method because they can’t figure out what else to do.

 

-Mark Miller

SDET, CLR