CoverStory

LANGUAGES: C#

ASP.NET VERSIONS: 3.5

 

LINQed & Layered

Implement CRUD Operations with LINQ in Three-tier ASP.NET Applications

 

By Bilal Haidar

 

With the rise of LINQ (language-integrated query) technology, developers are having a lot of fun playing around with the new standard way of accessing data that can be stored either in the database, XML files, or even plain objects. However, when it comes to developing three-tier Web applications, the tricks and workarounds start to show up to integrate LINQ into such applications. Many steps must be taken when developing three-tier Web applications; by discussing the CRUD (create, read, update, and delete) operations and how they must be implemented by LINQ, this article will show you how you can develop such applications depending solely on LINQ.

 

Developing three-tier ASP.NET Web applications is usually composed of the User Interface (UI) layer, Business Logic Layer (BLL), and Data Access Layer (DAL). The UI layer usually contains the ASP.NET Web Forms and user controls; BLL contains all the validation code and business rules that govern the business; and, finally, DAL contains all the nasty detailed code of accessing the data store and applying the different functionalities.

 

In the past, developing a DAL was a hectic job for the developer because of the redundant code that must be written mainly to access the data store and perform operations. The same code would have been written several times in the same application with minor changes from one class to another. Several third-party software products were written that allow the developer to create templates to generate DAL classes to preserve the wasted time spent, but that came at the expense of learning the language used by that software, and this itself was an obstacle.

 

Then along came LINQ technology, which helps developers save time by generating the DAL in a matter of a few clicks. All you have to do is create a new DataContext class and drag all the tables you have in the database into it, or create new ones. Relations will be automatically created once tables are dragged to the surface of the DataContext. Strongly typed classes will be generated for each table in the dragged tables, and properties to reflect relations between the objects will be added.

 

Having explained briefly how LINQ will affect an application, it is clear that the focus is now moved to the BLL in three-tier architecture Web applications to make it work gently with LINQ DAL; this is what we ll do in this article, by showing you how to create a three-tier ASP.NET application to retrieve, update, delete, and add data related to customers.

 

References to LINQ

This article assumes a fair knowledge of using LINQ and it is not intended as a primer explaining LINQ. If you feel you need more information on LINQ, I recommend you check the following link to a compiled version of Scott Guthrie s articles on LINQ to SQL (in PDF book format): http://it-box.blogturk.net/2007/10/19/linq-to-sql-tutorial-series-by-scott-guthrie-pdf-book-format/.

 

Implement CRUD Operations

To start, let s create a new ASP.NET Web site using Visual Studio 2008. This ensures that LINQ is enabled by default inside the Web application. Once the Web site is created, add two sub-folders to the App_Code folder, as shown in Figure 1.

 


Figure 1: Add two sub-folders to the App_Code folder.

 

Creating Data Access Layer Classes

Assuming there is a database with a single table named Customers, we ll create a new LINQ to SQL class, then drag into it the single table present in the database. That s it; you re done with the DAL classes. At this point, all the required classes have been generated.

 

You might have different options here. One option is to have a single DataContext class to hold all the tables in your database; another option is to have a DataContext class for each related set of tables. Option one was implemented above; however, if you plan to go by the other option, make sure to completely separate the tables in each DataContext class this will make the DataContext function properly when it wants to track the objects and relations between them.

 

Creating Business Logic Layer Classes

Now that the DataContext class is created, it s time to create the BLL. We ll add a new class named CustomerManager. This class will hold all the methods required for managing a Customer, from retrieving all Customers to retrieving a single Customer, inserting a new Customer, and updating and deleting an old Customer record. This class will be based solely on the DataContext class that was generated in the section above. The first method to be discussed is GetCustomers (see Figure 2).

 

[DataObjectMethodAttribute(DataObjectMethodType.Select, false)]

 public static List<Customer> GetCustomers(int startRowIndex,

   int maximumRows)

 {

     List<Customer> customers = null;

     string key = "Customers_Customers_" + startRowIndex.ToString() +

        "_" + maximumRows.ToString();

     // If data already present in Cache, get them from there

     // else load the data from the database

     if (HttpContext.Current.Cache[key] != null)

     {

        customers = (List<Customer>)HttpContext.Current.Cache[key];

     }

     else

     {

        // Get all customers from the database

        // and by having custom paging using Skip/Take

        // Query Expressions in LINQ

        customers = (

          from

              c in BizObject.Context.Customers

          orderby

              c.CustomerID descending

          select

              c

        ).Skip(startRowIndex).Take(maximumRows).ToList();

        // Add the records retrieved to cache

        if (customers != null && customers.Count > 0)

          HttpContext.Current.Cache.Insert(key, customers, null,

             DateTime.Now.AddDays(1), TimeSpan.Zero);

     }

     return customers;

 }

Figure 2: The GetCustomers method.

 

The GetCustomers method has been designated with the DataObjectMethodAttribute with the Select type, so it will automatically be selected by the ObjectDataSource used later inside the UI layer. The method returns a list of Customer objects. A Customer object has been generated inside the DataContext class and includes all the columns present in the Customer data table.

 

As a good habit, it s worth making use of caching to improve the performance of three-tier architecture applications. Instead of accessing the database every time the customers data is to be retrieved, it s enough to retrieve the data for the first time and place it inside the Cache object. This way, the next time the UI layer asks for the same customers data, it ll be retrieved from the cache instead!

 

If the records were not in the cache, then the method executes a LINQ Query to select all customers from the data table, sort them by the CustomerID in a descending way, then apply custom paging using the Skip and Take methods added to LINQ. There is nothing special about this method except the LINQ Query.

 

The second method to be discussed is InsertCustomer (see Figure 3). The InsertCustomer method accepts as input an object of type Customer. The first step is creating a new instance of the DataContext class. If there is a need to compare this DataContext instance to the old way of developing three-tier architecture applications, it corresponds to an instance of the DAL object.

 

public static int InsertCustomer(Customer customer)

{

   // Validate the object

   if (customer == null)

       throw new ArgumentException("Customer object is invalid");

   // Create a new instance of the DataContext

   CustomerDataContext db = BizObject.Context;

   // Create a new customer and add it to the DataContext

   // to be able to have the DataContext track changes on the object

    Customer localCustomer = new Customer {

           FirstName = customer.FirstName,

           LastName = customer.LastName,

           Email = customer.Email

   };

   // Add the item to the DataContext

   db.Customers.InsertOnSubmit(localCustomer);

   // Save the item to the database

   db.SubmitChanges();

   // Clear the cache for all cache keys that

   // start with customers_customer. This will include

   // the keys for all customers retrieved and

   // single customers too.

   BizObject.PurgeCacheItems("customers_customer");

   // Return ID of the new customer created

   return localCustomer.CustomerID;

}

Figure 3: The InsertCustomer method.

 

Notice that in the body of the method, a new instance of a Customer object is created. All the fields are mapped from the input parameter to the local instance.

 

After the local Customer instance is filled, it is added to the Customers table inside the DataContext instance object. Then a call to SubmitChanges against the DataContext instance object is done, which ensures the new customer information has been added to the database. At this stage, the DataContext internally will issue an INSERT SQL Statement to insert this new record into the database.

 

Creating a new instance of the Customer class might be misleading, and prompts a major question why wouldn t it be enough to directly add the input Customer instance into the Customers table of the DataContext class? The DataContext object, to be able to keep track of the Customer object s changes throughout the life of that object, the DataContext instance must be created before creating a new instance of the Customer class. This newly created object resides inside the context of the DataContext class. It needs to keep track of it, and that s why there is a need to create a new instance of the DataContext class first, then add the locally created Customer instance to the DataContext object. Once the object is added to the DataContext object, tracking that object starts, and, in this case, the DataContext object will notice that this is a new object and, upon hitting SubmitChanges, the new object will be inserted into the database.

 

The third method to be discussed is UpdateCustomer (see Figure 4). The UpdateCustomer method follows the same technique used by the InsertCustomer method. A new instance of the DataContext class is created, then a new instance of the Customer class is created. It might look more convincing as to why there is a need to create a local instance of the Customer class in this method when the following facts are highlighted:

  • When the UI layer first requested that Customer object, it either used the GetCustomers or GetCustomerByID method to retrieve that object. Each method created its own DataContext instance in that specific HttpRequest. When updating the same Customer object, the old DataContext object is now dead.
  • When it comes to updating an object that was created by a different DataContext object, the current DataContext object will not have any clue about that object, and it will not be able to notice any changes inside it, because it was not responsible for tracking it from the creation time; hence the need to create a new instance of the Customer class, set the Primary Key(s), set any property that has a role in the Concurrency Checking, then attach the new object to the new DataContext instance.
  • Once the object is attached, updating all the other properties in the object can be done safely. Once all properties to be updated have been configured, a call to the DataContext s SubmitChanges is done and the changes will be reflected on the database immediately.

 

public static void UpdateCustomer(Customer customer)

{

   // Validate the object

   if (customer == null)

       throw new ArgumentException("Customer object invalid");

   // Create a new instance of the DataContext

   CustomerDataContext db = BizObject.Context;

   // Create a new instance of the Customer object

   // Make sure to include Primary Key and the Keys

   // used by DataContext for concurrency checking

   Customer localCustomer = new Customer {

       CustomerID = customer.CustomerID,

       Email = customer.Email

   };

   // Attach the new object to the DataContext

   db.Customers.Attach(localCustomer);

   // Now update the object, after adding it to the

   // DataContext, changes will be tracked

   localCustomer.Age = customer.Age;

   // Check Concurrency Conflict

   try

   {

       // Now you can submit changes using the DataContxet

       db.SubmitChanges(ConflictMode.ContinueOnConflict);

   }

    catch (System.Data.Linq.ChangeConflictException ex)

   {

       foreach (ObjectChangeConflict occ in db.ChangeConflicts)

       {

           // All database values overwrite current values.

           occ.Resolve(RefreshMode.OverwriteCurrentValues);

       }

       db.SubmitChanges();

   }

   finally

   {

       // Dispose the DataContext object

       db.Dispose();

   }

   // Purge the Cache

   BizObject.PurgeCacheItems("customers_customer_" +

      customer.CustomerID.ToString());

   BizObject.PurgeCacheItems("customers_customers");

}

Figure 4: The UpdateCustomer method.

 

The key point here is to notice that the input parameter was created by another DataContext object, and, to make the current DataContext instance of this current HttpRequest aware of that object, it s a must to first attach the object to the new DataContext object, then update the required properties.

 

The fourth method to be discussed is DeleteCustomer (see Figure 5), which is similar to the UpdateCustomer method explained previously. The same concept applies here, with some changes.

 

public static void DeleteCustomer(Customer customer)

{

   // Validate the object

   if (customer == null)

       throw new ArgumentException("Customer object invalid");

   // Create a new instance of the DataContext

   CustomerDataContext db = BizObject.Context;

   // Get the Customer from the database

   Customer localCustomer = (

       from

           c in db.Customers

       where

           c.CustomerID == customer.CustomerID

       select

           c

       ).FirstOrDefault();

   // Delete customer taken the CustomerID

   // of the customer retrieved, this way

   // changes will be tracked on

   db.Customers.DeleteOnSubmit(localCustomer);

   db.SubmitChanges();

   // Purge the Cache

   BizObject.PurgeCacheItems("customers_customer");

}

Figure 5: The DeleteCustomer method.

 

A new instance of the Customer class was retrieved from the database corresponding to the input parameter Customer object. The new instance could have been easily created from the input Customer object but this way, a check is done to make sure the record really exists in the database before deleting it!

 

Once the object is retrieved by the current DataContext instance, removing it from the Customers table using the Remove method would signal to the DataContext object that an object has to be deleted. This would not have been true if the input Customer object was directly removed from the Customers table; the reason is, again, the input Customer object was retrieved by another DataContext object, and there is no way for the current DataContext object to keep track of it but to create a local instance of the same record!

 

Creating User Interface Web Forms

Up till now, we ve detailed creating the BLL and the DAL based on LINQ. What s still missing is creating a UI layer to complete the three-tier architecture.

 

The UI layer we ll create is a very simple UI. The main goal is to show how the different layers in the application are interacting with each other in the presence of LINQ. That interaction has been shown between the BLL and DAL; now the interaction between the UI layer and BLL will be shown.

 

Create a simple ASPX page that shows a GridView listing all the customers in the database. The GridView is configured with an ObjectDataSource:

 

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"

   DataObjectTypeName="bhaidar.DAL.Customer"

   SelectMethod="GetCustomers"

   TypeName="bhaidar.BLL.CustomerManager" />

 

The DataObjectTypeName is bhaidar.DAL.Customer. This property could have been pointed to an object created in the BLL that represents a wrapper over the Customer classes auto-generated inside the DataContext class. However, for the sake of this article, the UI layer will be satisfied by the DAL Customer object.

 

Configuring the ObjectDataSource is the same as it used to be before the days of LINQ; nothing changes at all!

 

A FormView is also included to display a single customer, edit it, delete it, or create a new one. Figure 6 shows a snapshot of that Web Form when a customer record is selected in the GridView and details about it are shown in the FormView to be edited or deleted.

 


Figure 6: ASP.NET Web Form customer management.

 

Clicking the Edit link would cause the FormView to be changed to Edit Mode. If a new customer is to be added, clicking the New link would put the FormView in Insert Mode. And finally, if there s a need to delete an existing customer, clicking the Delete link will do so!

 

Conclusion

This article opens with a brief explanation of the new LINQ technology, three-tier ASP.NET applications, and references to important links on getting started with LINQ. A detailed explanation then illustrated how to create a new DAL based on LINQ, as well as how to create a BLL and integrate it with the LINQ DAL classes. Finally, we saw how to create a UI layer, with Web Forms and user controls that interact with the BLL.

 

Source code accompanying this article is available for download.

 

Bilal Haidar is a Microsoft MVP in ASP/ASP.NET (since 2004). He is an MCP, MCTS, MCPD, and MCT holder. He is a Telerik MVP and the Webmaster for the LebDev user group. Bilal is one of the top posters on the ASP.NET official forums, as well as a moderator. He is a senior software developer at CCC, a multinational construction company based in Athens, Greece. You can check his published articles on http://www.aspalliance.com and http://www.code-magazine.com. He runs his own blog at http://www.bhaidar.net. Contact him with any questions or for help at mailto:bhaidar@gmail.com.