AppDomains - Not as insular as you would expect!

by Remco 13. February 2010 15:44

Lately I've been toying quite a bit with loading and exchanging data between several Application Domains. AppDomains are considered to be an excellent way to isolate executing code within its own separate memory space, they are often used by Test Runners and other tools that need to insulate themselves from external code, or code that is expected to have a limited lifespan within its host process. Chris Brumme has an excellent article describing Microsoft's implementation of AppDomains in detail.

Despite their advantages, Application Domain boundaries within processes do have their limitations. It is important to consider that the AppDomain abstraction has still been built on top of standard windows process and is subject to the same constraints. Two different application domains hosted within the same process will still need to have shared and coordinated access to certain system resources and handles. It's an unfortunate truth that most of the very well designed and nicely packaged frameworks abstractions that .NET provides are still built on the W32 API.

Consider a situation where we are spawning a second application domain hosted within the same process, which makes a call to Directory.SetCurrentDirectory().

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace AppDomainTest
{
    public class DirectoryChangerInOtherAppDomain: MarshalByRefObject
    {
        public void ChangeDirectory(string newDirectory)
        {
            Directory.SetCurrentDirectory(newDirectory);
        }
    }
}



When the SetCurrentDirectory method is invoked in the secondary application domain, underneath the hood, a Win32 API call is invoked by the .NET framework.

...
if (!Win32Native.SetCurrentDirectory(fullPathInternal))
{
    int errorCode = Marshal.GetLastWin32Error();
    if (errorCode == 2)
    {
        errorCode = 3;
    }
    __Error.WinIOError(errorCode, fullPathInternal);
}



This API call doesn't just change the current directory in the secondary application domain, it changes the directory for the entire process. Consider the following test fixture.

using System;
using System.IO;
using System.Security;
using System.Security.Policy;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AppDomainTest
{
    [TestClass]
    public class AppDomainBuildingTest
    {
        public AppDomainBuildingTest()
        {
        }

        private TestContext testContextInstance;
        private const string _testDirectoryName = "TestDirectory";
        private const string _testFileName = "TestFile.txt";

        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        [TestInitialize]
        public void InitializeTest()
        {
            if (Directory.Exists(_testDirectoryName) == false)
            {
                Directory.CreateDirectory(_testDirectoryName);
            }
        }

        [TestCleanup]
        public void CleanUpTest()
        {
            // Clean up the leftovers so our test can re-run without trouble
            if (Directory.Exists(_testDirectoryName) == true)
            {
                Directory.Delete(_testDirectoryName, true);
            }

            if (File.Exists(_testFileName))
            {
                File.Delete(_testFileName);
            }
        }

        [TestMethod]
        public void TestMethod()
        {
            CreateTestFileInMainAppDomain();
            DirectoryChangerInOtherAppDomain directoryChangerInOtherAppDomain = LoadDirectoryChangerInOtherAppDomain();

            Assert.IsTrue(File.Exists(_testFileName)); // Assertion passes

            directoryChangerInOtherAppDomain.ChangeDirectory(_testDirectoryName); // We change the directory in the other AppDomain (but not this one)

            Assert.IsTrue(File.Exists(_testFileName)); // Assertion fails!!
        }

        private void CreateTestFileInMainAppDomain()
        {
            using (StreamWriter sw = new StreamWriter(_testFileName))
            {
                sw.Write("Test!");
            }
        }

        private static DirectoryChangerInOtherAppDomain LoadDirectoryChangerInOtherAppDomain()
        {
            // Set up another application domain
            var setup = new AppDomainSetup
            {
                ApplicationName = "TestAppDomain",
                ApplicationBase = Path.GetDirectoryName(typeof(DirectoryChangerInOtherAppDomain).Assembly.Location)
            };

            var evidence = new Evidence();
            evidence.AddHostEvidence<Zone>(new Zone(SecurityZone.MyComputer));
            evidence.AddHostEvidence<Url>(new Url(typeof(DirectoryChangerInOtherAppDomain).Assembly.CodeBase));

            var appDomain = AppDomain.CreateDomain("TestAppDomain", evidence, setup);

            // Load our directory changer class into it, an obtain a reference
            var directoryChangerInOtherAppDomain = (DirectoryChangerInOtherAppDomain) appDomain.CreateInstanceFromAndUnwrap(
                                                    typeof(DirectoryChangerInOtherAppDomain).Assembly.Location,
                                                    typeof(DirectoryChangerInOtherAppDomain).FullName
                                                    );

            return directoryChangerInOtherAppDomain;
        }
    }
}



This should always be a consideration when using an Application Domain to execute 3rd party code. Unfortunately, in my situation this means I still need to rely on spawning external processes to properly insulate my code. The current directory isn't the only piece of state information managed under the hood by Win32.

Tags: , , , , ,

Month List

Trial NCrunch
Take NCrunch for a spin
Do your fingers a favour and supercharge your testing workflow
Free Download