ADO.NET
Building a Distributed Component Server using C# and .NET

Gopalan Suresh Raj

Note
To work with any of these samples, you will need the following:
.........................................Microsoft .NET SDK
.........................................Microsoft Visual Studio.NET Beta 2 or higher

 

ADO.NET's DataSet

The DataSet is actually an in-memory view of the database. It can contain multiple DataTable and DataRelation objects. This allows developers to navigate and manipulate the collection of tables and their relationships. ADO.NET involves disconnected DataSets as it is modeled for a distributed architecture. As the DataSet itself is disconnected from the data source, it must provide a way to track changes to itself. For this purpose, the DataSet class provides a number of methods which can be used to reconcile changes to itself with the actual database (or other data source) at a later point in time. Some of these methods include HasChanges(), HasErrors(), GetChanges(), AcceptChanges(), and RejectChanges(). These methods can be used to check for changes that have happened to the DataSet, obtain modifications in the form of a changed DataSet, inspect the changes for errors, and then either accept or reject those changes. If the changes need to be communicated back to the data store back-end, the Data Adapter can be asked to be updated using the Data Adapter's Update() method.

The DataSet is intended to help web applications which are by their very nature disconnected. You never know that the data in the database has changed until you have updated the records that you were editing or perform other tasks which needs reconciliation with the back end Data Store.

The BookKeeper Module

In this article, we are going to build a Distributed Component to illustrate ADO.NET's disconnected operation facility - the DataSet. As mentioned earlier, the DataSet facilitates the client to manipulate and update a local copy of any number of related tables while still disconnected from the data source and submit the modified data back for processing using a related data adapter at a later point in time. As a result, all our data is going to be maintained on the server in an XML datastore (or flat-file) until changes are committed to a database back-end at a later point in time !!!

Our hypothetical BookKeeper Module (that we will build in this article), is actually a Distributed component that performs just a bunch of functions. It is the module that manages creation and deletion of accounts (Checking accounts or Savings accounts) for a Large Commercial Bank Project. It offers no other services except "Create Account", and "Delete Account", and "Find Account".

Building Distributed Components using the .NET Framework

While traditional Distributed COM (DCOM - the wire protocol that provides support for distributed computing in COM) is fine for distributed computing, it is not appropriate for the internet because it does'nt work well in the face of firewalls and NAT software. The other shortcomings of DCOM are expensive lifecycle management, protocol negotiation, and binary formats.

To ease these shortcomings, .NET provides the Remoting API that allows you to use a host of channels - such as TCP and HTTP (which uses SOAP) - for distributed computing. It also permits you to plug in your own custom channels, if you require this functionality.

To ease development, I recommend using the Visual Studio.NET IDE. However, you are free to develop your application in the favorite editor of your choice, using the command-line to execute the various commands to build and deploy it.

The various steps that are involved in creating a Distributed Component using C# and the .NET Framework are as follows (I'm going to assume you're using the VS.NET IDE):

  1. Create a Visual C# - Console Application project

  2. Generate a Key-Value pair to use when deploying your Shared Assembly

  3. Configure your Project Property Pages with the right information

  4. Develop the BookKeeper.cs library

  5. Modify the generated AssemblyInfo.cs to add the right assembly information

  6. Build the Project Files

  7. Deploy the application as a Shared Assembly

1. Create a Visual C# - Console Application project

Create a new Visual C# Console Application project.

 

2. Generate a Key-Value pair to use when deploying your Shared Assembly

Shared Assemblies are those that can be used by any client application, such as a system DLL that every process in the system can use. Unlike private-assemblies, shared assemblies must be published or registered in the system's Global Assembly Cache (GAC). As soon as they are registered in the GAC, they act as system components. An essential requirement for GAC registration is that the component must possess originator and version information. In addition to other metadata information, these two items allow multiple versions of the same component to be registered and executed on the same machine. Unlike Classic COM, we don't have to store any information in the system registry for clients to use these shared assemblies.

There are three general steps to registering shared assemblies in the GAC:

  1. The Shared Name (sb.exe) utility should be used to obtain the public/private key pair. This utility generates a random key pair value, and stores it in an output file - for example, BookKeeper.key.

  2. Build the assembly with an assembly version number and the key information in the BookKeeper.key

  3. Using the .NET Global Assembly Cache (gacutil.exe) utility, register the assembly in the GAC.

The assembly now becomes a shared assembly and can be used by any client in the system.

Therefore, as a first step, use the Shared Name Utility to obtain a public/private key pair and store it in a file (BookKeeper.key, in this case) as shown below.

Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper>sn -k BookKeeper.key

Microsoft (R) .NET Framework Strong Name Utility Version 1.0.2914.16
Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

Key pair written to BookKeeper.key

C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper>

The -k option generates the random key pair and saves the key information in the BookKeeper.key file. We use this file as input when we build our Shared Assemblies.

 

3.  Configure your Project Property Pages with the right information

Configure the Project Properties with the right information. Make sure you specify the Assembly Name that you want for the Assembly. Specifically, move to the General tab, and in the Wrapper Assembly Key File area, enter the key file to use. In this case, it is BookKeeper.key.

 

4. Develop the BookKeeper.cs library

The DataSet's properties expose the Tables and Relations that it contains. After creating a new DataSet object on Line 54, we add a Table and Columns to it. The createSchema() method from Lines 143-163 demonstrates how to create a Table, and it's columns dynamically. Line 147 shows how to create a Table using the Add() method of the Table object. We go through a similar process from Lines 149 to 156 to create Columns in a similar fashion by invoking the Add() method on each Table's Column collections. Each of these Tables or Columns can later be referred to by name. In order to assign the primary key for the Accounts table, we have to create the DataColumn array to hold one or more fields representing a key or a composite key as shown from Lines 159 to 161. In this case we only have a single key field, AccountKey. We set the PrimaryKey property of the table to this array of key columns.

The insertData() method from Lines 172 to 191 shows how to insert data into the Table. Tables and Relations are important properties of the DataSet. Not only do they describe the structure of the in-memory database, but the DataTables inside the collection also hold the content of the DataSet.

Note:
There is an inherent and 'intentional bug' in the way deletes are handled in the BookKeeper Server. You can only delete records in the order that they were created - i.e., you'd have to delete the latest record first !!! If you do try to delete in any other order, you will not be able to create the next new Account !!! This is because of the way new Primary Keys are generated. For explanation, please look at the
BookKeeper::getNextKey() method. I intentionally introduced this bug in the program to demonstrate that even though ADO.NET's DataSet is physically disconnected from the DataBase, it maintains and manages its data internally,  and  still checks Constraints and behaves very much like a regular database.
 

XML and the DataSet

The DataSet tightly integrates with XML and is therefore universally interoperable. The three methods that facilitate this are WriteXml(), WriteXmlSchema(), and ReadXml(). WriteXmlSchema() only dumps the schema of the tables, including all tables and the relationship between the tables. WriteXml() can dump both the schema and the table data as an XML encoded string as shown on Lines 283 and 285. Both WriteXmlSchema() and WriteXml()accept a Stream, TextWriter, XmlWriter, or a String representing a filename. WriteXml() accepts an XmlWriteMode as the second argument.

When the XmlWriteMode.WriteSchema is passed in as the second argument to WriteXml() method, the method dumps both the Schema and the Data as shown on Lines 283 and 285. You can retrieve only the data portion of the XML by using the XmlWriteMode.IgnoreSchema property.

The DataSet also provides methods to reconstruct itself from an XML Document. Use ReadXmlData() for reading XML data documents, and ReadXmlSchema() for reading XML Schema documents. As shown on Line 58, use ReadXml() to read both the schema and the data from an XML file and reconstruct and populate the DataSet object.

The DataSet is therefore the most important construct in ADO.NET. Since the DataSet does not tie to an underlying representation such as Microsoft Access or SQL Server, it is extremely portable. Its data format is self-described in its schema, and its data is in pure XML. A DataSet is self-contained regardless of how it was created, perhaps be reading data from Microsoft Access, from SQL Server, from an external XML file, or even being dynamically generated as we've seen in this example. This portable XML-based entity sets a new standard for data exchange.

BookKeeper.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
195:
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
206:
207:
208:
209:
210:
211:
212:
213:
214:
215:
216:
217:
218:
219:
220:
221:
222:
223:
224:
225:
226:
227:
228:
229:
230:
231:
232:
233:
234:
235:
236:
237:
238:
239:
240:
241:
242:
243:
244:
245:
246:
247:
248:
249:
250:
251:
252:
253:
254:
255:
256:
257:
258:
259:
260:
261:
262:
263:
264:
265:
266:
267:
268:
269:
270:
271:
272:
273:
274:
275:
276:
277:
278:
279:
280:
281:
282:
283:
284:
285:
286:
287:
288:
289:
290:
291:
292:
293:
294:
295:
296:
297:
298:
299:
300:
301:
302:
303:
304:
305:
306:
//////////////////////////////////////////////////////
/// The following example shows a Distributed Component
/// developed using C# and the .NET Framework.
///
/// author: Gopalan Suresh Raj
/// Copyright (c), 2001-2002. All Rights Reserved.
/// URL: https://gsraj.tripod.com/
/// email: gopalan@gmx.net
///
//////////////////////////////////////////////////////
using System;
// Include the following to use the Remoting API
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
// Include the following for the Window Message Box
using System.Windows.Forms;
// Include the following for ADO.NET functionality
using System.Data;
// Include the BookKeeper namespace
using BookKeeper;

namespace BookKeeper {

  /// <summary>
  /// The AccountDetails structure
  /// </summary>

  /// Make this structure serializable
  [Serializable]
  public struct AccountDetails {
    public AccountKey key;
    public string customerNames;
    public float balance;
  }

  /// <summary>
  /// The BookKeeper Class.
  /// </summary>

  /// Make this class serializable
  [Serializable]
  public class BookKeeper : MarshalByRefObject, IDisposable {

    int nextKey_;
    DataSet dataSet_;
    bool isDisposed_ = false;

    /// <summary>
    /// Public No-argument Default Constructor
    /// </summary>
    public BookKeeper () {
      // Construct a new DataSet object
      dataSet_ =
new DataSet ("BookKeeper");
      try {
        // Populate the object with the contents of the XML File which
        // contains its schema and data from a previous invocation.
        dataSet_.ReadXml ("Accounts.xml");
      }
      catch (Exception exception) {
        Console.WriteLine (exception.ToString ());
        // If XML File is not found, the schema has not been
        // created yet. So create a schema.
        this.createSchema ();
        MessageBox.Show ("BookKeeper::BookKeeper() created Schema", "BookKeeper");
      }
      nextKey_ = getNextKey ();
    }

    /// <summary>
    /// The Destructor method
    /// </summary>
    ~BookKeeper () {
      if (false == this.isDisposed_) {
        Dispose ();
      }
    }

    /// <summary>
    /// The Dispose method
    /// </summary>
    public void Dispose () {
      this.writeDataSet ();
      MessageBox.Show ("BookKeeper::Dispose() wrote Schema to disk", "BookKeeper");
      // No need to finalize if user called Dispose()
      // so suppress finalization
      GC.SuppressFinalize (this);
      this.isDisposed_ = true;
    }


    /// <summary>
    /// The accessor for the DataSet member
    /// </summary>
    public DataSet dataSet {
      get {
        return this.dataSet_;
      }
    }

    /// <summary>
    /// The createAccount method
    /// </summary>
    /// <param name="accountType">Specifies Checking or Savings account</param>
    /// <param name="customerNames">The Customer Names, owners of this account</param>
    /// <param name="startingBalance">The tarting balance</param>
    /// <returns></returns>
    public AccountKey createAccount (AccountType accountType,
      string[] customerNames,
      float startingBalance) {
      MessageBox.Show ("BookKeeper::createAccount() invoked", "BookKeeper");
      AccountKey key = null;
      key = new AccountKey ();
      key.Key = nextKey_++;
      key.Type = accountType;
      // Concatenate all customer names
      string names = "";
      foreach (string element in customerNames) {
        names += (" "+element);
      }
      int type = 1;
      if (key.Type == AccountType.SavingsAccount) {
        type = 2;
      }
      this.insertData (key.Key, type, names.Trim(), startingBalance);
      MessageBox.Show ("Key is :"+key.Key, "BookKeeper");

      return key;
    }

    /// <summary>
    /// The deleteAccount method
    /// </summary>
    /// <param name="key">The Account Number to delete</param>
    public bool deleteAccount (AccountKey key) {
      MessageBox.Show ("BookKeeper::deleteAccount() with Key :"+key.Key, "BookKeeper");
      return this.removeAccount (key.Key);
    }

    /// <summary>
    /// The createSchema method
    /// </summary>
    protected void createSchema () {
      MessageBox.Show ("BookKeeper::createSchema() invoked", "BookKeeper");
      lock (this) {
        // Add a new Table named "Accounts" to the DataSet collection tables
        dataSet_.Tables.Add ("Accounts");
        // Add new columns to the table "Accounts"
        dataSet_.Tables ["Accounts"].Columns.Add ("AccountKey",
          Type.GetType (
"System.Int32"));
        dataSet_.Tables [
"Accounts"].Columns.Add ("AccountType",
          Type.GetType (
"System.Int32"));
        dataSet_.Tables [
"Accounts"].Columns.Add ("CustomerNames",
          Type.GetType (
"System.String"));
        dataSet_.Tables [
"Accounts"].Columns.Add ("Balance",
          Type.GetType (
"System.Currency"));

        // Register the column "AccountKey" as the primary key of the table "Accounts"
        DataColumn[] keys = new DataColumn [1];
        keys [0] = dataSet_.Tables [
"Accounts"].Columns ["AccountKey"];
        dataSet_.Tables [
"Accounts"].PrimaryKey = keys;
      }
    }

    /// <summary>
    /// The insertData method
    /// </summary>
    /// <param name="accountKey">The Account Number to create</param>
    /// <param name="accountType">Specifies Checking or Savings account</param>
    /// <param name="customerNames">The Customer Names, owners of this account</param>
    /// <param name="balance">The tarting balance</param>
    protected void insertData(int accountKey,
      int accountType,
      string customerNames,
      float balance) {
      MessageBox.Show ("BookKeeper::insertData() invoked", "BookKeeper");
      lock (this) {
        try {
          DataRow newRow = dataSet_.Tables ["Accounts"].NewRow () ;
          newRow ["AccountKey"] = accountKey;
          newRow ["AccountType"] = accountType;
          newRow ["CustomerNames"] = customerNames;
          newRow ["Balance"] = balance.ToString();
          dataSet_.Tables ["Accounts"].Rows.Add (newRow);
        }
        catch (Exception exception) {
          Console.WriteLine (exception.ToString ());
          MessageBox.Show ("BookKeeper::insertData() Failed", "BookKeeper");
        }
      }
    }

    /// <summary>
    /// The findAccount method
    /// </summary>
    /// <param name="accountKey">The Account Number</param>
    /// <returns>The Details of this account</returns>
    protected AccountDetails findAccount (int accountKey) {
      MessageBox.Show ("BookKeeper::findAccount() invoked", "BookKeeper");
      AccountDetails details = new AccountDetails ();

      lock (this) {

        DataTable table = dataSet_.Tables ["Accounts"];
        // Find an order from the table
        DataRow row = table.Rows.Find (accountKey);

        // Populate the details object
        AccountKey key = new AccountKey ();
        key.Key = accountKey;
        int type = (int)System.Int32.Parse(row ["AccountType"].ToString ());
        if (type == 1) {
          key.Type = AccountType.CheckingAccount;
        }
        else {
          key.Type = AccountType.SavingsAccount;
        }
        details.key = key;
        details.customerNames = row ["CustomerNames"].ToString ();
        details.balance = (float)System.Double.Parse(row ["Balance"].ToString ());
      }
      return details;
    }

    /// <summary>
    /// The removeAccount method
    /// </summary>
    /// <param name="accountKey">The Account Number</param>
    /// <returns>true if successful, false if not</returns>
    protected bool removeAccount (int accountKey) {
      MessageBox.Show ("BookKeeper::removeAccount() invoked with key: "+accountKey, "BookKeeper");
      bool result = false;
      lock (this) {
        DataTable table = dataSet_.Tables ["Accounts"];
        try {

          table.Rows.Find (accountKey).Delete ();
          table.AcceptChanges ();
          dataSet_.AcceptChanges ();

          result = true;
        }
        catch (Exception exception) {
          Console.WriteLine (exception.ToString ());
          MessageBox.Show (exception.ToString (), "BookKeeper::removeAccount");
        }
      }
      return result;
    }

    /// <summary>
    /// The getNextKey method
    /// </summary>
    /// <returns>The New Account Number (primary key)</returns>
    protected int getNextKey () {
      int result = 1;
      lock (this) {
        try {
          DataTable table = dataSet_.Tables ["Accounts"];
          // This is a hack. But what the heck!
          // This is just a demo !!!

          if (null != table) {
            result = table.Rows.Count+1;
          }
        }
        catch (Exception exception) {
          Console.WriteLine (exception.ToString ());
        }
        finally {
          result = (result == 0)?1:result;
        }
      }
      return result;
    }

    /// <summary>
    /// The writeDataSet method
    /// </summary>
    public void writeDataSet () {
      MessageBox.Show ("BookKeeper::writeDataSet() invoked", "BookKeeper");
      lock (this) {
        dataSet_.AcceptChanges();
        // Dump the DataSet to the Console
        dataSet_.WriteXml (Console.Out, XmlWriteMode.WriteSchema);
        // Dump the DataSet to an XML File
        dataSet_.WriteXml ("Accounts.xml", XmlWriteMode.WriteSchema);
      }
    }

    /// <summary>
    /// The Main method
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args) {
      TcpChannel channel = new TcpChannel (1099);
      ChannelServices.RegisterChannel (channel);

      RemotingConfiguration.RegisterWellKnownServiceType (typeof(BookKeeper),
                                                          
                                  "BookKeeper",
                                                                                            WellKnownObjectMode.Singleton);

      System.Console.WriteLine ("Press <Enter> to Exit ...");
      // Instruct the runtime to Garbage Collect
      GC.Collect ();
      System.Console.ReadLine ();
    }
  }
}

Distributed BookKeeper

In the code example of BookKeeper.cs above, since we're using the TCP channel, we need to tell the compiler that we need definitions in the System.Runtime.Remoting and System.Runtime.Remoting.Channels.Tcp namespaces as shown on Lines 14 and 15. Also note on Line 43, that the BookKeeper class derives from MarshalByRefObject so that it can have a distributed identity. The default is marshaling-by-value, which means that a copy of the remote object is created on the client side. Subclassing from the MarshalByRefObject class gives the object a distributed identity, allowing the object to be referenced across application domains, or even across process and machine boundaries. Even though a marshal-by-reference object requires a proxy to be setup on the client side and a stub to be setup on the server side, the infrastructure handles this automatically, without us having to do any other extra work.

Any external client can invoke all the public methods of the BookKeeper class because the Main() method uses the TcpChannel class. The Main() method on Line 294 instantiates a TcpChannel, passing in a port number from which the server will listen for incoming requests.

Once we've created a Channel object, we register the Channel to the ChannelServices, which supports channel registration and object resolution on Line 295. The RegisterWellKnownServiceType() method of the RemotingConfiguration class allows you to register you object with the RemotingConfiguration so that it can be activated as shown on Line 297. When you invoke this method, you need to provide the class name, URI, and an object activation mode. The URI is important as the client will use it to refer specifically for this registered object.

AccountKey.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
//////////////////////////////////////////////////////
/// The following example is a helper class for the
/// BookKeeper example
///
/// author: Gopalan Suresh Raj
/// Copyright (c), 2001. All Rights Reserved.
/// URL: https://gsraj.tripod.com/
/// email: gopalan@gmx.net
///
//////////////////////////////////////////////////////
using System;

namespace BookKeeper {

  /// <summary>
  /// This enum type maps to an inderlying byte type
  /// </summary>

  /// Make this type Serializable
  [Serializable]
  public enum AccountType : int {
    CheckingAccount = 1,
    SavingsAccount  = 2
  }

  /// <summary>
  /// Summary description for AccountKey.
  /// </summary>

  /// Make this type Serializable
  [Serializable]
  public class AccountKey {

    /// <summary>
    /// The Account Number
    /// </summary>
    int accountKey_;

    /// <summary>
    /// The Type of Account - Checking or Savings
    /// </summary>
    AccountType accountType_;

    /// <summary>
    /// The Public Default No-argument constructor
    /// </summary>
    public AccountKey() {
      accountKey_ = 0;
      accountType_ = AccountType.CheckingAccount;
    }

    /// <summary>
    /// Accessors for the account key
    /// </summary>
    public int Key {

      get {
        return this.accountKey_;
      }

      set {
        this.accountKey_ = value;
      }
    }

    /// <summary>
    /// Accessors for the account type
    /// </summary>
    public AccountType Type {

      get {
        return this.accountType_;
      }

      set {
        this.accountType_ = value;
      }
    }
  }
}

 

5. Modify the generated AssemblyInfo.cs to add the right assembly information

You provide the compiler with your assembly information in an assembly file called AssemblyInfo.cs. The assembly information file is compiled with the  rest of the project's source files. The information is in the form of assembly attributes - directives to the compiler on the information to embed in the assembly.

AssemblyInfo.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
using System.Reflection;
using System.Runtime.CompilerServices;

//
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
//
[assembly: AssemblyTitle("BookKeeper for Bank")]
[assembly: AssemblyDescription("Manages to keep track of Account Numbers for the AccountManager")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("eCommWare Corporation")]
[assembly: AssemblyProduct("COM+ Bank Server")]
[assembly: AssemblyCopyright("(c) 2001, Gopalan Suresh Raj. All Rights Reserved.")]
[assembly: AssemblyTrademark("Web Cornucopia")]
[assembly: AssemblyCulture("")]

//
// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("1.0.0.0")]

//
// In order to sign your assembly you must specify a key to use. Refer to the
// Microsoft .NET Framework documentation for more information on assembly signing.
//
// Use the attributes below to control which key is used for signing.
//
// Notes:
//   (*) If no key is specified, the assembly is not signed.
//   (*) KeyName refers to a key that has been installed in the Crypto Service
//       Provider (CSP) on your machine. KeyFile refers to a file which contains
//       a key.
//   (*) If the KeyFile and the KeyName values are both specified, the
//       following processing occurs:
//       (1) If the KeyName can be found in the CSP, that key is used.
//       (2) If the KeyName does not exist and the KeyFile does exist, the key
//           in the KeyFile is installed into the CSP and used.
//   (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility.
//       When specifying the KeyFile, the location of the KeyFile should be
//       relative to the project output directory which is
//       %Project Directory%\obj\<configuration>. For example, if your KeyFile is
//       located in the project directory, you would specify the AssemblyKeyFile
//       attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")]
//   (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
//       documentation for more information on this.
//
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("BookKeeper.key")]
[assembly: AssemblyKeyName("")]

In particular, pay attention to the fact that we specify a version number for this library using the AssemblyVersion attribute and also specify the assembly key file using the AssemblyKeyFile attribute.

6. Build the Project Files

Build the files that make up the project.

------ Rebuild All started: Project: BookKeeper, Configuration: Debug .NET ------

Preparing resources...
Updating references...
Performing main compilation...

Build complete -- 0 errors, 0 warnings
Building satellite assemblies...



---------------------- Done ----------------------

Rebuild All: 1 succeeded, 0 failed, 0 skipped
 

7. Deploy the component as a Shared Assembly

After you've built the assembly, you can use the .NET Global Assembly Cache (GAC) utility to register this assembly into the GAC as shown below.

Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>gacutil /i BookKeeper.exe

Microsoft (R) .NET Global Assembly Cache Utility. Version 1.0.2914.16
Copyright (C) Microsoft Corp. 1998-2001. All rights reserved.

Assembly successfully added to the cache

C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>

Successful registration against the cache turns this component into a shared assembly. A version of this component is copied into the GAC so that even if you delete this file locally, you will still be able to run your client program.

8. Execute the Server

Once you've built the Server, start the server program, which will wait endlessly until you hit the Enter Key. The server is now ready to service client requests.

Now you need to build a client application that can access this Distributed Component.

Channels and .NET Remoting
Building a Distributed Component Server using C# and .NET
Building a Distributed Component Client using C# and .NET

 

Download the entire source code as a zip file.

 

click here to go to
My Advanced C#/.NET Tutorial Page...

About the Author...
Gopalan Suresh Raj is a Software Architect, Developer and an active Author. He has co-authored a number of books including "Professional JMS", "Enterprise Java Computing-Applications and Architecture" and "The Awesome Power of JavaBeans". His expertise spans enterprise component architectures and distributed object computing. Visit him at his Web Cornucopia© site (https://gsraj.tripod.com/) or mail him at gopalan@gmx.net.

 


Go to the Component Engineering Cornucopia page

This site was developed and is maintained by Gopalan Suresh Raj

This page has been visited times since January 1, 2002.

Last Updated : Jan 01, '02

If you have any questions, comments, or problems regarding this site, please write to me I would love to hear from you.


Copyright (c) 1997-2002, Gopalan Suresh Raj - All rights reserved. Terms of use.

All products and companies mentioned at this site are trademarks of their respective owners.