[ Team LiB ] Previous Section Next Section

19.1 Application Domains

A process is, essentially, a running application. Each .NET application runs in its own process. If you have Word, Excel, and Visual Studio open, you have three processes running. If you open another copy of Word, another process starts up. Each process is subdivided into one or more application domains. An app domain acts like a process but uses fewer resources.

App domains can be independently started and halted. They are secure, lightweight, and versatile. An app domain can provide fault tolerance; if you start an object in a second app domain and it crashes, it will bring down the app domain but not your entire program. You can imagine that web servers might use app domains for running users' code; if the code has a problem, the web server can maintain operations.

An app domain is encapsulated by an instance of the AppDomain class, which offers a number of methods and properties. A few of the most important are listed in Table 19-1.

Table 19-1. Methods and properties of the AppDomain class

Method or property

Details

CurrentDomain

Public static property that returns the current application domain for the current thread

CreateDomain( )

Overloaded public static method that creates a new application domain

GetCurrentThreadID( )

Public static method that returns the current thread identifier

Unload( )

Public static method that removes the specified app domain

FriendlyName

Public property that returns the friendly name for this app domain

DefineDynamicAssembly( )

Overloaded public method that defines a dynamic assembly in the current app domain

ExecuteAssembly( )

Public method that executes the designated assembly

GetData( )

Public method that gets the value stored in the current application domain given a key

Load( )

Public method that loads an assembly into the current app domain

SetAppDomainPolicy( )

Public method that sets the security policy for the current app domain

SetData( )

Public method that puts data into the specified app domain property

App domains also support a variety of events—including AssemblyLoad, AssemblyResolve, ProcessExit, and ResourceResolve—that are fired as assemblies are found, loaded, run, and unloaded.

Every process has an initial app domain, and can have additional app domains as you create them. Each app domain exists in exactly one process. Until now, all the programs in this book have been in a single app domain: the default app domain. Each process has its own default app domain. In many, perhaps in most of the programs you write, the default app domain will be all that you'll need.

However, there are times when a single domain is insufficient. You might create a second app domain if you need to run a library written by another programmer. Perhaps you don't trust the library, and want to isolate it in its own domain so that if a method in the library crashes the program, only the isolated domain will be affected. If you were the author of Internet Information Server (IIS, Microsoft's web hosting software), you might spin up a new app domain for each plug-in application or each virtual directory you host. This would provide fault tolerance, so that if one web application crashed, it would not bring down the web server.

It is also possible that the other library might require a different security environment; creating a second app domain allows the two security environments to co-exist. Each app domain has its own security, and the app domain serves as a security boundary.

App domains are not threads and should be distinguished from threads. A thread exists in one app domain at a time, and a thread can access (and report) which app domain it is executing in. App domains are used to isolate applications; within an app domain there might be multiple threads operating at any given moment (see Chapter 20).

To see how app domains work, let's set up an example. Suppose you wish your program to instantiate a Shape class, but in a second app domain.

There is no good reason for this Shape class to be put in a second app domain, except to illustrate how these techniques work. It is possible, however, that more complex objects might need a second app domain to provide a different security environment. Further, if you are creating classes that might engage in risky behavior, you might like the protection of starting them in a second app domain.

Normally, you'd load the Shape class from a separate assembly, but to keep this example simple, you'll just put the definition of the Shape class into the same source file as all the other code in this example (see Chapter 17). Further, in a production environment, you might run the Shape class methods in a separate thread, but for simplicity, you'll ignore threading for now. (Threading is covered in detail in Chapter 20.) By sidestepping these ancillary issues, you can keep the example straightforward and focus on the details of creating and using application domains and marshaling objects across app domain boundaries.

19.1.1 Creating and Using App Domains

Create a new app domain by calling the static method CreateDomain( ) on the AppDomain class:

AppDomain ad2 = 
   AppDomain.CreateDomain("Shape Domain");

This creates a new app domain with the friendly name Shape Domain. The friendly name is a convenience to the programmer; it is a way to interact with the domain programmatically without knowing the internal representation of the domain. You can check the friendly name of the domain you're working in with the property System.AppDomain.CurrentDomain.FriendlyName.

Once you have instantiated an AppDomain object, you can create instances of classes, interfaces, and so forth using its CreateInstance( ) method. Here's the signature:

public ObjectHandle CreateInstance( 
   string assemblyName,
   string typeName,
   bool ignoreCase, 
   BindingFlags bindingAttr,
   Binder binder,
   object[] args,
   CultureInfo culture,
   object[] activationAttributes,
   Evidence securityAttributes
);

And here's how to use it:

ObjectHandle oh = ad2.CreateInstance( 
"ProgCSharp",                                   // the assembly name
"ProgCSharp.Shape",                             // the type name with namespace
false,                                          // ignore case
System.Reflection.BindingFlags.CreateInstance,  // flag
null,                                           // binder
new object[] {3, 5},                            // args
null,                                           // culture
null,                                           // activation attributes
null );                                         // security attributes

The first parameter (ProgCSharp) is the name of the assembly, and the second (ProgCSharp.Shape) is the name of the class. The class name must be fully qualified by namespaces.

A binder is an object that enables dynamic binding of an assembly at runtime. Its job is to allow you to pass in information about the object you want to create, to create that object for you, and to bind your reference to that object. In the vast majority of cases, including this example, you'll use the default binder, which is accomplished by passing in null.

It is possible, of course, to write your own binder that might, for example, check your ID against special permissions in a database and reroute the binding to a different object, based on your identity or your privileges.

Binding typically refers to attaching an object name to an object. Dynamic binding refers to the ability to make that attachment when the program is running, as opposed to when it is compiled. In this example, the Shape object is bound to the instance variable at runtime, through the app domain's CreateInstance( ) method.

Binding flags help the binder fine tune its behavior at binding time. In this example, use the BindingFlags enumeration value CreateInstance. The default binder normally only looks at public classes for binding, but you can add flags to have it look at private classes if you have the right permissions.

When you bind an assembly at runtime, do not specify the assembly to load at compile time; rather, determine which assembly you want programmatically, and bind your variable to that assembly when the program is running.

The constructor you're calling takes two integers, which must be put into an object array (new object[] {3, 5} ). You can send null for the culture because you'll use the default (en) culture and won't specify activation attributes or security attributes.

You get back an object handle, which is a type that is used to pass an object (in a wrapped state) between multiple app domains without loading the metadata for the wrapped object in each object through which the ObjectHandle travels. You can get the actual object itself by calling Unwrap( ) on the object handle, and casting the resulting object to the actual type—in this case, Shape.

The CreateInstance( ) method provides an opportunity to create the object in a new app domain. If you were to create the object with new, it would be created in the current app domain.

19.1.2 Marshaling Across App Domain Boundaries

You've created a Shape object in the Shape domain, but you're accessing it through a Shape object in the original domain. To access the shape object in another domain, you must marshal the object across the domain boundary.

Marshaling is the process of preparing an object to move across a boundary; once again, like Captain Kirk teleporting to the planet's surface. Marshaling is accomplished in two ways: by value or by reference. When an object is marshaled by value, a copy is made. It is as if I called you on the phone and asked you to send me your calculator, and you called up the hardware store and had them send me one that is identical to yours. I can use the copy just as I would the original, but entering numbers on my copy has no effect on your original.

Marshaling by reference is almost like sending me your own calculator. Here's how it works. You do not actually give me the original, but instead keep it in your house. You do send me a proxy. The proxy is very smart: when I press a button on my proxy calculator, it sends a signal to your original calculator, and the number appears over there. Pressing buttons on the proxy looks and feels to me just like I reached through the telephone wire between us and touched your original calculator.

19.1.2.1 Understanding marshaling with proxies

The Captain Kirk and hardware analogies are fine as far as analogies go, but what actually happens when you marshal by reference? The Common Language Runtime (CLR) provides your calling object with a transparent proxy (TP).

The job of the TP is to take everything known about your method call (the return value, the parameters, etc.) off of the stack and stuff it into an object that implements the IMessage interface. That IMessage is passed to a RealProxy object.

RealProxy is an abstract base class from which all proxies derive. You can implement your own real proxy, or any of the other objects in this process except for the transparent proxy. The default real proxy will hand the IMessage to a series of sink objects.

Any number of sinks can be used depending on the number of policies you wish to enforce, but the last sink in a chain will put the IMessage into a channel. Channels are split into client-side and server-side channels, and their job is to move the message across the boundary. Channels are responsible for understanding the transport protocol. The actual format of a message as it moves across the boundary is managed by a formatter. The .NET Framework provides two formatters: a Simple Object Access Protocol (SOAP) formatter, which is the default for HTTP channels, and a Binary formatter, which is the default for TCP/IP channels. You are free to create your own formatters and, if you are truly a glutton for punishment, your own channels.

Once a message is passed across a boundary, it is received by the server-side channel and formatter, which reconstitute the IMessage and pass it to one or more sinks on the server side. The final sink in a sink chain is the StackBuilder, whose job is to take the IMessage and turn it back into a stack frame so that it appears to be a function call to the server.

19.1.2.2 Specifying the marshaling method

To illustrate the distinction between marshaling by value and marshaling by reference, in the next example you'll tell the Shape object to marshal by reference but give it a member variable of type Point, which you'll specify as marshal by value.

Note that each time you create an object that might be used across a boundary, you must choose how it will be marshaled. Normally, objects cannot be marshaled at all; you must take action to indicate that an object can be marshaled, either by value or by reference.

The easiest way to make an object marshal by value is to mark it with the Serializable attribute:

[Serializable]
public class Point

When an object is serialized, its internal state is written out to a stream, either for marshaling or for storage. The details of serialization are covered in Chapter 21.

The easiest way to make an object marshal by reference is to derive its class from MarshalByRefObject:

public class Shape : MarshalByRefObject

The Shape class will have just one member variable, upperLeft. This variable will be a Point object, which will hold the coordinates of the upper-left corner of the shape.

The constructor for Shape will initialize its Point member:

public Shape(int upperLeftX, int upperLeftY)
{
    Console.WriteLine( "[{0}] Event{1}",
       System.AppDomain.CurrentDomain.FriendlyName, 
       "Shape constructor");
    upperLeft = new Point(upperLeftX, upperLeftY);
}

Provide Shape with a method for displaying its position:

public void ShowUpperLeft( )
{
    Console.WriteLine( "[{0}] Upper left: {1},{2}",
        System.AppDomain.CurrentDomain.FriendlyName, 
        upperLeft.X, upperLeft.Y);
}

Also provide a second method for returning its upperLeft member variable:

public Point GetUpperLeft( )
{
    return upperLeft;
}

The Point class is very simple as well. It has a constructor that initializes its two member variables and accessors to get their value.

Once you create the Shape, ask it for its coordinates:

s1.ShowUpperLeft( );     // ask the object to display

Then ask it to return its upperLeft coordinate as a Point object that you'll change:

Point localPoint = s1.GetUpperLeft( );

localPoint.X = 500;
localPoint.Y = 600;

Ask that Point to print its coordinates, and then ask the Shape to print its coordinates. So, will the change to the local Point object be reflected in the Shape? That will depend on how the Point object is marshaled. If it is marshaled by value, the localPoint object will be a copy, and the Shape object will be unaffected by changing the localPoint variables' values. If, on the other hand, you change the Point object to marshal by reference, you'll have a proxy to the actual upperLeft variable, and changing that will change the Shape. Example 19-1 illustrates. Make sure you build Example 19-1 in a project named ProgCSharp. When Main( ) instantiates the Shape object, the method is looking for ProgCSharp.exe.

Example 19-1. Marshaling across app domain boundaries
using System;
using System.Runtime.Remoting;

using System.Reflection;

namespace ProgCSharp
{
   

   // for marshal by reference comment out
   // the attribute and uncomment the base class
   [Serializable]
   public class Point  // : MarshalByRefObject
   {
      private int x;
      private int y;

      public Point (int x, int y)
      {
         Console.WriteLine( "[{0}] {1}",
            System.AppDomain.CurrentDomain.FriendlyName, 
            "Point constructor");

         this.x = x;
         this.y = y;
      }

    
      public int X
      {
         get
         {
            Console.WriteLine( "[{0}] {1}",
               System.AppDomain.CurrentDomain.FriendlyName, 
               "Point x.get");

            return this.x;
         }

         set
         {
            Console.WriteLine( "[{0}] {1}",
               System.AppDomain.CurrentDomain.FriendlyName, 
               "Point x.set");
            this.x = value;
         }
      }

      public int Y
      {
         get
         {
            Console.WriteLine( "[{0}] {1}",
               System.AppDomain.CurrentDomain.FriendlyName, 
               "Point y.get");
            return this.y;
         }

         set
         {
            Console.WriteLine( "[{0}] {1}",
               System.AppDomain.CurrentDomain.FriendlyName, 
               "Point y.set");
            this.y = value;
         }
      }
   }

   // the shape class marshals by reference
   public class Shape : MarshalByRefObject
   {
      private Point upperLeft;

      public Shape(int upperLeftX, int upperLeftY)
      {
         Console.WriteLine( "[{0}] {1}",
            System.AppDomain.CurrentDomain.FriendlyName, 
            "Shape constructor");

         upperLeft = new Point(upperLeftX, upperLeftY);
      }
      public Point GetUpperLeft( )
      {
         return upperLeft;
      }

      public void ShowUpperLeft( )
      {
         Console.WriteLine( "[{0}] Upper left: {1},{2}",
            System.AppDomain.CurrentDomain.FriendlyName, 
            upperLeft.X, upperLeft.Y);
      }
   }
   public class Tester
   {
      public static void Main( )
      {

         Console.WriteLine( "[{0}] {1}",
            System.AppDomain.CurrentDomain.FriendlyName, 
            "Entered Main");
                           
         // create the new app domain
         AppDomain ad2 = 
            AppDomain.CreateDomain("Shape Domain");
    
         //  Assembly a = Assembly.LoadFrom("ProgCSharp.exe");
         //  Object theShape = a.CreateInstance("Shape");
         // instantiate a Shape object
         ObjectHandle oh = ad2.CreateInstance( 
            "ProgCSharp", 
            "ProgCSharp.Shape", false,
            System.Reflection.BindingFlags.CreateInstance,
            null, new object[] {3, 5},
            null, null, null );

         Shape s1 = (Shape) oh.Unwrap( );

         s1.ShowUpperLeft( );     // ask the object to display

         // get a local copy? proxy?
         Point localPoint = s1.GetUpperLeft( );

         // assign new values
         localPoint.X = 500;
         localPoint.Y = 600;

         // display the value of the local Point object
         Console.WriteLine( "[{0}] localPoint: {1}, {2}",
            System.AppDomain.CurrentDomain.FriendlyName, 
            localPoint.X, localPoint.Y);

         s1.ShowUpperLeft( );     // show the value once more
      }
   }
}

Output:
[Programming CSharp.exe] Entered Main
[Shape Domain] Shape constructor
[Shape Domain] Point constructor
[Shape Domain] Point x.get
[Shape Domain] Point y.get
[Shape Domain] Upper left: 3,5
[Programming CSharp.exe] Point x.set
[Programming CSharp.exe] Point y.set
[Programming CSharp.exe] Point x.get
[Programming CSharp.exe] Point y.get
[Programming CSharp.exe] localPoint: 500, 600
[Shape Domain] Point x.get
[Shape Domain] Point y.get
[Shape Domain] Upper left: 3,5

Read through the code, or better yet, put it in your debugger and step through it. The output reveals that the Shape and Point constructors run in the Shape domain, as does the access of the values of the Point object in the Shape.

The property is set in the original app domain, setting the local copy of the Point object to 500 and 600. Because Point is marshaled by value, however, you are setting a copy of the Point object. When you ask the Shape to display its upperLeft member variable, it is unchanged.

To complete the experiment, comment out the attribute at the top of the Point declaration and uncomment the base class:

// [serializable]
public class Point   : MarshalByRefObject

Now run the program again. The output is quite different:

[Programming CSharp.exe] Entered Main
[Shape Domain] Shape constructor
[Shape Domain] Point constructor
[Shape Domain] Point x.get
[Shape Domain] Point y.get
[Shape Domain] Upper left: 3,5
[Shape Domain] Point x.set
[Shape Domain] Point y.set
[Shape Domain] Point x.get
[Shape Domain] Point y.get
[Programming CSharp.exe] localPoint: 500, 600
[Shape Domain] Point x.get
[Shape Domain] Point y.get
[Shape Domain] Upper left: 500,600

This time you get a proxy for the Point object and the properties are set through the proxy on the original Point member variable. Thus, the changes are reflected within the Shape itself.

    [ Team LiB ] Previous Section Next Section