Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / AppDomain

JointCode.Shuttle, a fast, flexible and easy-to-use service-oriented framework for cross-AppDomain communication

4.50/5 (2 votes)
27 Jul 2017CPOL4 min read 6.3K  
JointCode.Shuttle is a fast, flexible and easy-to-use service-oriented framework for cross-AppDomain communication. It's a replacement for MarshalByrefObject provided by the runtime libraries.

Download JointCode.Shuttle and samples

Introduction

This article briefly describes a new technology to improve the functions and performance while marshaling across application domains.

Background

All .net/mono code runs within AppDomain boundry under the hood, yet AppDomain is not a common technology to use when developing these applications, this is because usually our code runs in one AppDomain (the default AppDomain), and we don't create it ourselves, the runtime system creates it for us instead.

However, sometimes you just needs to create a secondary AppDomain and run code in it. For example, when you needs to load and unload assemblies at runtime without stopping the application.

In these cases, you needs to make cross-AppDomain calls. So, what's the problem? Well, the problem is not that you make the calls, it’s how you make these calls.

Generally, most people uses the MarshalByrefObject subclassing based mechanism provided by the runtime to achieve the cross-AppDomain communication. This is the easiest and most convenient way to go, and everything will be fine, as long as the service to be operated across AppDomains inherits from MarshalByrefObject, like this: 

C#
namespace JoitCode.Shuttle.SimpleSample
{
    public class MyService : MarshalByRefObject
    {
        public void Do() { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var serviceDomain = AppDomain.CreateDomain("ServiceDomain", null, null);
            
            var myService = (MyService)serviceDomain.CreateInstanceAndUnwrap
                (typeof(MyService).Assembly.FullName, 
                 "JoitCode.Shuttle.SimpleSample.MyService");

            myService.Do();

            Console.Read();
        }
    }
}

Simple as it is, there are some limitations in using this approach.

  1. AppDomain is a single point accessing infrastructure, that is, a child AppDomain can only be accessed by its parent (or host) AppDomain that created it, and no other AppDomains, even the parent AppDomain of the host or sibling AppDomains created by the same host, can access it.
  2. Lack of flexibility, because the service must inherit from the MarshalByrefObject, this limits the flexibility.
  3. Performance concerns, tests show that cross-AppDomain method calls using this way is hundreds to a thousand times slower than normal method calls within a same AppDomain.
  4. Two-way communication, no two-way communications can be achieved this way.

In addition, there are Remoting, WCF, even message queuing, and other IPC mechanisms can be used to achieve cross-AppDomain communication as well, and these approaches eliminate more or less the above restrictions. However, we can imagine that the performance loss is bound to be more serious than MarshalByrefObject, and its learning costs will be higher, the implementation will be more complex.

So, it seems that there is no good solution. Is that right? 

Solution

The solution is simple, find another way! That’s why i created JointCode.Shuttle.

JointCode.Shuttle is a fast, flexible and easy-to-use service-oriented framework for cross-AppDomain communication. It's aimed to be a replacement for MarshalByrefObject provided by the runtime libraries.

JointCode.Shuttle provides the same cross-AppDomain communication function as MarshalByrefObject, and features:

  1. Service-oriented.
  2. Access to any AppDomain from one AppDomain (the MarshalByrefObject only allow access to child AppDomains from the parent AppDomain).
  3. Better performance: 60 ~ 70 times faster than MarshalByrefObject.
  4. Services are manageable: dynamically register/unregister services at runtime without having to restart the application, or even restart AppDomain.
  5. Strong type, easy to use (while the MarshalByrefObject way relies on magic string to find the service type).
  6. Built-in IoC functionality for automatic service dependencies management.
  7. Supports for lazy type / assembly loading.
  8. The remote service lifetime can be managed by leasing, or on demand (the MarshalByrefObject way does not provide remote service life management).
  9. Simple and quick to get started.
  10. Support .net 2.0. 

How to use it

In the JointCode.Shuttle distribution package, it contains two files: JointCode.Shuttle.dll and JointCode.Shuttle.Library.dll, where JointCode.Shuttle.dll is a library written in managed language, and JointCode.Shuttle.Library.dll is a component written in non-managed language, which is depended by the former.

Prepare

To use JointCode.Shuttle, we first need to refer to JointCode.Shuttle.dll in our project, and copy the JointCode.Shuttle.Library.dll to the folder where JointCode.Shuttle.dll is compiled (for example, after the project is compiled, if the JointCode.Shuttle.dll is copied to the c:/projects/sampleproject folder, you need to manually copy the JointCode.Shuttle.Library.dll to this folder).

Using the code

As the JointCode.Shuttle is interface-oriented, so we first need to create a service interface (also called service contract), and apply ServiceInterface attribute to it.

C#
[ServiceInterface]
public interface ISimpleService
{
    string GetOutput(string input);
}

Then we create a service class that implements the contract and apply the ServiceClass attribute to it.

C#
[ServiceClass(typeof(ISimpleService), Lifetime = LifetimeEnum.Transient)]
public class SimpleService : ISimpleService
{
    public string GetOutput(string input)
    {
        return string.Format
            ("SimpleService.GetOutput says: now, we are running in AppDomain: {0}, and the input passed from the caller is: {1}",
                AppDomain.CurrentDomain.FriendlyName, input);
    }
}

Because we want to make cross-AppDomain calls, we need to write a class for starting/stopping the remote services and let it inherit from MarshalByRefObject.

C#
public class ServiceProvider : MarshalByRefObject
{
    // We need a field to keep the _shuttleDomain alive, because if it is garbage collected, 
    // we'll lose all communications with other AppDomains.
    ShuttleDomain _shuttleDomain;

    public void RegisterServices()
    {
        // A Guid is needed when registering service group
        var guid = Guid.NewGuid();
        _shuttleDomain.RegisterServiceGroup(ref guid,
            new ServiceTypePair(typeof(ISimpleService), typeof(SimpleService)));
    }

    public void CreateShuttleDomain()
    {
        // Create a ShuttleDomain object
        _shuttleDomain = ShuttleDomainHelper.Create("domain1", "domain1");
    }

    public void DisposeShuttleDomain()
    {
        _shuttleDomain.Dispose();
    }
}

Now, we are ready to use JointCode.Shuttle:

C#
namespace JoitCode.Shuttle.SimpleSample
{
    public static class ShuttleDomainHelper
    {
        public static ShuttleDomain Create(string assemblySymbol, string assemblyName)
        {
            return Create(assemblySymbol, assemblyName, null);
        }

        public static ShuttleDomain Create(string assemblySymbol, string assemblyName, ServiceContainer svContainer)
        {
            var dynAsmOptions = new DynamicAssemblyOptions
            {
                AccessMode = AssemblyBuilderAccess.Run,
                AssemblyName = new AssemblyName(assemblyName)
            };

            var options = new ShuttleDomainOptions
            {
                DynamicAssemblySymbol = assemblySymbol,
                DynamicAssemblyOptions = dynAsmOptions,
                DefaultLeaseTime = 10.Seconds(),
                PollingInterval = 5.Seconds()
            };

            try
            {
                return ShuttleDomain.Create(ref options, svContainer);
            }
            catch (Exception e)
            {
                if (e.InnerException != null)
                    Console.WriteLine(e.InnerException.Message);
                else
                    Console.WriteLine(e.Message);
                return null;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // To be able to make inter-AppDomain communication with JointCode.Shuttle, firstly we must 
            // initialize the ShuttleDomain.
            // It doesn't matter whether the initialization operation is done in default AppDomain or 
            // any other AppDomains, but it must be done before any ShuttleDomain instance is created.
            ShuttleDomain.Initialize();

            // Creating a child AppDomain in default AppDomain.
            var serviceEnd1Domain = AppDomain.CreateDomain("ServiceEndDomain1", null, null);

            // Creating a ServiceProvider instance for operating that child AppDomain.
            var serviceProvider = (ServiceProvider)serviceEnd1Domain.CreateInstanceAndUnwrap
                (typeof(Program).Assembly.FullName, "JoitCode.Shuttle.SimpleSample.ServiceProvider");

            // Creating a ShuttleDomain instance in the child AppDomain.
            serviceProvider.CreateShuttleDomain();

            // Registering ISimpleService service in child AppDomain.
            serviceProvider.RegisterServices();

            // Creating a ShuttleDomain instance in default AppDomain.
            // Actually, we needs to create one and only one ShuttleDomain instance in every AppDomains.
            // The ShuttleDomain instances communicates with each other across AppDomains.
            var str = Guid.NewGuid().ToString();
            var shuttleDomain = ShuttleDomainHelper.Create(str, str);

            // Get the ISimpleService service in default AppDomain, which is registered by the child AppDomain.
            // The lifetime of service is default to 1 minute, every call to the service method 
            // extends that time for 30 seconds.
            ISimpleService service;
            if (shuttleDomain.TryGetService(out service))
            {
                try
                {
                    Console.WriteLine("Currently, we are running in AppDomain {0} before calling the remote service method...", 
                        AppDomain.CurrentDomain.FriendlyName);

                    Console.WriteLine();

                    // Call the service method of ISimpleService service.
                    var output = service.GetOutput("China");
                    Console.WriteLine(output);

                    Console.WriteLine();
                    Console.WriteLine("Tests completed...");
                }
                catch
                {
                    Console.WriteLine();
                    Console.WriteLine("Failed to invoke the remote service method...");
                }
            }
            else
            {
                Console.WriteLine();
                Console.WriteLine("Failed to create remote service instance...");
            }

            // Indicate the child AppDomain to release the ISimpleService service immediately, instead of waiting for its lifetime to end.
            // This is optional, because even if we don't do this explicitly, the ISimpleService service will still get released in the 
            // child AppDomain automatically when its lifetime ends.
            // And, if the ISimpleService derives from IDisposable, the Dispose method will also get called at that time.
            shuttleDomain.ReleaseService(service);

            // Releasing the ShuttleDomain instance in the child AppDomain, this will unregister all services registered by that 
            // instance, and shut down all communications between that child AppDomain and all other AppDomains.
            serviceProvider.DisposeShuttleDomain();

            Console.Read();
        }
    }
}

Here is the screenshot of result :

JointCode.Shuttle.SimpleTest

Future

JointCode.Shuttle is still a young project, and it lacks some features, such as service registering/unregistering notification, cross-AppDomain events, etc. The author will continue to improve existing functions, as well as introduce more new features in the future, but there are some limitations right now, including:

  1. Only supports 32-bit applications (x86 target platforms)
  2. Only supports Windows (only supports for .net framework, no mono support at this time)
  3. No events supported in the service interface
  4. No support for cross-AppDomain events
  5. Not thoroughly tested

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)