Run Anonymous Methods in Another AppDomain, Part 2

In my last post, I discussed a method that could be used to run an anonymous method in another AppDomain. 

This approach works great if you have an object (of type Tin) to pass to the anonymous method, and return an object (of type Tout) back.  This handles a lot of scenarios, but many time it is useful for the method running in the remote AppDomain to somehow let the local AppDomain know about something (e.g. report its progress, write to a log, send a message to update the UI, alert that something abnormal happened, etc.)

There are really two different problems here.  First, we need to be able to allow the anonymous method to invoke another method of its caller.  Once we have that figured out, then we need to allow the MarshalByRefObject in the remote AppDomain publish an event that the caller in the local AppDomain can subscribe to, taking all of the complex cross-AppDomain serialization issues into account.

Calling Another Method From Within the Anonymous Method

The motivation for this requirement is modeled after the ReportProgress method of the BackgroundWorker class that allows the code executing on another thread to pass an object containing information about its current progress back to the host UI thread.  One way that I figured out how to do this is to create a small generic class called EventNotifier that can be passed as a parameter to the anonymous method.

    public class EventNotifier<Targs> where Targs : EventArgs

    {

        private Action<Targs> m_NotificationFired;

 

        public EventNotifier(Action<Targs> handler)

        {

            m_NotificationFired = handler;

        }

 

        public void Notify(Targs args)

        {

            if (m_NotificationFired != null)

            {

                m_NotificationFired(args);

            }

        }

    }

When an instance of this class is created, a delegate must be specified in the constructor that contains a method (anonymous or not) that will get called when the Notify method is called.  It essentially serves as a conduit for the caller of an anonymous method to expose a single method for calling.  It can be passed to an anonymous method by defining the signature like this in the DoSomething method:

        public static class DummySandbox

        {

            public static void DoSomething<Tin, Targs>(

                Tin input,

                Action<Tin, EventNotifier<Targs>> method,

                Action<Targs> notification) where Targs : EventArgs

            {

                EventNotifier<Targs> notifier = new EventNotifier<Targs>(

                    args =>

                    {

                        if (notification != null)

                        {

                            notification(args);

                        }

                    });

                method(input, notifier);

            }

        }

This just defines a static method called DoSomething that takes 3 arguments — an input object of type Tin, a delegate that accepts any method which takes a Tin object and an EventNotifier and returns null, and a delegate that accepts any method which only takes one object of type Targ.  Then it creates an EventNotifier object and specifies an anonymous method for the constructor argument that tells it to call the other notification delegate whenever a client of the EventNotifier calls its Notify method.  Then it calls the method delegate, passing it the input argument and the EventNotifier.  Whew, that was a mouthful.  It’s not all that difficult to use (which is the point of abstracting out those details):

DummySandbox.DoSomething<int, ProgressChangedEventArgs>(

    100,

    (delay, notifier) =>

    {

        for (int i = 0; i < 10; i++)

        {

            notifier.Notify(new ProgressChangedEventArgs(10 * i, null));

            Thread.Sleep(delay);

        }

    },

    (args) =>

    {

        Console.WriteLine("Percent Complete: {0}", args.ProgressPercentage);

    });

Console.ReadLine(); 

By utilizing the EventNotifier "conduit", we’ve successfully passed an object to the anonymous delegate that allowed it to call another outside method (in this case the Action<Targ> method specified as the third argument to DoSomething).  Running the above example yields the following output, exactly as you’d expect, with a 1/10 second pause occurring between each line:

image

Note that this contrived example operates on the same thread in the same AppDomain. 

We now expand the generic RemoteSandbox class from yesterday’s post to include the type parameter Targs and mirror the usage of the EventNotifier parameter:

    public class RemoteSandbox2<Tin, Targs, Tout> : MarshalByRefObject where Targs : EventArgs

    {

        public RemoteSandbox2()

        {

        }

 

        public override object InitializeLifetimeService()

        {

            return null;

        }

 

        public Tout Execute(Tin input, Func<Tin, EventNotifier<Targs>, Tout> method)

        {

            EventNotifier<Targs> notifier = new EventNotifier<Targs>(

                args =>

                {

                    EventHandler<Targs> handler = m_NotificationFired;

                    if (handler != null)

                    {

                        handler(this, args);

                    }

                });

 

            return method(input, notifier);

        }

 

        private event EventHandler<Targs> m_NotificationFired;

        public event EventHandler<Targs> NotificationFired

        {

            add { m_NotificationFired += value; }

            remove { m_NotificationFired -= value; }

        }

    }

Now that we have this critical piece of plumbing, it’s time to combine what we learned from the last post and allow the anonymous method to call back to the local AppDomain from the remote AppDomain.

Firing Events Across AppDomain Boundaries

We’ve now encountered an interesting situation — the Sandbox2 class fires an event, NotificationFired, whenever the anonymous method passed into the Execute method uses its EventNotifier.Notify method.  Things get tricky because Sandbox2 is a MarshalByRefObject that actually lives in the remote AppDomain.  Subscribing to events across AppDomain boundaries is certainly possible (the remoting happens under the covers automatically) but requires a little bit more work.  Let’s dig into how we can make this happen, though we’ll need to take a close look at how events work.

An event, put simply, is just a collection of delegates (typesafe function pointers to methods).  It contains a list of references to the methods that have subscribed to it.  Whenever you fire an event, you actually sequentially go through that list and invoke all of the subscribers.  The problem shows up when we subscribe to an event in another AppDomain, because that event can’t be given a reference to the real subscriber method — it must be given a reference to a proxy object that knows how to remotely call the real method in the other AppDomain.  Directly subscribing to an event fired by a MarshallByRefObject to a method in a class that doesn’t inherit from MarshallByRefObject means that a proxy class cannot be created, and the event firing won’t work.

To illustrate this, what happens if we (naively) subscribe to the NotificationFired event and watch what happens?  Let’s say that we have a static method in some class that creates a new RemoteSandbox2 and directly subscribes to its NotificationFired event like so:

            sandbox.NotificationFired +=

                (sender, args) =>

                { …

                };

If we try to run this, we get the following SerializationException: Type ‘AppDomainHelper.AppDomainHelper2+<>c__DisplayClass1`3′ in assembly ‘AppDomainHelper, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null’ is not marked as serializable.  Huh?  What’s going on here?  Well, it turns out that by subscribing to the NotificationFired event, it is trying to get a reference to the method (in this case an anonymous method called (strangely enough) "’AppDomainHelper.AppDomainHelper2+<>c__DisplayClass1`3′ ", but it can’t serialize this class to the other AppDomain since it’s not marked as Serializable.  This is definitely not what we want — by no means so we want to initiating class to get sucked over to the remote AppDomain.  We need to create another class that inherits from MarshallByRefObject and use that class’s method as the subscriber of the event.  The real instance of this class will live in the local AppDomain, and because it inherits from MarshallByRefObject, a proxy class can be created in the remote AppDomain for the event subscriber.

Here’s the (reusable) implementation — RemotableObject:

    public abstract class RemotableObject<Targs> : MarshalByRefObject where Targs : EventArgs

    {

        public void CallbackMethod(object sender, Targs args)

        {

            InternalCallbackMethod(sender, args);

        }

 

        protected abstract void InternalCallbackMethod(object sender, Targs args);

    }

and the concrete implementation for handling our notification events:

    public class EventSink<Targs> : RemotableObject<Targs> where Targs : EventArgs

    {

        protected override void InternalCallbackMethod(object sender, Targs args)

        {

            if (m_NotificationFired != null)

            {

                m_NotificationFired(sender, args);

            }           

        }

 

        private event EventHandler<Targs> m_NotificationFired;

        public event EventHandler<Targs> NotificationFired

        {

            add { m_NotificationFired += value; }

            remove { m_NotificationFired -= value; }

        }

    }

To use this implementation, we wire up the event handling like this:

            EventSink<Targs> sink = new EventSink<Targs>();

            sandbox.NotificationFired += sink.CallbackMethod;

            sink.NotificationFired +=

                (sender, args) =>

                { …

                };

I wish I could say that I "invented" that slick little trick.  I hunted around for a very long time trying to figure out how to subscribe to events on another AppDomain, and eventually found this bugfix on Microsoft’s site that provided the motivation for this implementation.  It certainly makes sense after the fact, but completely eluded me before I found it :)

And, that brings us to the final implementation of the ExecuteInNewAppDomain static method!  Everything is tied in together here:

    public static class AppDomainHelper2

    {

        public static Tout ExecuteInNewAppDomain<Tin, Targs, Tout>(

            Tin input, Func<Tin, EventNotifier<Targs>, Tout> method,

            Action<Targs> notification) where Targs : EventArgs

        {

            // Set up the new AppDomain’s ApplicationBase

            // to be the assembly’s current directory

            AppDomainSetup setup = new AppDomainSetup();

            setup.ApplicationBase =

                Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

 

            // TODO: Allow more restrictive permission sets to be used

            PermissionSet grantSet =

                PolicyLevel.CreateAppDomainLevel().GetNamedPermissionSet("FullTrust");

            AppDomain appDomain = AppDomain.CreateDomain(

                "AppDomainHelper.ExecuteInNewAppDomain", // FriendlyName

                null, // Evidence

                setup,

                grantSet,

                null); // FullTrust assemblies

 

            Type sandboxType = typeof(RemoteSandbox2<Tin, Targs, Tout>);

            // In the format "Name.Space" (no file extension!)

            string sandboxAssemblyName = sandboxType.Assembly.GetName().Name;

            // In the format "Name.Space.ClassName"

            string sandboxTypeName = sandboxType.FullName;

 

            // Create a new sandbox

            RemoteSandbox2<Tin, Targs, Tout> sandbox =

                (RemoteSandbox2<Tin, Targs, Tout>)appDomain.CreateInstanceAndUnwrap(

                sandboxAssemblyName,

                sandboxTypeName);

 

            // Call the ‘notification’ delegate when the

            // EventSink.Notify method is called in the remote

            // AppDomain

            EventSink<Targs> sink = new EventSink<Targs>();

            sandbox.NotificationFired += sink.CallbackMethod;

            sink.NotificationFired +=

                (sender, args) =>

                {

                    if (notification != null)

                    {

                        notification(args);

                    }

                };

 

            // Serialize the input type to the new sandbox,

            // execute it, and serialize the result back here

            Tout output = sandbox.Execute(input, method);

 

            // Unload the AppDomain since we’re done using it

            AppDomain.Unload(appDomain);

 

            return output;

        }

    }

Whew.  All of that work abstracting out the details of doing cross-AppDomain method invocation, event subscription, and event publishing will pay off, since we can now do things like this:

    Print("Main executing.");

 

    string message = AppDomainHelper2.ExecuteInNewAppDomain<int, MyArgs, string>(

        100,

        (delay, notifier) =>

        {

            Print("Now executing in anonymous method.");

            for (int i = 0; i < 10; i++)

            {

                string payload = String.Format("Counting: i={0}", i);

                Print("Sending payload: {0}", payload);

                notifier.Notify(new MyArgs(payload));

                Thread.Sleep(delay);

            }

            return "test";

        },

        args =>

        {

            Print("Received payload: {0}", args.Payload);

        });

    Print("The message is: {0}", message);

    Console.ReadLine();

with the help of this custom EventArgs class:

        [Serializable]

        public class MyArgs : EventArgs

        {

            public MyArgs(string payload)

            {

                this.Payload = payload;

            }

 

            public string Payload { get; private set; }

        }

and the Print helper methods that just show the currently-executing AppDomain Id:

        private static void Print(string line)

        {

            Console.WriteLine("[AppDomain {0}] {1}", AppDomain.CurrentDomain.Id, line);

        }

 

        private static void Print(string format, params object[] args)

        {

            Print(String.Format(format, args));

        }

This simple example counts from 0 to 9 in the remote AppDomain and notifies the local AppDomain of its progress. 

All of this is done completely transparently!  All the user of this code needs to do is 1) Pass in an input argument of type Tin, 2) provide a method that should be executed in the remote AppDomain, and 3) Provide a method that will execute in the local AppDomain when the remote AppDomain fires a notification.  You can’t get my simpler than that.

Also, the code is quite readable since all of the logic is contained in one place (as opposed to being spread out among many different plumbing classes).  The generic approach to treating "code as data" in C# 3.0 really pays off here in a big way.

The output of the following test case is just as you’d expect:

image

Sweet!  While this is a contrived example, the AppDomainHelper2.ExecuteInNewAppDomain method can be reused any time you might need to run some operation in another AppDomain, such as executing a plugin assembly, performing some reflection-heavy operations where you need to unload the assemblies when you’re done, etc.  You could also turn it into a class (instead of a static method) that creates a new AppDomain in the constructor and tears down the remote AppDomain in Dispose().  All of the plumbing concepts are here.  Use your imagination :)

Hope someone finds that useful!


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

4 Responses to “Run Anonymous Methods in Another AppDomain, Part 2”

  1. João P. Bragança Says:

    Have you taken a look at InterLinq (http://www.codeplex.com/interlinq)? It has some helper methods to convert an anonymous type to a serializable one.

  2. [...] 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 [...]

  3. brian dunnington Says:

    your description of how to consume events across appdomain boundaries was perfect – i was facing the same problem and this solved it nicely. you are right – after the fact, it makes perfect sense, but at the time, i was thankful to find your solution that pointed me in the right direction.

  4. Hey Brian,

    Glad it helped! I’ve actually packages all of this up into a library called NSandbox. In fact, there is a RemoteEventWrapper class that makes this kind of thing even easier:
    http://thevalerios.net/matt/2008/10/nsandbox-an-introduction/

    Good luck and thanks for the feedback!

Leave a Reply