Export (0) Print
Expand All
This topic has not yet been rated - Rate this topic

 

 

Introduction
Chapter 1: The "Longhorn" Application Model
Chapter 2: Building a "Longhorn" Application
Chapter 3: Controls and XAML
Chapter 4: Storage
Chapter 5: Data Binding


Chapter 6: Communication

Brent Rector
Wise Owl Consulting

March 2004

Contents

Types of Communication Services
What Is Indigo?
The Indigo Architecture
Indigo Applications
Programming Web Services
Obtaining the WSDL for Your Web Service
Programming Remote Objects
Securing Indigo Applications
Reliable and Durable Messaging
Programming Transactions
Summary

In the past decade, connectivity between computers has become ubiquitous. Ten years ago most personal computers were not attached to a network. In fact, the idea of communicating to another computer typically meant that the user initiated a dial-up connection to a bulletin board system (BBS), and that user's personal computer pretended to be a dumb terminal, simply displaying information from the remote system. As Internet usage grew, users became used to connecting—again, typically via a dial-up connection—to the Internet. The computer pretended to be a not-quite-so-dumb terminal that served as a Web browser and displayed rich media content: text, graphics, animations, and eventually video.

As network and Internet connectivity became ubiquitous, developers started producing applications that expected and, in fact, required such connectivity. Some examples are application such as Microsoft® Windows® Messenger, numerous e-mail clients, newsgroup readers, peer-to-peer applications, and many more. It's not uncommon today for normal desktop applications that don't actually need a network connection to use a connection when present to notify the user when a new version of the application is available.

Unfortunately, developing an application that communicates with other applications has traditionally been rather difficult. The developer of a communicating application needs to deal with numerous difficulties: firewalls, address boundaries, routing issues, authentication and authorization, data security, differing protocols, differing data representations, and more. The "Longhorn" communication services generally handle all these difficulties for you, allowing you to focus on developing your custom application functionality.

Types of Communication Services

Let's look at various types of applications a platform should support.

Private Network Services

Many network-aware applications never attempt to communicate to systems outside of some internal network. For example, many corporate applications communicate solely with one or more internal servers on the corporate network. In this tightly controlled environment, a developer can typically assume a common network protocol, security domain, authentication and authorization scheme, and often, development platform. Interoperability with foreign platforms is not a requirement. The number of users is often relatively small—at least when compared to the number of users on the Internet as a whole.

Private Network to Private Network Services

When an application on one private network needs to communicate with an application on a different private network, complications arise. A business-to-business (B2B) application is a typical example of such an application. Company A's purchasing system would like to place an order with Company B's order entry system. In this situation, the purchasing application would typically run on one private network, the order entry application would run on a different private network, and the companies would use the Internet as the bridge between the two private networks.

This introduces a few complications in the connectivity. Because the communication occurs over the public Internet, data security becomes an issue. To establish secure communications between the applications, the two companies will typically need to implement authentication, authorization, and possibly, encryption services. These requirements aren't trivial, even when both companies use the same platform. However, the more common scenario is that the two companies do not use the same platforms and operating systems, meaning that the applications will very likely not have a common security infrastructure.

Of course, neither company has any control of the public portion of the network connection. The Internet is well known for its high latencies, but it's not quite as well known that certain types of messages can be dropped entirely. A robust application might need to handle these scenarios. In addition, limitations in transports and encodings might exist if there is a different platform on each side of a conversation.

Protocol Bridges

Over time, companies often want to connect existing applications to each other and to new applications. Existing systems often use disparate protocols, platforms, and data interchange formats. New applications must interoperate with existing deployed systems. This scenario has many similar requirements as the previous business-to-business scenario. In addition, interoperability to existing applications often requires support for protocols other than Transmission Control Protocol/Internet Protocol (TCP/IP), the standard protocol used on the Internet.

End User Communication Applications

The previously described, communication-based applications typically run within a corporate environment—from server to server, within a company, or across companies. However, end user systems also run communications applications. A traditional client-server application runs on the client and initiates communication with a server. A server can communicate back to its clients only after the client has connected to it. A server cannot arbitrarily establish a connection to a client. A new, but growing area of communications applications is a client-to-client application, also known as a peer-to-peer application. Many games, instant messengers, and file-sharing applications support direct communication from one client application to another client application. Collaboration applications also typically share data by using peer-to-peer communications. Some applications combine client-server and peer-to-peer applications. Instant messenger applications typically contact a central server to receive presence notifications about a person, but they connect directly to the person's instant messenger application to send a file to that person.

What Is Indigo?

Code-named "Indigo" is an implementation of a messaging system that enables secure, reliable, transacted messaging over multiple transports and across heterogeneous systems.

Two Forms of Communication

An application that uses the Indigo messaging services is called, not surprisingly, an Indigo service application. Indigo service applications support two main communications types: a stateless model, in which messages are received with few if any guarantees, and a stateful model, which creates a communication session between two service objects. The stateless model is equivalent to current-day Web services and is useful when broadcasting noncritical information. The stateful model, on the other hand, uses session state to enable Web services to provide functionality across the Internet, including callback methods, events, widely distributed transactions, and reliability and durability guarantees that enterprise applications require.

Reliability and Durability

Once a session has been established between two service instances, Indigo provides delivery assurances about messages within the session to insulate applications from transient communications problems. You can require that messages arrive at least once, at most once, or exactly once and that they arrive in the order in which they were sent. The default session configuration ensures that messages arrive exactly once and in the order in which they were sent.

Indigo can also persist service instance data and messages to support long-running services and to protect against the consequences of a service application failure. Services that indicate which instance data should be persisted can be stored after a period of inactivity and be reactivated with their state restored when another message arrives. In addition, messages are persisted on arrival and departure. Should an application failure occur, a durable service can resurrect itself and pick up processing messages where its predecessor left off.

Message-Based Activation

Indigo service applications can be hosted in any type of application, but services hosted in ASP.NET can be activated automatically when a message arrives for them. This not only enables automatic startup, but also means that long-running services that persist instance data can be stored on disk, to be reactivated with their state data restored when a message for them arrives after a long period. In addition, Indigo service applications can be activated using an extensible protocol layer that supports HTTP, TCP/IP, and cross-process protocols out of the box.

The Indigo Architecture

The Indigo framework is a layered architecture. You generally interact with the top layer (known as the service layer) and work in terms of program abstractions—methods, events, and callbacks. The service layer converts these abstractions into messages, which it delivers to lower layers—a process that eventually result in the message transmission.

The SOAP specification defines messages at the XML Infoset level. The XML Infoset defines a hierarchical structure for information. Specifically, it does not require that information be represented by angle-bracket-encapsulated data. Angle-bracket encapsulation just happens to be one particular serialization format for an XML Infoset. Soap requires a sender to pass an XML Infoset to a receiver; however, the actual serialization of the XML Infoset could be text or some other representation. It's the job of the formatter to convert an Infoset to and from a particular serialized representation.

SOAP does specify how a message gets from one place to another. Delivery and receipt of messages is the function of the transport layer, which deals with wire-level details. The transport layer defines abstract classes and interfaces you can use to implement any wire protocol. The transport layer also provides default implementations of those interfaces for TCP and Hypertext Transfer Protocol (HTTP) wire protocols.

Figure 6-1 shows the layered architecture of the Indigo system. The following sections describe each layer in more detail.

Figure 6-1. The Indigo architecture and important classes

The Service Layer

The service layer is the top-most layer in the Indigo architecture and provides a managed application programming interface (API) to both the service and client applications. It contains the classes that provide the basis for building services using the system-provided implementations. Using the service layer enables you to harness the power of the underlying layers without needing to understand the details of their implementation.

The service layer is a set of managed classes that implement strongly typed message sending and receiving. Your application calls a method with a return value when it wants to send a message and receive a response to that message (known as a correlated message pair). Similarly, your application implements an event handler, and the Indigo services call your event handler when a service receives the appropriate message. While you can process the actual messages directly, it's much easier to use traditional methods, events, and callbacks and let the underlying serialization system build, send, read, and convert messages.

Typed Channels

The methods provided by the service layer translate the method calls to and from a message-based protocol. The service layer passes strongly typed messages to a typed channel. A typed channel exchanges strongly typed messages by using a general messaging pattern, depending on the kind of typed channel. For example, a datagram typed channel sends a strongly typed message without any guarantees of delivery. A dialog exchange typed channel sends a strongly typed message with reliable delivery. There will typically be a typed-channel for each type of message you send or receive.

Untyped Channels

A typed channel sends its messages using an untyped channel. Basically, the typed channel provides the type-safe wrapper around use of the untyped channel. It's actually the untyped channel that implements a particular message exchange pattern—either datagram or dialog exchange. An untyped channel sends its messages via a port.

Ports

A port exchanges messages from untyped channels to the underlying transport and vice versa. A port contains a pipeline of one or more untyped channels that process messages from and to the transport and the untyped channel.

Transports and Formatters

A transport represents an adapter for an underlying wire protocol, such as TCP or HTTP. The transport handles connections, addressing, and flow control, but it delegates serializing of a message to a formatter. Conceptually, a transport accepts a message from a port, requests a formatter to serialize the message into a particular wire format, and sends the formatted message using a particular adapter. Of course, it also performs the inverse operations.

Transports receive messages from or pass messages to their associated Port message pipeline. Transports are the only objects in the hierarchy that deal with message formats. All higher levels always handle message objects. You can see this structure in Figure 6-1, which shows the layer architecture of the Indigo system.

Managers

The Indigo manager objects provide most, if not all, of the complex functionality supported by Indigo service applications. When an application makes what seems like a simple request to the service layer—for example, "Send this message, and wait for the response"—a complex series of messages might result. For example, the system might time out while waiting for the response and might need to resend the request. Or the request might require sending authentication messages to the recipient. Whatever the situation, the managers handle the complexities resulting from your application requests. Managers often inject port extensions into the port message pipeline and take ownership of messages they need to process to provide the service your application requests. Figure 6-2 provides a picture of how managers interact with the port and untyped channels.

Click here to see larger image

Figure 6-2. Managers acquiring messages from a port

Indigo Applications

The Indigo infrastructure enables you to create a wide variety of applications. You can create something as simple as a chat application that operates between two peers on an intranet and as complex as a scalable Web service for millions of users. And just as the complexities of such applications vary so widely, so do the features of Indigo. In other words, the Indigo infrastructure is also highly variable, and you need to use only those parts that are appropriate to the complexity of your solution.

Indigo service applications are one or both sides of a SOAP message exchange. The simplest example of an Indigo application resembles an XML Web service built with ASP.NET, an .asmx page. This type of application is known as an Indigo Web service application. You can also build Indigo applications that can create complex, two-way conversations between two objects in much the same way you can use proxies to remote classes in .NET remoting. This type of application is known as an Indigo RemoteObject service application.

Indigo Web Service Applications

Indigo Web service applications interoperate with Web services on many platforms, not just Microsoft platforms, because the applications are one implementation of the SOAP 1.1 or 1.2 and Web Services Description Language (WSDL) 1.1 specifications. An Indigo Web service application can do the following:

  • Securely communicate across any number of intermediaries, including firewalls
  • Participate in widely distributed transactions
  • Create conversations that flow in both directions (that is, services can call clients)
  • Provide guaranteed, reliable message delivery
  • Support Web service farms for excellent scalability
  • Use advanced features such a security, reliability, and transactions with participants that don't use Indigo or Microsoft platforms
  • Enable .NET Framework developers to build messaging applications without knowing anything about XML or SOAP
  • Enable developers familiar with XML Web services to leverage their XML, WSDL, and SOAP knowledge to work with XML messages described by XSD
  • Support easy management of deployed applications

An Indigo Web service application can integrate multiple, diverse participants—each running on different platforms, each separated by public networks, and each using differing security and transaction infrastructures—as components of a single application.

Indigo RemoteObject Service Applications

Indigo RemoteObject service applications provide behavior similar to .NET remoting but with greatly expanded features and support. Both .NET remoting and Indigo RemoteObjects allow client applications to connect with a running object on a server. Both technologies allow client applications to request a server to instantiate a type and then connect to the new instance. Both technologies require the client and the server to possess the type information (metadata) for the remote class.

However, .NET remoting has no authentication or encryption security mechanisms. .NET remoting requires the server to be up and running before any client attempts to connect—there is no automatic activation of the server. .NET remoting also cannot flow a transaction across a remote method call.

However, Indigo RemoteObject applications are built using the Indigo SOAP messaging infrastructure; therefore, they support end-to-end security features, including authentication and encryption, widely distributed and long-running transactions, automatic activation, and a robust management infrastructure, in addition to providing the ability to perform interface-based remoting and use asynchronous server methods.

Choosing Between Indigo Web and RemoteObject Web Services

Both Indigo Web services and Indigo RemoteObject services provide the advanced infrastructure features required for a modern communications application: end-to-end security, distributed transactions, durable messaging, Web farms, and automatic activation. These features simply work regardless of the number of intermediaries between two Indigo applications. Use the criteria shown in Table 6-1 (in descending order of importance) to choose the appropriate type of Indigo service application for your task.

Table 6-1. Criteria for Choosing Indigo Service

Criteria Indigo Web services Indigo RemoteObject services
Interoperability with non-Microsoft platforms Yes.

Requires the other side of the communication channel to understand SOAP, which is platform agnostic.

No.

Requires Indigo RemoteObject services on both sides of the communication channel.

An improved .NET remoting model No. Yes.

Adds increased support for security, transactions, automatic activation, and ease of management.

Type identity No.

Accepts structured information and return structured information. Does not maintain type identity.

Yes.

Preserves the exact type identity of the managed object on both sides of the communication channel.

Programming Web Services

Web services are methods that can be called by any client on any platform that communicates through public network and XML protocols. In this section, I'll show you how to define the Web service by using the Indigo framework. I'll also show you how to generate information that describes the service in a platform-agnostic manner. You typically publish this information so that clients on any platform can use your Web service. Finally, I'll show you how to call the service by using a Indigo client application.

Defining a Web Service

First, we need a Web service that clients can call. You perform the following steps to create a Web service using the Indigo Framework:

  1. Define a class with one or more public methods.
  2. Apply the DatagramPortTypeAttribute to the class.
  3. Apply the ServiceMethodAttribute to the public methods you want to expose as Web service methods. Note that you can have other methods in the class that aren't exposed to Web service clients.
  4. Compile the class into a library assembly.
  5. Host the service in a host application.

In the following example, we create the TimeService assembly, which contains a single class named WiseOwl.Time. The class contains a single Web service method named GetUtcTimeAndDate, which returns a string representation of the current Universal Time Coordinate (UTC) time for the specified culture.

// TimeService.cs
using System;
using System.Globalization;
using System.MessageBus;
using System.MessageBus.Services;

namespace WiseOwl {
  [DatagramPortType(Name="Time",
                    Namespace="http://www.wiseowl.com/WebServices")]
  public class Time {
    [ServiceMethod]
    public string GetUtcTimeAndDate(string culture) {    
      Console.WriteLine ("Client requested UTC time for culture {0}",
                         culture);
      CultureInfo ci = new CultureInfo (culture);
      return DateTime.UtcNow.ToString ("F", ci);
    }
  }
}

Hosting the Web Service

Generally, you will build Web service implementations into one or more library assemblies. You then need a host application that configures the Indigo Framework to listen for requests for that service. When Indigo receives a request for a service, it loads the Web service library, creates an instance of your service class, and calls the appropriate method on the instance.

You need to perform the following two steps to host an Indigo Web service:

  1. Call ServiceEnvironment.Load to acquire a service environment from a configuration file.
  2. Call ServiceEnvironment.Open to instruct MessageBus to begin listening for messages to this service.

If you are not using automatic activation, you must keep the application running if you want the service to continue processing messages. When your host application is a Windows Forms application or a Windows Service, the application automatically continues to run until it is explicitly shut down. Therefore, these types of applications typically need to do nothing special to keep the application running. However, as we're using a simple Console application as our host, we call Console.ReadLine to block the main thread until the user presses the ENTER key to terminate the host application. Thread pool threads will service the Web service requests.

The following example is a generic host application. It hosts all Web services defined in the application's configuration file.

// host.cs
using System;
using System.MessageBus;
using System.MessageBus.Services;

class Host {
  static void Main(string[] args) {
    // The service environment needs to be loaded before it can be used.
    // The Load method loads the configuration from the configuration file.
    ServiceEnvironment se = null;
    try {
        se = ServiceEnvironment.Load ();

        // Open the environment to allow client connections
        se.Open ();
        Console.WriteLine("Press enter to stop the services...");
        Console.ReadLine ();
    }
    finally {
      // Must close the environment to cleanup server promptly
      if (se != null) se.Close ();
    }
  }
}

The generic host application instructs Indigo to obtain all its configuration information from the application's configuration file. As the host application is named host.exe, the application configuration file should be named host.exe.config and reside in the same directory as host.exe.

There are three main items of interest in this configuration file:

  • The main service environment definition
  • The port identityRole element
  • The activatableServices child elements

You can define multiple service environments in a configuration file; however, the ServiceEnvironment.Load method loads the environment named main by default so that's the only one we've defined. We define a port mapped to the URL listed as the value of the identityRole element. Clients will use this URL to contact the server. The server will listen for requests addressed to this endpoint. The activatableServices element contains a list of the fully qualified types that the Indigo Framework can load and activate when receiving a request for the time. Recall that a fully qualified type name is a string consisting of the namespace-qualified type name, followed by a comma, followed by the full name of the assembly.

We've also included a number of configuration entries to disable security for this simple application. Later in the chapter, I'll discuss communications security in more detail.

<configuration>
  <system.messagebus>
    <serviceEnvironments>
      <serviceEnvironment name="main">
        <port>
   <identity-Role>soap.tcp://localhost:46000/TimeService/</identityRole>
        </port>
        <!-- CAUTION: Security disabled for demonstration purposes only. -->
        <remove name="securityManager" />
        <policyManager>
          <!-- CAUTION: Security disabled for demonstration purposes only. -->
          <!--  Permits unsigned policy statements. Default requires 
signed policy statements -- >
          <areUntrustedPolicyAttachmentsAccepted>
            true
          </areUntrustedPolicyAttachmentsAccepted>
          <isPolicyReturned>true</isPolicyReturned>
        </policyManager>
         <serviceManager>
          <activatableServices>
            <add type="WiseOwl.Time, TimeService" />
          </activatableServices>
        </serviceManager>
      </serviceEnvironment>
    </serviceEnvironments>
  </system.messagebus>
</configuration>

Obtaining the WSDL for Your Web Service

Now that you have a working Web service, you'd like to create a client that uses the Web service. Before you can write a client that uses the service, you'll need a description of the service. The Web Service Description Language (WSDL) 1.1 and extensions describe a contract that a Web service will uphold. Basically, WSDL allows Web service developers to publish a description of their services in a platform-agnostic manner. Typically, the first step in consuming a Web service is obtaining the WSDL description of the server. You can use the Wsdlgen.exe utility to produce the WSDL for your Indigo Web services.

To obtain WSDL for a Web Service, run the following command where <assemblyFileName> is the assembly containing the service: wsdlgen <assemblyFileName>.

In the prior example, I used "wsdlgen TimeService.dll", and it produced two files: a .wsdl file and an .xsd file. The .wsdl file contains the description of the service: the name of the messages accepted by the server, the names of the parts of the messages (for example, the parameters), the port name and type used by the server, and the operations supported on the port.

<!-- www_wiseowl_com.WebServices.wsdl -->

<definitions xmlns:s="http://www.w3.org/2001/XMLSchema" 
           xmlns:gxa="http://schemas.xmlsoap.org/wsdl/gxa/2003/01/extensions" 
           xmlns:i0="http://www.wiseowl.com/WebServices" 
           xmlns:tns=http://www.wiseowl.com/WebServices
           targetNamespace="http://www.wiseowl.com/WebServices" 
           xmlns="http://schemas.xmlsoap.org/wsdl/">
  <import namespace="http://www.wiseowl.com/WebServices" />
  <types />
  <message name="GetUtcTimeAndDateRequest">
    <part element="tns:GetUtcTimeAndDateRequest" name="parameters" />
  </message>
  <message name="GetUtcTimeAndDateResponse">
    <part element="tns:GetUtcTimeAndDateResponse" name="parameters" />
  </message>
  <port-Type gxa:correlation="response" name="Time" 
 gxa:usingName="TimeClient">
    <operation name="GetUtcTimeAndDate" gxa:parameterOrder="tns:culture"
                                        gxa:transaction="reject">
      <input message="tns:GetUtcTimeAndDateRequest"
             name="GetUtcTimeAndDateRequest" />
      <output message="tns:GetUtcTimeAndDateResponse" 
              name="GetUtcTimeAndDateResponse" />
    </operation>
  </portType>
</definitions>

The .xsd file contains the XML schema definition of the messages mentioned in the .wsdl file. Note that the prior .wsdl file described an input message of type GetUtcTimeAndDateRequest and an output message of type GetUtcTimeAndDateResponse, both in the http://www.wiseowl.com/WebServices namespace. The following .xsd file contains the definition of those two XML types.

<!-- www_wiseowl_com.WebServices.xsd -->

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns=http://www.wiseowl.com/WebServices
           elementFormDefault="qualified"
           targetNamespace="http://www.wiseowl.com/WebServices" 
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="GetUtcTimeAndDateRequest">
    <xs:complexType>
      <xs:sequence>
        <xs:element minOccurs="0" name="culture" type="xs:string" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="GetUtcTimeAndDateResponse">
    <xs:complexType>
      <xs:sequence>
        <xs:element minOccurs="0" name="returnValue" type="xs:string" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

Because the .wsdl and .xsd files describe your Web service in a platform-agnostic manner, all the information clients need to connect and send requests to your service is contained within those two files. If you publish those files, clients on any platform can, with varying levels of difficulty depending on their tools, create a client for your service.

Creating the Metadata for the Web Service Client

Let's look at the process of creating an Indigo Web service client for an arbitrary Web service. Processing XML directly, in my opinion, is a pain. I don't want to do that. I'd prefer that the framework encapsulate all the details of generating a Web service request, sending it, and parsing the response. In fact, I'd like a managed class that looks and behaves like the actual service to the client but, when used, really sends messages to the actual service. A class is a bit of overkill—an interface would be a better design choice.

Given that you have the .wsdl and .xsd definitions of a Web service, you can use the Wsdlgen.exe tool again to generate a source code file that contains a managed code definition for the Web service. You can then build the source code into an assembly that client code references, or you can include it as a source file in your client application.

Wsdlgen.exe uses .wsdl and .xsd files to create a source code definition of an interface you can use to call the service from a client application.

To create source code describing a Web service, obtain WSDL and any XSD that describes the service with which you want to have a conversation. Run the Wsdlgen utility using the following command:

wsdlgen <WSDL file> <any XSD files>

The following source code was generated by Wsdlgen.exe from the .wsdl and .xsd files that describe the TimeService Web service we previously created. I reformatted the code slightly to make it easier to read.

// www_wiseowl_com.WebServices.cs

//----------------------------------------------------------------------------
// <autogenerated>
//     This code was generated by a tool.
//     Runtime Version:1.2.30616.0
//
//     Changes to this file may cause incorrect behavior and will be lost if 
//     the code is regenerated.
// </autogenerated>
//----------------------------------------------------------------------------

// 
// This source code was auto-generated by WsdlGen, Version=1.2.30616.0.
// 
using System.MessageBus.Services;

namespace www_wiseowl_com.WebServices {
  [DatagramPortType( Namespace="http://www.wiseowl.com/WebServices")]
  public interface ITime {
        [ServiceMethod]
        string GetUtcTimeAndDate(string culture);
  }

  [PortTypeChannel(Namespace="http://www.wiseowl.com/WebServices")]
  public interface ITimeChannel : IDatagramPortTypeChannel {
    [WrappedMessage(Namespace="http://www.wiseowl.com/WebServices")]
    [ServiceMethod]
    [return: WrappedMessage(Namespace="http://www.wiseowl.com/WebServices")]
    string GetUtcTimeAndDate(string culture);
  }
}

The prior source file has two noteworthy aspects. The Wsdlgen tool defines an interface, named ITime, that contains each service method implemented by the Web service. It also defines an interface, named ITimeChannel, which is the strongly typed channel you use when communicating with the Web service.

Calling the Web Service from a Client

To use these interfaces in a client application, your client application must do the following:

  • Call ServiceEnvironment.Load to acquire the default service environment from a configuration file.
  • Extract the ServiceManager object from the ServiceEnvironment.
  • Use the ServiceManager.CreateChannel method to create the channel interface that can connect to the Web service.
  • Reference the definition of the channel interface you produced using the Wsdlgen tool.

The following code is a client that uses the Time Web service:

// client.cs
using System;
using System.MessageBus;
using System.MessageBus.Services;
using www_wiseowl_com.WebServices;  // The imported service namespace

public class Client {
  public static void Main(string[] args) {
    string culture = "en-US";
    if (args.Length > 0) culture = args[0];

    // Load the default service environment, called "main".
    ServiceEnvironment se = null;
    
    try {
      se = ServiceEnvironment.Load();

      // Retrieve the ServiceManager from the default environment
      ServiceManager sm =
        se[typeof(ServiceManager)] as ServiceManager;
      if (sm == null)
        throw new Exception ("ServiceManager is not available.");

      // Start the service environment.
      se.Open();

      // Create a proxy channel that points to the service to call.
      Uri uri = new Uri("soap.tcp://localhost:46000/TimeService/");
      ITimeChannel channel = (ITimeChannel)
          sm.CreateChannel(typeof(ITimeChannel), uri);

      Console.WriteLine(channel.GetUtcTimeAndDate (culture));
    }
    catch (Exception e) {
      Console.WriteLine (e);
    }
    finally {
      if (se != null) se.Close();
    }
  }
}

As with the Web service itself, the client application can obtain its Indigo configuration information by asking the Framework to load the information from the application configuration file. I named the client application client.exe, so here is its associated client.exe.config configuration file.

<configuration>
  <system.messagebus>
    <serviceEnvironments>
      <serviceEnvironment name="main">
        <port>
          <identity-Role>soap.tcp://localhost:46001/TimeClient/</identityRole>
        </port>
        <!-- CAUTION: Security disabled for demonstration purposes only. -->
        <remove name="securityManager" />
        <policyManager>
          <!-- CAUTION: Security disabled for demonstration purposes only. -->
          <!--  Permits unsigned policy statements. Default requires signed policy statements -- >
          <areUntrustedPolicyAttachmentsAccepted>
            true
          </areUntrustedPolicyAttachmentsAccepted>
          <isPolicyReturned>true</isPolicyReturned>
        </policyManager>
      </serviceEnvironment>
    </serviceEnvironments>
  </system.messagebus>
</configuration>

Note that I define a port and an identify role for the client. The identityRole element's value must be a unique identifier and allow a service to initiate communications with the client. In my example, I specify both the name and location of the client, which eliminates the need to specify available transports separately. Regardless, my client application doesn't use this server-callback capability.

Programmatic Configuration

I think placing most communications configuration information in a separate file from the application is the best approach for most scenarios. However, occasionally you might not know until runtime which service you want, or you might want to determine other configuration parameters dynamically. Indigo allows you to configure the runtime environment programmatically—in fact, loading the configuration file really just causes the runtime itself to issue these programmatic requests.

The code is fairly self-explanatory, so I'll simply list the source for the programmatic versions of the Web service and client. Note that these changes don't affect the service itself; all the changes occur in the service's host application. Here's the source code for the host:

// host.cs
using System;
using System.Authorization;
using System.MessageBus;
using System.MessageBus.Policy;
using System.MessageBus.Security;
using System.MessageBus.Services;

class Host {
  static void Main(string[] args) {
    // Load and configure the default ServiceEnvironment.
    ServiceEnvironment se = null;
    try {
      se = ServiceEnvironment.Load();
      Port port = se [typeof(Port)] as Port;
      port.IdentityRole = new Uri("soap.tcp://localhost:46000/TimeService/");

      // Register the Time type as activatable.        
      ServiceManager sm = se [typeof(ServiceManager)] as ServiceManager;
      sm.ActivatableServices.Add(typeof(WiseOwl.Time));

      // Enable the PolicyManager to accept unsigned policy messages
      // because this service does not have X509 certificates.
      // For demonstration purposes only.
      PolicyManager pm = se[typeof(PolicyManager)] as PolicyManager;
      pm.AreUntrustedPolicyAttachmentsAccepted = true;
      pm.IsPolicyReturned = true;
 
      // Disable security for receiving messages.
      // For demonstration purposes only.
      SecurityManager secman = (SecurityManager)se[typeof(SecurityManager)];
      secman.IsEnabledForReceive = false;

      se.Open();
      Console.WriteLine("Press enter to stop the services...");
      Console.ReadLine();
    }
    finally {
      if (se != null) se.Close();            
    }
  }
}

And here's the source code for the client:

// client.cs
using System;
using System.Authorization;
using System.MessageBus;
using System.MessageBus.Policy;
using System.MessageBus.Security;
using System.MessageBus.Services;
using www_wiseowl_com.WebServices;  // The imported service namespace

public class Client {
  public static void Main(string[] args) {
    string culture = "en-US";
    if (args.Length > 0) culture = args[0];

    // Load the default service environment, called "main".
    ServiceEnvironment se = null;
    
    try {
      se = ServiceEnvironment.Load();

      // Retrieve the ServiceManager from the default environment
      ServiceManager sm =
        se [typeof(ServiceManager)] as ServiceManager;
      if (sm == null)
        throw new Exception ("ServiceManager is not available.");

      // Start the service environment programmatically.
      Port port = se[typeof(Port)] as Port;
      port.IdentityRole = new Uri("soap.tcp://localhost:46001/TimeClient/");

      // Allow PolicyManager to accept unsigned policy messages because 
      // client does not have X509 certificates.
      // CAUTION: Security disabled for demonstration purposes.
      PolicyManager pm = se[typeof(PolicyManager)] as PolicyManager;
      pm.AreUntrustedPolicyAttachmentsAccepted = true;
      pm.IsPolicyReturned = true;

      // Turn off access control. 
      // CAUTION: Security disabled for demonstration purposes.
      SecurityManager secman = (SecurityManager) se[typeof(SecurityManager)];
    secman.DefaultReceiverScope.AccessControl.AccessRequirementChoices.Add
                                          (new AccessRequirementChoice());
      secman.IsEnabledForReceive = false;

      // Start the service environment.
      se.Open();

      // Create a proxy channel that points to the service to call.
      Uri uri = new Uri("soap.tcp://localhost:46000/TimeService/");
      ITimeChannel channel = (ITimeChannel)
          sm.CreateChannel(typeof(ITimeChannel), uri);

      Console.WriteLine(channel.GetUtcTimeAndDate (culture));
    }
    catch (Exception e) {
      Console.WriteLine (e);
    }
    finally {
      if (se != null) se.Close();
    }
  }
}

Programming Remote Objects

You can use RemoteObjects in two ways. You can create a new instance of a remote object. You can also obtain a proxy to an existing remote object so that you can invoke its methods. RemoteObjects provide communications functionality to programs running in separate application domains in the same Win32 process, running in different processes on the same machine, or even running across machines over the Internet.

From the server's point of view, there are two aspects to serving a remote object. You can publish the ability to create a new instance of a remote object class, or you can publish the ability to connect to an existing instance of a remote object class. Either way, you need a remote object class, so let's start there.

Creating a TimeZone RemoteObject

It's actually easier to define a class that supports Indigo RemoteObjects than it is to define a Web service class. A RemoteObject must derive directly or indirectly from System.MarshalByRefObject. Note that .NET remoting had the same base class requirement, so a class written to be used by .NET remoting can generally be used as an Indigo RemoteObject.

The following code example creates a TimeZoneObject RemoteObject that has public methods that return time and date information. I'll be using this class in the rest of the examples in this section. A client creates a TimeZoneObject instance for a specific time zone. The GetCurrentTimeAndDate method for that instance then returns the current time in the specified time zone.

using System;
using System.Globalization;

public class TimeZoneObject : MarshalByRefObject {
  private Guid     myID = Guid.NewGuid ();
  private TimeSpan myOffset;

  public TimeZoneObject (string tz) {
    myOffset = ConvertZoneToUtcOffset (tz);
    Console.WriteLine("Creating object {0} for zone {1}", myID, tz);
  }

  public string GetCurrentTimeAndDate (CultureInfo ci) {
    if (ci == null) ci = new CultureInfo ("");
    Console.WriteLine("{0} returning current time/date for culture {1}",
                      myID, ci.ToString());
    return (DateTime.UtcNow + myOffset).ToString ("F", ci);
  }

  public string GetUtcTimeAndDate (CultureInfo ci) {
    return DateTime.UtcNow.ToString ("F", ci);
  }
  
  private TimeSpan ConvertZoneToUtcOffset (string tz) {
    string upperTZ = tz.ToUpper();
    switch (upperTZ) {
      case "ADT":                         // Atlantic Daylight -3 hours
        return new TimeSpan (-3, 0, 0);
      case "AST":                         // Atlantic Standard
      case "EDT":                         // Eastern Daylight  -4 hours
        return new TimeSpan (-4, 0, 0);
      case "EST":                         // Eastern Standard
      case "CDT":                         // Central Daylight  -5 hours
        return new TimeSpan (-5, 0, 0); 
      case "CST":                         // Central Standard
      case "MDT":                         // Mountain Daylight -6 hours
        return new TimeSpan (-6, 0, 0); 
      case "MST":                         // Mountain Standard
      case "PDT":                         // Pacific Daylight -7 hours
        return new TimeSpan (-7, 0, 0);
      case "PST":                         // Pacific Standard
      //case "ADT":                       // Alaskan Daylight -8 hours
        return new TimeSpan (-8, 0, 0);
      case "ALA":                         // Alaskan Standard -9 hours
          return new TimeSpan (-9, 0, 0);
      case "HAW":                         // Hawaiian Standard -10 hours
        return new TimeSpan (-10, 0, 0);
      case "UTC":
      case "GMT":
      case "ZULU":                        // So it's not a real zone name
        return new TimeSpan (0, 0, 0);
      case "CET":                         // Central European
      case "FWT":                         // French Winter
      case "MET":                         // Middle European 
      case "MEWT":                        // Middle European Winter
      case "SWT":                         // Swedish Winter  +1 hour
        return new TimeSpan (+1, 0, 0);
      case "EET":                         // Eastern European, USSR Zone 1 +2
        return new TimeSpan (+2, 0, 0);
      case "BT":                          // Baghdad, USSR Zone 2 +3 hours
        return new TimeSpan (+3, 0, 0);
      case "ZP4":                         // USSR Zone 3 +4 hours
        return new TimeSpan (+4, 0, 0);
      case "ZP5":                         // USSR Zone 4 +5 hours
        return new TimeSpan (+5, 0, 0);
      case "ZP6":                         // USSR Zone 5 +6 hours
        return new TimeSpan (+6, 0, 0);
      case "WAST":                        // West Australian Standard +7 hours
        return new TimeSpan (+7, 0, 0);
      case "CCT":                         // China Coast, USSR Zone 7 +8 
        return new TimeSpan (+8, 0, 0);
      case "JST":                         // Japan Standard, USSR Zone 8 +9
            return new TimeSpan (+9, 0, 0);
      case "EAST":                        // East Australian Standard GST
      case "Guam":                        // Standard, USSR Zone 9  +10 hours
        return new TimeSpan (+10, 0, 0);
      case "IDLE":                        // International Date Line 
      case "NZST":                        // New Zealand Standard 
      case "NZT":                         // New Zealand  
        return new TimeSpan (+12, 0, 0);
      default:
        throw new Exception ("Unrecognized time zone");
    }
  }
}

Hosting a RemoteObject Class

In your RemoteObject hosting application, just like the Web service, you create a ServiceEnvironment object by loading an application configuration file and passing the name of the service environment configuration settings that you want to use. As before, I'll default to loading the main service environment from the configuration file.

using System; 
using System.MessageBus;
using System.MessageBus.Remoting;

class Host {
  static void Main () {
    ServiceEnvironment se = null;
  
    try {
        se = ServiceEnvironment.Load ();
        RemotingManager rm = se[typeof(RemotingManager)] as RemotingManager;
        if (rm == null)
            throw new Exception("No RemotingManager in ServiceEnvironment.");
  
        // Start the ServiceEnvironment. 
        se.Open ();

        // Register an instance of a TimeZoneObject for 
        // remote connection hosted in this service environment.
        TimeZoneObject tzoPST = new TimeZoneObject ("PST");
        TimeZoneObject tzoHAW = new TimeZoneObject ("HAW");
        TimeZoneObject tzoCET = new TimeZoneObject ("CET");
        TimeZoneObject tzoGMT = new TimeZoneObject ("GMT");
   
        // A client can create a proxy to a published object using 
        // RemotingManager.GetObject
        PublishedServerObject psoPST = 
            new PublishedServerObject (tzoPST, new Uri("urn:PST"));
        rm.PublishedServerObjects.Add (psoPST);

        PublishedServerObject psoHAW = 
            new PublishedServerObject (tzoHAW, new Uri("urn:HAW"));
        rm.PublishedServerObjects.Add (psoHAW);

        PublishedServerObject psoCET = 
            new PublishedServerObject (tzoCET, new Uri("urn:CET"));
        rm.PublishedServerObjects.Add (psoCET);

        PublishedServerObject psoGMT = 
            new PublishedServerObject (tzoGMT, new Uri("urn:GMT"));
        rm.PublishedServerObjects.Add (psoGMT);

        Console.WriteLine("Listening for requests. Press Enter to exit...");
        Console.ReadLine();

        // Cancel the publication of the objects.
        rm.PublishedServerObjects.Remove(psoPST);
        rm.PublishedServerObjects.Remove(psoHAW);
        rm.PublishedServerObjects.Remove(psoCET);
        rm.PublishedServerObjects.Remove(psoGMT);
    }
    finally {
        if (se != null) se.Close();
    }
  }
}

Note that in a RemoteObject hosting application, I use the RemotingManager class instead of the ServiceManager, as I did in my Web service. The unique part of this example is my use of the RemotingManager to publish objects the hosting application has created and initialized. I create a few TimeZoneObject instances as usual:

        TimeZoneObject tzoPST = new TimeZoneObject ("PST");
        . . .
        TimeZoneObject tzoGMT = new TimeZoneObject ("GMT");

However, I then publish the objects by creating a set of PublishedServerObject instances, each one of which associates a particular TimeZoneObject object with a unique name. When I add the PublishedServerObject instances to the RemotingManager PublishedServerObjects collection, the RemotingManager allows clients to access each published object by its unique name.

        PublishedServerObject psoPST = 
            new PublishedServerObject (tzoPST, new Uri("urn:PST"));
        rm.PublishedServerObjects.Add (psoPST);

I also have, by now, the expected configuration file for the server. The only difference between this RemoteObject configuration file and the earlier Web service configuration file is that I've specified a different port identity.

<configuration>   
  <system.messagebus>
    <serviceEnvironments>
      <serviceEnvironment name="main">
        <port>
          <identityRole>
            soap.tcp://localhost:46010/TimeZoneObjects
          </identityRole>
        </port>
        <!-- CAUTION: Security disabled for demonstration purposes only. -->
        <remove name="securityManager" />
        <policyManager>
          <!-- CAUTION: Security disabled for demonstration purposes only. -->
          <!--  Permits unsigned policy statements. Default requires signed policy statements -- >
        <areUntrustedPolicyAttachmentsAccepted>
          true
        </areUntrustedPolicyAttachmentsAccepted>
          <isPolicyReturned>true</isPolicyReturned>
        </policyManager>
        <remotingManager>
        </remotingManager>
      </serviceEnvironment>
    </serviceEnvironments>
  </system.messagebus>
</configuration>

Connecting to a Remote Instance of a RemoteObject

A client application can connect to a published RemoteObject by

  • Creating the ServiceEnvironment by calling the Load method as usual
  • Obtaining a RemotingManager from the ServiceEnvironment
  • Opening the ServiceEnvironment
  • Creating a URI that addresses the desired RemotingObject server
  • Calling the RemotingManager GetObject method and specifying the server's URI and the unique name of the specific object to which you want to connect
  • Calling methods, access fields, and properties on the remote object

The following RemotingObject client application connects to a published TimeZoneObject and retrieves and displays the current time in that time zone.

using System;
using System.Globalization;
using System.MessageBus;
using System.MessageBus.Remoting;

class Client {
  static void Main (string[] args) {
    string zone = "PST"; 
    if (args.Length > 0 && args[0].Length > 0) zone = args[0];

    CultureInfo ci = new CultureInfo ("");
    if (args.Length > 1 && args[1].Length > 0) ci = new CultureInfo (args[1]);
    
    ServiceEnvironment se = null;
  
    try {
       se = ServiceEnvironment.Load ();
       RemotingManager rm = se[typeof(RemotingManager)] as RemotingManager;
       if (rm == null)
        throw new Exception("No RemotingManager in ServiceEnvironment.");
  
      // Start the ServiceEnvironment. 
      se.Open();

      Uri serverPortUri =
          new Uri("soap.tcp://localhost:46010/TimeZoneObjects");

      // Build a proxy to an existing object
      string urn = "urn:" + zone;
      TimeZoneObject tzo =
          rm.GetObject (serverPortUri, new Uri(urn)) as TimeZoneObject;

      if (tzo != null) {
        Console.WriteLine ("It is currently {0} in the {1} time zone.",
                           tzo.GetCurrentTimeAndDate (ci), zone);
      }
      else {
        Console.WriteLine ("Could not connect to the remote object.");
      }
    }
    finally {
      if (se != null) se.Close();
    }
  }
}

Allowing Clients to Create a New Instance of a RemoteObject

A RemoteObject host application can also allow client applications to create their very own personal, private instances of a hosted class. In this case, the hosting application is typically extremely simple. It simply loads the configuration file in which all the real information resides.

using System; 
using System.MessageBus;
using System.MessageBus.Remoting;

class Host {
  static void Main() {
    ServiceEnvironment se = null;

    try {
      se = ServiceEnvironment.Load ();
      se.Open ();

      Console.WriteLine ("Listening for requests. Press Enter to exit...");
      Console.ReadLine ();
    }
    finally {
      if (se != null) se.Close ();
    }
  }
}

A configuration file that allows clients to create new instances of types contains a publishedServerTypes element. Each publishedServerTypes element's child add element lists a fully qualified type that client applications can request the server to create.

<configuration>   
  <system.messagebus>
    <serviceEnvironments>
      <serviceEnvironment name="main">
        <port>
          <identityRole>
            soap.tcp://localhost:46010/TimeZoneObjects
          </identityRole>
        </port>
        <!-- CAUTION: Security disabled for demonstration purposes only. -->
        <remove name="securityManager" />
        <policyManager>
          <!-- CAUTION: Security disabled for demonstration purposes only. -->
          <!--  Permits unsigned policy statements. 
Default requires signed policy statements -- >
          <areUntrustedPolicyAttachmentsAccepted>
            true
          </areUntrustedPolicyAttachmentsAccepted>
          <isPolicyReturned>true</isPolicyReturned>
        </policyManager>
        <remotingManager>
          <publishedServerTypes>
            <add type="TimeZoneObject, TimeZoneObject"/>
          </publishedServerTypes>
        </remotingManager>
      </serviceEnvironment>
    </serviceEnvironments>
  </system.messagebus>
</configuration>

Creating a New Instance of a RemoteObject

A client application can also create its very own personal, private instance of a remote class using the RemotingManager. The host application in this case is extremely simple:

  • Creating the ServiceEnvironment by calling the Load method as usual
  • Obtaining a RemotingManager from the ServiceEnvironment
  • Opening the ServiceEnvironment
  • Creating a URI that addresses the desired RemotingObject server
  • Calling the RemotingManager GetObject method and specifying the server's URI and the unique name of the specific object to which you want to connect
  • Calling methods, access fields, and properties on the remote object

The following RemotingObject client application connects to a published TimeZoneObject and retrieves and displays the current time in that time zone.

using System;
using System.Globalization;
using System.MessageBus;
using System.MessageBus.Remoting;

class Client {
  static void Main (string[] args) {
    string zone = "PST"; 
    if (args.Length > 0 && args[0].Length > 0) zone = args[0];

    CultureInfo ci = new CultureInfo ("");
    if (args.Length > 1 && args[1].Length > 0) ci = new CultureInfo (args[1]);
    
    ServiceEnvironment se = null;
  
    try {
       se = ServiceEnvironment.Load ();
       RemotingManager rm = se[typeof(RemotingManager)] as RemotingManager;
       if (rm == null)
        throw new Exception("No RemotingManager in ServiceEnvironment.");
  
      // Start the ServiceEnvironment. 
      se.Open();

      Uri serverPortUri =
          new Uri("soap.tcp://localhost:46010/TimeZoneObjects");

      // Build a proxy to an existing object
      string urn = "urn:" + zone;
      TimeZoneObject tzo =
          rm.GetObject (serverPortUri, new Uri(urn)) as TimeZoneObject;

      if (tzo != null) {
        Console.WriteLine ("It is currently {0} in the {1} time zone.",
                           tzo.GetCurrentTimeAndDate (ci), zone);
      }
      else {
        Console.WriteLine ("Could not connect to the remote object.");
      }
    }
    finally {
      if (se != null) se.Close();
    }
  }
}

Securing Indigo Applications

One major improvement in Indigo applications as compared to prior managed-communications technologies is that Indigo applications allow you to create secure communications. Indigo provides common security services such as confidentiality (also known as encryption), integrity, authentication, and authorization. By default, the Indigo performs many security tasks (such as encryption and digital signing) for you, greatly easing the job of building a secure application.

Effectively securing your Indigo application primarily consists of the following steps:

  1. Securing access to the methods your application publishes or exposes.
  2. Securing the content of messages sent to and from nodes of your application.

Let's look at securing access to published methods. Often you need to restrict access to your Indigo application to only a select group of predefined users. To do so, Indigo must determine the identity of a client using an authentication service. Indigo provides the following built-in authentication services:

  • Microsoft Windows authentication
  • X.509 certificate authentication
  • Basic user name and password authentication

When writing an application, the developer determines which roles can access a method. When deploying a Message Bus application, an administrator assigns user and group memberships to one or more roles. When Indigo authenticates a client, it determines the client's identity and maps that identity to a role.

An Indigo application might need to communicate across a network where anyone can monitor the messages. In many applications, you will not want third parties to read the messages during transmission or to alter or replace messages during transmission. You can use encryption to protect the confidentiality of your messages. Indigo supports encryption using both symmetric and asymmetric algorithms. Similarly, you can use digital signatures to protect the integrity of your messages and to verify the authenticity of the message sender.

Indigo provides most of the security you need by default. Message Bus applications automatically adapt to meet the security requirements specified by receiving nodes as defined by the WS-SecurityPolicy specification. For example, if you write a client and use the default configuration schemes, your client will automatically conform to the encryption and authentication requirements of most servers to which it might connect. In many cases, security requirements need to be explicitly specified only on the server side. Indigo provides many default security schemes that you can easily control through code attributes and configuration.

The Indigo security system automatically implements most security details when you add security attributes to your communications methods. For example, when you need to encrypt a message, you specify that you want to perform encryption by decorating your methods with descriptive attributes. When the method is accessed, Indigo automatically encrypts the message for you. You request Web method access control similarly.

Indigo provides two mechanisms to define and modify security requirements. A developer can define security requirements in a configuration file or explicitly in code, similar to the way you can define the communications transports and ports in a configuration file or code. However, system administrators might also want to define or modify the security requirements for a deployed application. They would use the configuration files to specify the new requirements.

Security Manager

The SecurityManager is the primary component of the Indigo security system. It defines the security requirements that are applied to incoming and outgoing messages of a specified port. Typically, every port has one SecurityManager.

You can easily create a new SecurityManager automatically when you create a new ServiceEnvironment. When you create a ServiceEnvironment by loading a configuration file, the Indigo framework automatically creates a SecurityManager if a definition of a SecurityManager is present in the configuration file. The SecurityManager contains three important properties: Scopes, Bindings, and EndpointSettings.

Scopes Property

The Scopes property contains a collection of security scope objects.

Security Scope

A security scope defines the security requirements you want associated with a service method. For example, if you want to specify that a method needs to use encryption, you would define it in a security scope. If you want to specify that a method requires an access check, you would also define it in a security scope.

You use a security scope to specify the following requirements for a Web service method:

  • It must encrypt its message bodies.
  • It requires an access check (and that implicitly involves message integrity and sender authentication)

Security Profile

A security profile defines how to implement a security requirement. It specifies how the encryption, integrity and sender authentication requirements can be met. For example, most systems support a variety of encryption algorithms. One security profile can specify that the DES3 encryption should be used, while a different profile specifies RSA encryption.

The following security requirement implementations can be defined using security profiles:

  • Algorithms to use for encryption
  • Keys or X.509 certificates to use for encryption
  • The type of authentication to use (such as Windows, basic user name and password, or X.509 certificate authorization)
  • How to use licensing

Bindings Property

Security scopes are not associated with a security profile until the two are explicitly bound by a security binding. Generally, your application will have one or more scopes associated with every service method or remote object. A system will typically have numerous profiles available. When an administrator configures or deploys an application, the administrator decides which implementation—for example, authentication type—to use by binding a scope to a profile. The SecurityManager Bindings property contains these bindings and is populated by configuration information in the Machine.config and Application.config files when you create a new SecurityManager.

EndpointSettings Property

The EndpointSettings property encapsulates security capabilities of the current endpoint. All cryptographic algorithms, keys, X.509 certificates, and authentication providers are enumerated here. Like the Bindings property, the EndpointSettings property is typically populated by information from Machine.config and Application.config files when you create a new SecurityManager. These values are usually configured during application deployment and administration rather than during development.

Design-Time vs. Deploy-Time

While this design initially seems complicated, the distinction between security scopes and security profiles allow you to separate your security requirements from your security implementation. Typically, you will specify your security requirements in code (for example, this method restricts access control to users in role X). However, you typically specify the implementation of that security requirement in a configuration file (for example, that Windows authentication should be used). This separation allows you to change the implementation of a security requirement (type of authentication, type of encryption, and so forth) that your application uses without having to recompile.

Creating an Authenticated Web Service

Let's add authentication to my original Web service example from this chapter. In the following example, I've simply added the two lines in bold: a using declaration, and a ServiceSecurity attribute that indicates Indigo should secure access to the GetUtcTimeAndDate method.

using System;
using System.Globalization;
using System.MessageBus;
using System.MessageBus.Services;
using System.MessageBus.Security;

namespace WiseOwl {
  [DatagramPortType(Name="Time",
                    Namespace="http://www.wiseowl.com/WebServices")]
  public class Time {
    [ServiceSecurity(Name="GetUtcDateAndTimeScope", Role="StandardUserRole")]
    [ServiceMethod]
    public string GetUtcTimeAndDate(string culture) {    
      Console.WriteLine ("Client requested UTC time for culture {0}", culture);
      CultureInfo ci = new CultureInfo (culture);
      return DateTime.UtcNow.ToString ("F", ci);
    }
  }
}

I didn't change the host application at all, so I won't list it again here. However, I did add a securityManager element to the host's application configuration file, host.exe.config. Note that the securityManager element contains an applicationSecurity element with a binding that maps the scope named "GetUtcDateAndTimeScope" to the profile named "windows".

<configuration>
  <system.messagebus>
    <serviceEnvironments>
      <serviceEnvironment name="main">
        <port>
          <identity-Role>soap.tcp://localhost:46000/TimeService/</identityRole>
        </port>
        <!--  Bind the scope defined using ServiceSecurityAttribute to 
a profile that uses Windows Authentication. -->
        <securityManager>
          <applicationSecurity>
            <binding scope="GetUtcDateAndTimeScope" profile="windows" />
          </applicationSecurity>
        </securityManager>
        <policyManager>
          <!-- CAUTION: Security disabled for demonstration purposes only. -->
          <!--  Permits unsigned policy statements. Default requires signed policy statements -- >
          <areUntrustedPolicyAttachmentsAccepted>
            true
          </areUntrustedPolicyAttachmentsAccepted>
          <isPolicyReturned>true</isPolicyReturned>
        </policyManager>
         <serviceManager>
          <activatableServices>
            <add type="WiseOwl.Time, TimeService" />
          </activatableServices>
        </serviceManager>
      </serviceEnvironment>
    </serviceEnvironments>
  </system.messagebus>
</configuration>

The ServiceSecurity attribute on my Web service method states that access to the method is constrained by the scope named GetUtcDateAndTimeScope. Conceptually, an administrator has bound that scope name to the "windows" profile—a system-defined profile that specifies that the desired implementation is the Windows authentication implementation.

Indigo will now require a client to authenticate using Windows authentication. The method's ServiceSecurity attribute requires the authenticated user to be in the role named StandardUserRole to access the method. The mapping between users and roles resides in the application's security configuration file, which in this example, is named host.exe.security. In my WiseOwl domain, I've created a new group named "Indigo Users" and added my user account to that group. Then in the application's security configuration file, I specify that an authenticated user in the Indigo Users group is in the StandardUserRole role and, therefore, has access to my Web service method.

<securityData>
  <authorizationData>
    <memoryMapping id="mainAuthorizationData">
      <globalEntries>
        <windowsRoleAssignment domain="WiseOwl" group="Indigo Users" 
                     roles = "StandardUserRole"/>
      </globalEntries>
    </memoryMapping>
  </authorizationData>
</securityData>

Calling an Authenticated Web Service Method

You can use the original Web service client listed previously in this chapter to call the authenticated version of the Web service method. Run the client while logged in using an account that is currently authenticated by the domain that the Web service recognizes. You don't need to make any changes to the client code to call an authenticated Web service. However, I did need to make one small change to the client's configuration file. The original version removed the security manager from the service environment as follows:

<remove name="securityManager" />

Now I need the default security manager in the service environment, so I deleted the preceding line from the client.exe.config file for my authenticated Web service client.

Variations on Authentication

Now that I've enabled authentication for my Web service method, I can easily change the type and manner of authentication by editing the configuration files. For example, if I want to switch from Windows authentication to requiring a user name and password, I need to make only two changes at the server and one at the client. First, I change the securityManager in my host.exe.config application configuration file so that my security code now binds to the standard userNamePassword profile as follows:

     <securityManager>
        <applicationSecurity>
           <binding scope="GetUtcDateAndTimeScope" profile="userNamePassword" />
          </applicationSecurity>
     </securityManager>

Then I change my application security file, host.exe.security, so that it specifies the allowed user names and their respective passwords, and maps the user names to the appropriate roles.

<SECURITYDATA>
  <credentials>
    <username id="mainUsers" nonceLength="24">
      <memoryPasswordResolver>
        <add user="Brent" password="password1" />
        <add user="Lisa" password="password2" />
      </memoryPasswordResolver>
    </username>
  </credentials>
  <authorizationData>
    <memoryMapping id="mainAuthorizationData">
      <globalEntries>
        <userNameRoleAssignment user="Brent" roles = "StandardUserRole"/>
        <userNameRoleAssignment user="Lisa" roles = "StandardUserRole"/>
      </globalEntries>
    </memoryMapping>
  </authorizationData>
</securityData>

I feel obliged to point out that you can replace the memoryPasswordResolver used in the prior example with your own custom mechanism to retrieve user names and passwords from some more secure and presumably encrypted source.

Because the service application requires a user name and password, I must now change the client application so that it provides the user name and password. The easiest way is to use my original client application and specify the user name and password in the client application's security configuration file. Here's the original client application. The following code is a client application:

// CLIENT.CS
using System;
using System.MessageBus;
using System.MessageBus.Services;
using www_wiseowl_com.WebServices;  // The imported service namespace

public class Client {
  public static void Main(string[] args) {
    string culture = "en-US";
    if (args.Length > 0) culture = args[0];

    // Load the default service environment, called "main".
    ServiceEnvironment se = null;
    
    try {
      se = ServiceEnvironment.Load();

      // Retrieve the ServiceManager from the default environment
      ServiceManager sm =
        se[typeof(ServiceManager)] as ServiceManager;
      if (sm == null)
        throw new Exception ("ServiceManager is not available.");

      // Start the service environment.
      se.Open();

      // Create a proxy channel that points to the service to call.
      Uri uri = new Uri("soap.tcp://localhost:46000/TimeService/");
      ITimeChannel channel = (ITimeChannel)
          sm.CreateChannel(typeof(ITimeChannel), uri);

      Console.WriteLine(channel.GetUtcTimeAndDate (culture));
    }
    catch (Exception e) {
      Console.WriteLine (e);
    }
    finally {
      if (se != null) se.Close();
    }
  }
}

Because the prior client application loads its configuration from the default service environment, I can specify my security settings using a security configuration file. My client application is named client.exe—therefore my client application security file is client.exe.security and looks like this:

<securityData>
    <tokens>
        <tokenCache id="mainTokenCache">
            <userNameToken user="Brent" password="password"/>
            <userNameToken user="Lisa" password="wordpass"/>
        </tokenCache>
    </tokens>
</securityData>

Rather than specifying the user name and password pairs in the configuration file, I can alternatively initialize the security token cache programmatically before I open the security environment:

// Retrieve the SecurityManager from the default environment
SecurityManager secMan = se [typeof(SecurityManager)] as SecurityManager;

if (secMan == null) {
  throw new ApplicationException("Security Manager not present.");
}
§// Create a new UserName Token and add password and user information.
UserNameToken t = new UserNameToken (userName, password, 24); 
secMan.EndpointSettings.TokenCache.AddToken(t);
§se.Open ();

Message Confidentiality

When you enable confidentiality on a Web service method, Indigo encrypts the body of your messages automatically. When a client calls your encrypted Web service, Indigo automatically acquires the appropriate keys and encrypts the messages to meet the requirements of the service. An administrator typically decides encryption requirements during deployment by using configuration files.

You enable confidentiality by passing the true Confidentiality property of the ServiceSecurityAttribute. Therefore, using following attribute in place of the one listed previously requires authentication and authorization to access the method. It also encrypts the SOAP message body of the message sent to the method.

[ServiceSecurity(Name="GetUtcDateAndTimeScope", 
                 Role="StandardUserRole", Confidentiality = true)]

As with the prior example, you typically select the specific encryption implementation using configuration file entries.

Reliable and Durable Messaging

Reliable messaging helps protect an application from transient communications problems between nodes by automatic failure detection and transient failure recovery. Indigo normally hides these recoverable errors from your application, allowing you to concentrate on developing your functionality rather than writing lots of code to deal with network reliability issues. Reliable messaging provides certain assurances about the delivery of messages to the applications, and it either fulfills the assurances or informs the application it was unable to fulfill them.

Durable messaging addresses a different problem. Durable messaging insulates applications from node failures at the messaging endpoints that are hosting the application by providing durable storage at the messaging endpoints. This allows Indigo to restore a communications session between messaging endpoints after a failure.

A dialog is the key Indigo abstraction that provides reliable message-based communication between Indigo services and other application endpoints. A dialog is a reliable, bi-directional, session-oriented message exchange between two endpoints and essentially consists of the state at these two endpoints. Dialogs can be configured to provide a variety of communication requirements, from a tightly coupled, exactly once, in-order delivery to a more loosely coupled, asynchronous style of delivery.

Reliable messaging between two endpoints requires that each of the endpoints store information about the state of the dialog between them. Indigo stores the state of a dialog in a dialog store at each endpoint. The store allows the objects representing that state to disappear and Indigo can recover the dialog. A durable store provides the ability to recover the dialog. When either the client or the server process that hosts the dialog terminates, it can come back at a later time and continue the dialog from where it left off. Durable storage of dialog state thus makes recovery from process or node failure possible. When the storage is not durable, the objects representing the dialog state are retained only in memory and the dialog will be lost in the event of node failure.

A dialog channel provides the delivery assurances for each endpoint of the dialog. This interface provides state management at each dialog endpoint and the methods to manage messages in the send and receive buffers.

Defining a Dialog-Based Service

I need to make only one major change to the previous service application for it to operate using a dialog. Instead of applying the DatagramPortTypeAttribute to the Web service class, I need to apply the DialogPortTypeAttribute to the class, as shown in the following example. The DialogPortTypeAttribute requests Indigo to establish a session between an instance of this service class and the client who created it. All subsequent communication from and to the client uses the same service instance.

using System;
using System.Globalization;
using System.MessageBus;
using System.MessageBus.Services;

namespace WiseOwl {
  [DialogPortType (Name="Time",
                  Namespace="http://www.wiseowl.com/WebServices")]
  public class Time {
    [ServiceMethod]
    public string GetUtcTimeAndDate(string culture) {    
      Console.WriteLine ("Client requested UTC time for culture {0}", culture);
      CultureInfo ci = new CultureInfo (culture);
      return DateTime.UtcNow.ToString ("F", ci);
    }
  }
}

The client could pass an object or an interface as a method parameter to this service. The service could subsequently call methods on the client's object or interface implementation to initiate callbacks to the client. Using a dialog produces a stateful, persistent session between the client and the service.

The client application for this dialog-based service is also nearly identical to my original Web service client application. In the following example, the CreateChannel call creates a session from the client to the service:

using System;
using System.Threading;
using System.MessageBus;
using System.MessageBus.Services;
using www_wiseowl_com.WebServices;  // The imported service namespace

public class Client {

  private static AutoResetEvent doneEvent = new AutoResetEvent (false);

  public static void Main(string[] args) {
    string culture = "en-US";
    if (args.Length > 0) culture = args[0];

    // Load the default service environment, called "main".
    ServiceEnvironment se = null;
    
    try {
      se = ServiceEnvironment.Load();

      // Retrieve the ServiceManager from the default environment
      ServiceManager sm =
        se[typeof(ServiceManager)] as ServiceManager;
      if (sm == null)
        throw new Exception ("ServiceManager is not available.");

      // Start the service environment.
      se.Open();

      // Create a proxy channel that points to the service to call.
      Uri uri = new Uri("soap.tcp://localhost:46000/TimeService/");

      ITimeChannel channel = (ITimeChannel)
          sm.CreateChannel(typeof(ITimeChannel), uri);

      Console.WriteLine(channel.GetUtcTimeAndDate (culture));

      Console.WriteLine("Press enter to stop this client...");
      Console.ReadLine();

      // Either the client or the service can initiate showdown of the session
      // by calling IDialogPortTypeChannel.DoneSending

      // First, register for the Done event
      channel.Done += new EventHandler (DoneHandler);

      // Now, initiate the shutdown
      channel.DoneSending();

      // Now, wait for the shutdown to complete
      doneEvent.WaitOne ();
    }
    catch (Exception e) {
      Console.WriteLine (e);
    }
    finally {
      if (se != null) se.Close();
    }
  }

  static void DoneHandler (object sender, EventArgs e) {
    doneEvent.Set();
  }
}

Because I decorated the definition of the service with the DialogPortTypeAttribute, the Wsdlgen utility produces a different definition of the ITimeChannel interface definition. The class that implements the interface must also implement the IDialogPortTypeChannel interface, resulting in the client and service using a dialog-based communication channel.

Finally, either the client or the service can initiate an orderly shutdown of the session by calling the IDialogPortTypeChannel.DoneSending method. The application must then wait for the Done event before terminating. The previously listed client does this after you press Enter to allow the client application to shut down.

Programming Transactions

Indigo allows you to pass a transaction across a communication channel, which lets you coordinate operations performed by multiple services. To users, a transaction is a single event that either happens or doesn't happen. To developers, a transaction allows them to write components that can participate in distributed environments.

The System.Transactions.Transaction is the base class representing a single transaction. A transaction is created through a TransactionManager instance using the CreateTransaction or UnmarshalTransaction method. Alternatively, you can also create a transaction by calling the static System.Transactions.Transaction.Create method, which creates a new transaction with default values.

A transaction can be committed only by the client application that created the transaction. When a client application wants to allow access to the transaction by multiple threads but also wants to prevent those other threads from committing the transaction, the application can use a clone of the transaction. A cloned transaction has the same capabilities as the original transaction, except for the ability to commit the transaction.

Transaction instances can be constructed using the static method Create of the Transaction class as follows:

      tx = Transaction.Create();

Alternatively, you can create a new TransactionManager instance and have it create a transaction for you, as follows:

      tm = TransactionManager.Create();                                             
      tx = tm.CreateTransaction();

There are also alternative creation mechanisms that allow you to supply the description, isolation level, and timeout value of the transaction. To commit or roll back a transaction, you will call the respective Commit or Rollback methods of the Transaction instance.

tx.Commit();
tx.Rollback();

Applications use outcome notifications to release resources or perform other actions after a transaction commits or aborts. A transaction participant that wants to be notified of the commit-abort decision registers for the TransactionOutcome event.

In the following example, we have implemented an event handler named OnTransactionCompleted that is going to handle the TransactionCompleted event. We then associate this handler with the event by using the following code:

tx.TransactionCompleted +=
         new TransactionCompletedEventHandler (OnTransactionCompleted);

After you have committed or rolled back the transaction, the TransactionCompleted event will be fired automatically by the system, which triggers the execution of OnTransactionCompleted.

This example will demonstrate how to enlist in a Microsoft SQL Server transaction. It first creates a transaction, and then opens a connection to "pubs," which is a SQL server database. The example then marshals the transaction object to make it compatible with EnterpriseService transactions, and associates the marshaled transaction with the opened connection. The user then has a chance to commit or roll back the transaction.

A new SqlConnection is instantiated and opened, with the connecting string set to the local server and the database set to "pubs".

conn = new SqlConnection();
conn.ConnectionString =  @"Server=(local);Trusted_Connection=SSPI;Database=pubs;E nlist=false;";
conn.Open();

To enlist the current transaction as a distributed transaction of the open connection, you will call the EnlistDistributedTransaction method of the SqlConnection class. However, because this method only takes an object of the System.EnterpriseServices.ITransaction type, you will need to marshal the existing transaction for compatibility first. The Marshal method is used to perform such functionality.

estx = (System.EnterpriseServices.ITransaction)
                   tx.Marshal(MarshaledTransactionTypeNamespaceUri.ITransaction);
conn.EnlistDistributedTransaction (estx);

Summary

Indigo is the "Longhorn" general-purpose messaging framework that you can use to build a wide variety of rich communication-based applications. You can build stateless, Web service applications and clients for such applications. You can build RemoteObject services and their clients. You can establish reliable and durable communications sessions.

Indigo is a great communications framework that you can use to build interesting and powerful collaboration applications.


Continue to Chapter 7: Creating Mobility-Aware Longhorn Applications

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.