asp tutorials, asp.net tutorials, sample code, and Microsoft news from 15Seconds
Data Access  |   Troubleshooting  |   Security  |   Performance  |   ADSI  |   Upload  |   Email  |   Control Building  |   Component Building  |   Forms  |   XML  |   Web Services  |   ASP.NET  |   .NET Features  |   .NET 2.0  |   App Development  |   App Architecture  |   IIS  |   Wireless
 
Pioneering Active Server
 Power Search





Subscribe Now!
Free Newsletter
Reference
News
Articles
Archive
Writers
Code Samples
Components
Tools
FAQ
Feedback
Books
Links
DL Archives
Community
Messageboard
List Servers
Mailing List
WebHosts
Consultants
Tech Jobs
15 Seconds
Home
Site Map
Press
Legal
Privacy Policy
internet.commerce














internet.com
IT
Developer
Internet News
Small Business
Personal Technology

Search internet.com
Advertise
Corporate Info
Newsletters
Tech Jobs
E-mail Offers

HardwareCentral
Compare products, prices, and stores at Hardware Central!

-->
An Introduction to LINQ - Part 2
By Thiru Thangarathinam
Rating: 3.8 out of 5
Rate this article


  • email this article to a colleague
  • suggest an article



  • download source code
  • In Part 1 of this series on LINQ, you learned all the basics of LINQ including type inference, Lamda expressions, extension methods, and anonymous types. You also discussed the use of standard query operators for querying in-memory objects, and collections. With that background, this installment will now focus on the use of LINQ in querying relational data. Specifically you will learn DLINQ (also known as LINQ to SQL) framework and its role in querying relational data. Along the way, you will also understand the utilities shipped with LINQ that make this relational querying a breezy experience. Finally, you will also learn how to submit changes back to the database after making changes to the in-memory data contained in objects.

    Introduction to LINQ

    Microsoft's stated goal for DLINQ sounds quite simple and straightforward, which is representing relational data as objects and providing a translation mechanism to convert from relational data to objects and vice versa. However this is a very ambitious goal when considering the fact that the previous attempts at creating an object relational mapping framework never really took off and did not result in production-worthy code. Now with DLINQ, it is about to change as this new framework provides excellent relational to object mapping capabilities in addition to building on top of the LINQ's strong query capabilities.

    DLINQ provides this relational to object mapping solution by giving you the object representation of a strongly typed database. This means you could use treat the relational tabular representation of data in the database as objects that are not only strongly typed but also checked at compile time in your .NET code.

    To run the code samples supplied with this article, you need Visual Studio 2005 Professional RTM as well as LINQ May 2006 Community Technology Preview.

    Simple DLINQ Example

    At the heart of DLINQ is the DataContext class. The DataContext is the main object through which you retrieve objects from the database and submit changes back. You use it in the same way that you would use an ADO.NET Connection object. In fact, the DataContext is initialized with a connection or connection string you supply. The purpose of the DataContext is to translate your requests for relational data into SQL queries and then assemble objects out of the results.

    Before you can access the database, you need to create the DLINQ layer that is required for communicating with the database. You can create this using any of the three different approaches.

    1. Manually hard code the data classes by deriving from the DataContext class
    2. Use the DLINQ Visual Designer to create the classes
    3. Use the command line utility called sqlmetal.exe to create the classes

    For the purposes of this article, I will make use of the sqlmetal.exe utility to create the DLINQ layer for communicating with the AdventureWorks database. The next section discusses the steps to be followed for creating this object model.

    Creating the Object Model

    To create the object model that maps to the relational data, there are two ways you can do this.

    1. To the class, add the mapping attributes that indicate how and where they should be stored in the database. This is referred to as attribute based mapping.
    2. It is also possible for you to specify the mapping using an external XML file. The external XML file abstracts the underlying database schema from the code thereby enabling you to make changes to the runtime schema mapping on the fly. This is a very powerful feature that can go a long way in increasing the extensibility of the solution.

    I will show examples of both of these. Based on your requirements, you can choose any of these approaches with no impact to performance.

    First, let us focus on how to use the sqlmetal utility. If you install the May 2006 LINQ CTP edition, you can find the sqlmetal utility in the "< Drive_Name>\Program Files\LINQ Preview\Bin" folder.

    sqlmetal /server:localhost /database:AdventureWorks /user:username /password:password /code:AdventureWorks.cs

    By default, the auto-generated AdventureWorks class follows the attribute-based mapping approach meaning that all the classes are decorated with the required relational data mapping.

    As an example, if you look at the declaration of the ProductSubcategory class, you will see the class being decorated with the name of the table, which is ProductSubCategory, in this case.

    [Table(Name="[Production].[ProductSubcategory]")]
    public partial class ProductSubcategory :
      System.Data.DLINQ.INotifyPropertyChanging,
      System.ComponentModel.INotifyPropertyChanged
    {
      private int _ProductSubcategoryID;
      private int _ProductCategoryID;
      private string _Name;
      -----
      -----
    }

    To use the external XML file for mapping the table with the class, you need to supply the /map switch along with the name of the XML file as shown below:

    sqlmetal /server:localhost /database:AdventureWorks /user:username /password:password /map:AdventureWorksMapping.xml /code:AdventureWorks.cs

    As an example, here is how the mapping for the ProductSubcategory table looks.

    <?xml version="1.0" encoding="utf-8"?>
    <Database xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="AdventureWorks">
      <Table Name="[Production].[ProductSubcategory]">
        <Type Name=".ProductSubcategory">
          <Column Name="ProductSubcategoryID" Member="ProductSubcategoryID"
            Storage="_ProductSubcategoryID" DbType="Int NOT NULL IDENTITY"
            IsIdentity="True" IsAutoGen="True" />
          <Column Name="ProductCategoryID" Member="ProductCategoryID"
            Storage="_ProductCategoryID" DbType="Int NOT NULL" />
          <Column Name="Name" Member="Name" Storage="_Name"
            DbType="NVarChar(50) NOT NULL" />
          <Column Name="rowguid" Member="Rowguid" Storage="_Rowguid"
            DbType="UniqueIdentifier NOT NULL" />
          <Column Name="ModifiedDate" Member="ModifiedDate" Storage="_ModifiedDate"
            DbType="DateTime NOT NULL" />
          <Association Name="FK_Product_ProductSubcategory_ProductSubcategoryID"
            Member="Product" Storage="_Product" ThisKey="ProductSubcategoryID"
            OtherTable="Product" OtherKey="ProductSubcategoryID" />
          <Association
            Name="FK_ProductSubcategory_ProductCategory_ProductCategoryID"
            Member="ProductCategory" Storage="_ProductCategory"
            ThisKey="ProductCategoryID" OtherTable="ProductCategory"
            OtherKey="ProductCategoryID" IsParent="True" />
        </Type>
      </Table>
    ---
    ---
    ---
    </Database>

    Since the mappings are established in the external file, if you examine the generated code file (AdventureWorks.cs) you will find that the references to the table names and column names are stripped off from the classes and properties. This approach is a lot cleaner as it allows you to keep the code separate from the mappings information.

    Once data mappings and relationships are established, you can then easily write DLINQ code to perform queries and updates against a database.

    Executing Queries

    This section will focus on the code required to query the database using the AdventureWorks class created in the previous section. For example, the below code uses the AdventureWorks class to retrieve all categories and displays them in a GridView.

    To start with, create a new Visual C# LINQ ASP.NET Web Site Template project using Visual Studio 2005. When you use this template to create the project, Visual Studio automatically adds an entry to the Web.config file to use the C# 3.0 compiler.

    Once the Web site is created, add a new ASP.NET page named SimpleDLinq.aspx and modify its code to look as follows:

    <%@ Page Language="C#" %>
    <%@ Import Namespace="System.Data.DLINQ" %>
    <%@ Import Namespace="System.Data.SqlClient" %>
    <%@ Import Namespace="System.Query" %>
    <%@ Import Namespace="System.Web.Configuration" %>

    <script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
      string connectionString = WebConfigurationManager.ConnectionStrings
        ["AdventureWorks"].ConnectionString;
      SqlConnection connection = new SqlConnection(connectionString);
      AdventureWorks db = new AdventureWorks(connection);
      gridCategories.DataSource = from category in
                        db.Production.ProductSubcategory
                        orderby category.ProductSubcategoryID
                        select category;
      gridCategories.DataBind();
    }
    </script>
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" runat="server">
      <title>Simple Data retrieval using DLINQ</title>
    </head>
    <body>
      <form id="form1" runat="server">
        <div>
          <asp:GridView runat="server" ID="gridCategories"
            AutoGenerateColumns="false">
            <Columns>
              <asp:BoundField DataField="ProductSubcategoryID"
                HeaderText="Category ID"/>
              <asp:BoundField DataField="Name" HeaderText="Name"/>
              <asp:BoundField DataField="ModifiedDate"
                HeaderText="ModifiedDate"/>
            </Columns>
          </asp:GridView>
        </div>
      </form>
    </body>
    </html>

    To start with, all the namespaces including the core LINQ namespaces such as System.Query and System.Data.DLINQ are imported.

    In the Page_Load event, you retrieve the connection string from the Web.config file and then create an instance of the SqlConnection object passing in the connection string to the constructor of the SqlConnection object.

    SqlConnection connection = new SqlConnection(connectionString);

    Once the SqlConnection object is created, you supply that as an argument to the constructor of the AdventureWorks class.

    AdventureWorks db = new AdventureWorks(connection);

    Note that if you are using the mapping file, you need to pass that mapping information (in the form of XmlMappingSource object) to the constructor of the AdventureWorks in addition to the SqlConnection object.

    With this plumbing in place, you can now query any table in the AdventureWorks database using the simple dot notation. As an example, you can retrieval all the categories from the ProdutSubcategory table just by using the format db.Production.ProductSubcategory.

    gridCategories.DataSource = from category in
                      db.Production.ProductSubcategory
                      orderby category.ProductSubcategoryID
                      select category;

    As discussed in Part 1 of this article series, the from…orderby…select query retrieves all the categories and orders them by the category id column. Here is the output produced by the page when requested from the browser.

    Selecting Specific Columns from the Resultset

    In the previous example, the DLINQ query returned all the columns from the categories table. However there are times where you might want to restrict the number of columns returned from the query. To do this, you can use the anonymous type feature discussed in the Part 1 of this series. As an example, if you want to return just the category id and name columns, you modify the query to look as follows:

    gridCategories.DataSource = from category in
                      db.Production.ProductSubcategory
                      orderby category.ProductSubcategoryID
                      select new
                      {
                        CategoryID = category.ProductSubcategoryID,
                        Name = category.Name
                      };

    Once you make the above changes to the query, you also need to make the corresponding changes in the GridView. Specifically you need to modify the DataField attribute of the <asp:BoundField> elements to reflect the property names of the anonymous type.

      <asp:GridView runat="server" ID="gridCategories"
        AutoGenerateColumns="false">
        <Columns>
          <asp:BoundField DataField="CategoryID"
            HeaderText="Category ID"/>
          <asp:BoundField DataField="Name" HeaderText="Name"/>
        </Columns>
      </asp:GridView>

    In the above declaration, the category id field is modified to reflect the property name, which is CategoryID in this case.

    Executing a Stored Procedure

    So far, you have seen how to query the tables directly using DLINQ layer. If you have a well layered design, you would typically have most of the logic contained within the stored procedures for reasons of abstraction and maintainability. If you want to reuse those stored procedures (as you migrate your existing applications to use DLINQ), you can still do that since DLINQ provides you with a way to invoke those stored procedures as well.

    This section will show you a simple example on invoking a stored procedure using DLINQ. First, let us create a stored procedure named GetAllProductsByCategoryID that returns all the products for a specific category in the AdventureWorks database.

    CREATE PROCEDURE [dbo].[GetAllProductsByCategoryID]
      @ProductSubcategoryID INT
    AS
    BEGIN
      SELECT ProductID, ProductSubcategoryID, Name, ProductNumber, ListPrice
      FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID
    END

    Now that the stored procedure is created, the next step is to create the required stored procedure to object mappings. Using the Sqlmetal utility, you can easily accomplish this by just adding the switch "/sprocs".

    sqlmetal /server:localhost /database:AdventureWorks /user:username /password:password /sprocs /code:AdventureWorks.cs

    With the above code executed, you can now execute all the stored procedures in the AdventureWorks database in the same way you queried the AdventureWorks tables in the previous section. As an example, the following ASP.NET page shows how to do execute the GetAllProductsByCategoryID stored procedure.

    <%@ Page Language="C#" %>
    <%@ Import Namespace="System.Data.DLINQ" %>
    <%@ Import Namespace="System.Data.SqlClient" %>
    <%@ Import Namespace="System.Query" %>
    <%@ Import Namespace="System.Web.Configuration" %>
    <script runat="server">
      void Page_Load(object sender, EventArgs e)
      {
        string connectionString = WebConfigurationManager.ConnectionStrings
          ["AdventureWorks"].ConnectionString;
        SqlConnection connection = new SqlConnection(connectionString);
        AdventureWorks db = new AdventureWorks(connection);
        gridProducts.DataSource = from prod in db.GetAllProductsByCategoryID(1)
                        orderby prod.ProductID
                        select prod;
        gridProducts.DataBind();
      }
    </script>
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" runat="server">
      <title>Executing a Stored Procedure using DLINQ</title>
    </head>
    <body>
      <form id="form1" runat="server">
        <div>
          <asp:GridView runat="server" ID="gridProducts"
            AutoGenerateColumns="false">
            <Columns>
              <asp:BoundField DataField="ProductSubcategoryID"
                HeaderText="Category ID" />
              <asp:BoundField DataField="Name" HeaderText="Name" />
              <asp:BoundField DataField="ListPrice" HeaderText="List Price" />
            </Columns>
          </asp:GridView>
        </div>
      </form>
    </body>
    </html>

    The key change in the above code is the use of stored procedure in the from..select query.

    gridProducts.DataSource = from prod in db.GetAllProductsByCategoryID(1)
                    orderby prod.ProductID
                    select prod;

    For reasons of brevity, the value for the category id parameter is hard coded in the above code snippet.

    If you navigate to the page using the browser, you should see an output that is somewhat similar to the below screenshot.

    Selecting Hierarchical Data using LINQ

    So far you have seen DLINQ queries that were focused on retreival of simple flat data. However you can use DLINQ queries to retrieve complex hierachical data as well. For example, you can use a single query to retrieve all the categories as well as all the products for each of the categories.

    In this case, the list of products for each category will be embedded as a collection within the category object. Here is the ASP.NET page code required to produce the hierarchical display.

    <%@ Page Language="C#" %>
    <%@ Import Namespace="System.Data.DLINQ" %>
    <%@ Import Namespace="System.Data.SqlClient" %>
    <%@ Import Namespace="System.Query" %>
    <%@ Import Namespace="System.Web.Configuration" %>

    <script runat="server">
      void Page_Load(object sender, EventArgs e)
      {
        string connectionString = WebConfigurationManager.ConnectionStrings
          ["AdventureWorks"].ConnectionString;
        SqlConnection connection = new SqlConnection(connectionString);
        AdventureWorks db = new AdventureWorks(connection);
        gridCategories.DataSource = from category in
                          db.Production.ProductSubcategory
                          orderby category.ProductSubcategoryID
                          select new
                          {
                            CategoryID = category.ProductSubcategoryID,
                            Name = category.Name,
                            Products = category.Product
                          };
        gridCategories.DataBind();
      }
    </script>
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
      <title>Selecting Hierarchical Data using DLINQ</title>
    </head>
    <body>
      <form id="form1" runat="server">
        <div>
          <asp:GridView ID="gridCategories" runat="server"
            AutoGenerateColumns="false" HeaderStyle-BackColor="blue"
            HeaderStyle-ForeColor="White">
            <Columns>
              <asp:TemplateField HeaderText="Category">
                <ItemStyle VerticalAlign="Top" Width="20%"/>
                <ItemTemplate>
                  <br/><b><%# Eval("CategoryID") %></b>
                  <br/><br/><%# Eval("Name") %><br/>
                </ItemTemplate>
              </asp:TemplateField>
              <asp:TemplateField HeaderText="Products">
                <ItemStyle VerticalAlign="Top"/>
                <ItemTemplate>
                  <asp:GridView AutoGenerateColumns="false" ID="gridProducts"
                    DataSource='<%# Eval("Products") %>' runat="server">
                    <Columns>
                      <asp:BoundField DataField="Name"
                        HeaderText="Product Name"/>
                      <asp:BoundField DataField="ProductNumber"
                        HeaderText="Product Number"/>
                      <asp:BoundField DataField="ListPrice"
                        HeaderText="List Price"
                        DataFormatString="{0:C}"/>
                    </Columns>
                  </asp:GridView>
                </ItemTemplate>
              </asp:TemplateField>
            </Columns>
          </asp:GridView>
        </div>
      </form>
    </body>
    </html>

    As you can see from the code, you have a parent GridView (that displays categories information) and a child GridView (that displays products information for each category) , which is embedded in the parent GridView. In the Page_Load event, when you set the parent GridView's DataSource property, you also include a property called Products that has all the products for the current category id. You get these list of products for the current category id using the Product property exposed through the category object. By examining the foreign key relationships between the ProductSubcategory and Product tables, the Sqlmetal utility automatically generated the Product property within the ProductSubcategory class.

    gridCategories.DataSource = from category in
                    db.Production.ProductSubcategory
                    orderby category.ProductSubcategoryID
                    select new
                    {
                      CategoryID = category.ProductSubcategoryID,
                      Name = category.Name,
                      Products = category.Product
                    };

    Once you have retrieved the products for the current category id, you can then assign that to the child GridView by setting the DataSource property to <% # Eval ("Products") %>. As each row in the category result set is populated in the parent GridView, the child GridView is also automatically populated with the corresponding products.

    The output produced by the page is as follows:

    Inserting a New Record

    In addition to exposing powerful query features, DLINQ also provides for a way to make changes to the in-memory data and submit those changes back to the database. To accomplish this, all you do is to modify the in-memory object and then call the SubmitChanges() method on the object that derives from the DataContext class. In our case, it is the AdventureWorks class that derives from the DataContext class. DLINQ maintains enough tracking information to enable it to submit a change and use optimistic concurrency to check for concurrency issues. It also provides various facilities for changing optimistic concurrency behaviors. The below code shows an example where you add a new category to the ProductCategory table through the DLINQ layer.

    <%@ Page Language="C#" %>
    <%@ Import Namespace="System.Data.DLINQ" %>
    <%@ Import Namespace="System.Data.SqlClient" %>
    <%@ Import Namespace="System.Query" %>
    <%@ Import Namespace="System.Web.Configuration" %>

    <script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
      string connectionString = WebConfigurationManager.ConnectionStrings
        ["AdventureWorks"].ConnectionString;
      SqlConnection connection = new SqlConnection(connectionString);
      AdventureWorks db = new AdventureWorks(connection);
      ProductCategory cate = new ProductCategory();
      cate.ProductCategoryID = 5;
      cate.Name = "New Category";
      cate.Rowguid = System.Guid.NewGuid();
      cate.ModifiedDate = DateTime.Now;
      db.Production.ProductCategory.Add(cate);
      db.SubmitChanges();
      lblResult.Text = "Category added successfully";
    }
    </script>
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" runat="server">
      <title>Inserting a new row</title>
    </head>
    <body>
      <form id="form1" runat="server">
        <div>
          <asp:Label ID="lblResult" runat="server" />
        </div>
      </form>
    </body>
    </html>

    The above code uses the following steps to add a new category to the category table.

    1. Create a new ProductCategory object that represents a category instance
    2. Populate the properties of the ProductCategory object with appropriate values
    3. Add the ProductCategory object to the ProductCategory collection using the Add() method
    4. Finally, commit the changes back to the database using the SubmitChanges() method

    Note that till you invoke the SubmitChanges() method, the changes are not committed to the actual data in the database. When you do call SubmitChanges(), the DataContext will attempt to translate all your changes into equivalent SQL commands, inserting, updating or deleting rows in corresponding tables.

    If you don't like the default submission behavior, you can also customize it by overriding the auto-generated code with custom commands. Similar to inserting a new record, you can also update an in-memory row and save the changes back to the database by calling the SubmitChanges() method.

    Conclusion

    In this part, we looked at the basics of performing data access using DLINQ. Then we looked at how to perform queries ranging all the way from simple queries, to executing stored procedures, and all the way to retrieving hierarchical data. Finally you also understood the steps involved in committing changes back to the database through the DLINQ layer. Part-3 of this series will discuss the XML extensions of LINQ (known as XLINQ) in depth by providing examples.

  • Rate This Article
    Not HelpfulMost Helpful
    1 2 3 4 5
    Other Articles
    Jul 21, 2005 - N-Tier Web Applications using ASP.NET 2.0 and SQL Server 2005 - Part 1
    While the .NET Framework made building ASP.NET applications easier then it had ever been in the past, .NET 2.0 builds on that foundation in order to take things to the next level. This article shows you to how to construct an N-Tier ASP.NET 2.0 Web application by leveraging the new features of ASP.NET 2.0 and SQL Server 2005.
    [Read This Article]  [Top]
    Apr 28, 2005 - New Files and Folders in ASP.NET 2.0
    With the release of ASP.NET 2.0, Microsoft has greatly increased the power of ASP.NET by introducing a suite of new features and functionalities. As part of this release, ASP.NET 2.0 also comes with a host of new special files and folders that are meant to be used to implement a specific functionality. This article examines these new files and folders in detail and provides examples that demonstrate how to utilize them to create ASP.NET 2.0 applications.
    [Read This Article]  [Top]
    Mar 10, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 2, Cont'd
    Alex Homer continues his detailed look at the major changes to the DataSet class. In this part, he looks at two features that allow developers to work with data in a more structured and efficient way when using the DataSet with a SQL Server 2005 database server.
    [Read This Article]  [Top]
    Mar 9, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 2
    Alex Homer continues his detailed look at the major changes to the DataSet class. In this part, he looks at two features that allow developers to work with data in a more structured and efficient way when using the DataSet with a SQL Server 2005 database server.
    [Read This Article]  [Top]
    Mar 3, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 1, Cont'd
    In this article, Alex Homer looks at the changes between the version 1.x and version 2.0 DataSet and their associated classes, showing you how you can take advantage of the new features to improve your applications' capabilities and performance.
    [Read This Article]  [Top]
    Mar 2, 2005 - The DataSet Grows Up in ADO.NET 2.0 - Part 1
    In this article, Alex Homer looks at the changes between the version 1.x and version 2.0 DataSet and their associated classes, showing you how you can take advantage of the new features to improve your applications' capabilities and performance.
    [Read This Article]  [Top]
    Feb 16, 2005 - Writing a Custom Membership Provider for the Login Control in ASP.NET 2.0
    In ASP.NET 2.0 and Visual Studio 2005, you can quickly program custom authentication pages with the provided Membership Login controls. In this article, Dina Fleet Berry examines the steps involved in using the Login control with a custom SQL Server membership database.
    [Read This Article]  [Top]
    Dec 29, 2004 - ClickOnce Deployment in .NET Framework 2.0
    In this article, Thiru Thangarathinam examines .NET 2.0's new ClickOnce deployment technology that is designed to ease deployment of Windows forms applications. This new technology not only provides an easy application installation mechanism, it also eases deployment of upgrades to existing applications.
    [Read This Article]  [Top]
    Dec 15, 2004 - A Sneak Peek at ASP.NET 2.0's Administrative Tools
    With ASP.NET 2.0, Microsoft has made great strides in increasing developer productivity and has made implementing previously complex solutions relatively easy. Where this version of ASP.NET really shines, however, is in its new administrative tools that allow developers to spend less time managing the configuration of the servers and software and more time developing great code.
    [Read This Article]  [Top]
    Nov 17, 2004 - The ASP.NET 2.0 TreeView Control
    Thiru Thangarathinam introduces ASP.NET 2.0's new TreeView control which provides a seamless way to consume and display information from hierarchical data sources. The article discusses this new control in depth and explains how to use this feature rich control in your ASP.NET applications.
    [Read This Article]  [Top]
    Mailing List
    Want to receive email when the next article is published? Just Click Here to sign up.

    Support the Active Server Industry