NSandbox: An Introduction

In my last post I introduced a new utility library called NSandbox that makes interacting with AppDomains, or sandboxes, incredibly easy.  I didn’t go into specifics about how to use it, so let’s dig into some code.

Overview

First off, what is an AppDomain and why do we need to create more? From the MSDN page,

Application domains, which are represented by AppDomain objects, help provide isolation, unloading, and security boundaries for executing managed code.

So, we want to execute code in its own AppDomain when:

  • It might bring down our process and we want to isolate its failures.
  • We need to be able to unload the assemblies that it is using.  By design, the .NET framework automatically loads assemblies containing referenced types into the AppDomain requiring those types.  However, it does not provide a means to unload that assembly while the process is running without unloading the entire AppDomain.
  • The code we are executing has different security requirements than our application. We might be executing partially untrusted code and need to lock down what permissions it has (e.g. no file access, etc).
  • We need to specify a new application configuration file (e.g. App.config) for a particular piece of code.  The .NET framework lets you specify a configuration file to use when creating a new AppDomain. This can be quite useful, for example, when testing WCF services which are usually configured via the application configuration file.

For more background information about AppDomains, I’d highly recommend Juval Lowy’s book "Programming .NET Components". I can’t tell you how useful this book has been, and how many times I’ve referred back to it.

Architecture

It helps to have a visual representation of the way NSandbox abstracts away the AppDomain management, so here’s a nice diagram. When every .NET application starts (the orange process box), a default AppDomain is created in which everything runs (the top blue box). It loads its configuration information from the App.config file (renamed to the executing assembly’s name + .config), if one is available.

image

NSandbox then provides two abstractions — a "local sandbox" and a "remote sandbox".  A local sandbox is an object that resides in the current AppDomain that knows how to create and initialize a remote sandbox object in another new AppDomain. In the above picture, a local sandbox is created that:

  • Makes a new directory to serve as the base directory for the new AppDomain
  • Copies all required files to the new directory (e.g. all required DLLs, data files, any files that the sandboxed code needs, etc)
  • Writes a new Sandbox.config file (if necessary)
  • Creates a new AppDomain (using this Sandbox.config file if necessary)
  • Initializes a new instance of the remote sandbox object in the new AppDomain

For the sake of usability, NSandbox provides a SandboxManager as a collection of local sandboxes, allowing all sandboxes to be set up or torn down at the same time.

Communication between local and remote sandboxes can go both ways. The local sandbox can invoke methods on the object in the remote sandbox.  Also, the local sandbox can subscribe to events fired by the remote sandbox (if it’s careful, more on this later).

Under the hood everything is using .NET Remoting, but I’ve tried to hide all of the details involving MarshalByRefObject and serialization.

Now that we’ve covered the basics of NSandbox, let’s dive into an example.

Remote Sandbox

In this example, we’ll set up a single remote sandbox (with a custom config file), invoke methods, and subscribe to an event.

The first thing we need to do is define the object that will live in the remote sandbox.  It must inherit from RemoteSandboxBase which is part of NSandbox.  Here’s our simple version of RemoteSandbox:

public class RemoteSandbox : RemoteSandboxBase

{

    public int GetAppDomainId()

    {

        return AppDomain.CurrentDomain.Id;

    }

 

    public string GetConfigValue(string key)

    {

        return ConfigurationSettings.AppSettings[key];

    }

 

    public SomethingUsefulResponse DoSomethingUseful(SomethingUsefulRequest input)

    {

        return new SomethingUsefulResponse(input.Message.Length);

    }

 

    public void FireEvent()

    {

        SomethingHappenedEvent.Fire(this, new SomethingHappenedEventArgs("FireEvent called."));

    }

 

    public event EventHandler<SomethingHappenedEventArgs> SomethingHappenedEvent;

}

Cross-AppDomain Objects

Pretty self-explanatory, and nothing too complicated going on here.  One thing we need to pay careful attention to — any type which will be making the journey between AppDomains MUST be marked as Serializable.  So, in this example, we see that DoSomethingUseful takes a SomethingUsefulRequest and returns a SomethingUsefulResponse, so both of these must be marked as Serializable.  In addition, the SomethingHappenedEventArgs will be traveling as well, so the rule applies here, too.  If any class going across the boundary isn’t marked as Serializable, an exception will be thrown at runtime.

So, here are the implementations of those 3 classes:

[Serializable] // Important!

public class SomethingUsefulRequest

{

    public SomethingUsefulRequest(string message)

    {

        this.Message = message;

    }

    public string Message { get; private set; }

}

 

[Serializable] // Important!

public class SomethingUsefulResponse

{

    public SomethingUsefulResponse(int length)

    {

        this.Length = length;

    }

    public int Length { get; private set; }

}

 

[Serializable] // Important!

public class SomethingHappenedEventArgs : EventArgs

{

    public SomethingHappenedEventArgs(string whatHappened)

        : base()

    {

        this.WhatHappened = whatHappened;

    }

    public string WhatHappened { get; private set; }

}

Configuration Files

Now we need to talk about is the creation of the configuration file for the remote sandbox.  Each local sandbox has an associated IConfigurationWriter. This implementation simply writes the Sandbox.config file for the new AppDomain.  Of course, if no configuration file is needed, then a value of null for the IConfigurationWriter can be used.

At the current time, only one implementation of IConfigurationWriter is provided, though you are more than welcome to create (and contribute!) your own.  The ParameterizedConfigurationWriter is constructed to point to a template configuration file but then performs a search/replace throughout the file looking for variable names in the format $variableName$.  For example, if we have a file named SandboxTest1.config:

<configuration>

  <appSettings>

    <add key="thing1" value="$var1$"/>

    <add key="thing2" value="$var2$"/>

  </appSettings>

</configuration>

we can specify the values to replace $var1$ and $var2$ with using:

ParameterizedConfigurationWriter sandbox1Config = new 

    ParameterizedConfigurationWriter("SandboxTest1.config");

sandbox1Config.Variables.Add("var1", "Variable 1");

sandbox1Config.Variables.Add("var2", "Variable 2");

Of course, if no variables need to be replaced in the file, then a ParameterizedConfigurationWriter can still be used without any variable/value pairs added to the Variables collection.  The ParameterizedConfigurationWriter will then just copy the template file to the remote sandbox directory.

Local Sandbox

Now let’s put all of these pieces together and set up the local sandbox:

LocalSandbox<RemoteSandbox> sandbox1 = new LocalSandbox<RemoteSandbox>("sandbox1", sandbox1Config);

sandbox1.DependentFiles.Add("NSandbox.Tests.dll");

sandbox1.DependentFiles.Add("NSandbox.dll");

The local sandbox is a generic class which accepts one type argument — the type of object to create in the remote AppDomain. In the constructor, we also specify a "friendly name" (sandbox1 in this case) and the IConfigurationWriter to use. The friendly name is used as the directory name for the sandbox and must be unique across all local sandbox instances.

Then we add the names of all required files that must be present in the AppDomain’s ApplicationBase directory.  The local sandbox takes care of copying these files for us.

SandboxManager

Next we create a new SandboxManager and add this local sandbox to its list of sandboxes.

SandboxManager manager = new SandboxManager();

manager.Sandboxes.Add(sandbox1);

manager.SetupAllSandboxes();

// Use the sandboxes here

manager.TeardownAllSandboxes();

We can then use the sandboxes between calls to SetupAllSandboxes and TeardownAllSandboxes.

The default constructor here uses the currently-executing assembly’s path as the root directory for all operations.  The friendly name subdirectories for each sandbox are relative to the SandboxManager’s root directory. You can also specify your own fully-qualified root directory, but then all of the filenames specified in the local sandbox should be fully-qualified.

Using the remote sandbox

Ok, now we have our sandboxes created and we can actually do something useful with them :)

First, we need to get a reference to the remote sandbox from the local sandbox.  Because we’re using .NET Remoting, we’re actually getting a reference to a proxy to the remote sandbox, but we don’t really need to worry about that.

RemoteSandbox remoteSandbox1 = sandbox1.RemoteSandbox.As<RemoteSandbox>();

Now we can call the GetAppDomainId method on the RemoteSandbox object, and make sure that their Ids aren’t the same (on my computer the Ids were 3 and 4, respectively):

int localId = AppDomain.CurrentDomain.Id;

int remoteId = remoteSandbox1.GetAppDomainId();

Assert.AreNotEqual(localId, remoteId);

Now we can grab some configuration values in the remote AppDomain to make sure that the ParameterizedConfigurationWriter worked correctly:

string thing1Value = remoteSandbox1.GetConfigValue("thing1");

Assert.AreEqual("Variable 1", thing1Value);

string thing2Value = remoteSandbox1.GetConfigValue("thing2");

Assert.AreEqual("Variable 2", thing2Value);

Next, we can subscribe to an event that is fired from the remote sandbox, and then fire it.

bool eventFired = false;

remoteSandbox1.SomethingHappenedEvent += (new RemoteEventWrapper<SomethingHappenedEventArgs>(

    (sender, args) =>

    {

        eventFired = true;

    })).LocalCallback;

remoteSandbox1.FireEvent();

Assert.IsTrue(eventFired);

The RemoteEventWrapper class is the secret to subscribe to remote sandbox events.  I explained why events can’t be subscribed to in the normal way back in this post (in the section "Firing Events Across AppDomain Boundaries") which references this bugfix from Microsoft. I think the RemoteEventWrapper around an anonymous method is an elegant solution to the problem.

As the final test, we can create a (serializable) object in the local sandbox, serialize it across the boundary, and invoke a method with it in the remote sandbox. The return value is also a serializable type.

string message = "This is a test.";

int length = remoteSandbox1.DoSomethingUseful(new SomethingUsefulRequest(message)).Length;

Assert.AreEqual(message.Length, length);

Conclusion

And….that’s a wrap! All of the code in this post is available in the NSandbox SVN in the NSandbox.Tests project.

If this sounds useful, head on over to Sourceforge and download the installer.  I wanted to license NSandbox with the BSD license (closer to public domain than LGPL or GPL) so that both open-source and commercial projects can use the code.

In my next post I’ll talk about using NSandbox to do full-blown integration testing with WCF services.

Hope that helps! :)


You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

AddThis Social Bookmark Button

Leave a Reply