Using Windows Azure Service Bus for communication across two ASP.NET MVC Applications

Posted by: Mahesh Sabnis , on 1/21/2015, in Category Microsoft Azure
Views: 24465
Abstract: Windows Azure Service Bus Queue provides the middleware for developing decoupled distributed applications. This article talks about communicating between two ASP.NET MVC applications using Azure Service bus.

Windows Azure provides a stable environment for deploying and managing enterprise applications. Typically enterprise applications are designed with multi-layer components. These applications often need to communicate with other applications and such communications is implemented and handled using Queue. Queue provides feature of sending and receiving messages. But only offline messaging across applications is not sufficient. A better way here is to make use of Publish-Subscribe model.

 

Windows Azure provides Service Bus is a multi-tenant cloud service and can be shared by multiple-users. Windows Azure Service Bus provides cloud based and message oriented middleware technologies for reliable message queuing and durable publish-subscribe messaging. Each user who wants to make use of Service Bus can create a namespace within which different methods of communication are provided as listed here:

o Queues - allows one-directional communication. The queue acts as a broker which stores messages until they are received.

o Topics - provides one-directional communication using subscription. Like queue, topics also act as a broker but messaging is managed based on matching specific criteria.

o Relays - provides bi-directional communication. This does not store any message, instead it just passes them to the destination application.

The Service Bus runs in the cloud, but applications which make use of service bus can run anywhere. The application must provide some name to the queue, topic, and relay so that it can be used by Service Bus for communication.

In the application explained in this article, we will use service bus and its objects to manage communication between applications. To make use of these objects we need to create Service Namespace on the Windows Azure Portal. You have to make sure that you have active subscription on the portal. To get a trial subscription, you can visit http://manage.windowsazure.com.

Section 1: Creating Service Namespace on the Windows Azure Portal

Step 1: Visit the Windows Azure portal and log in with your subscription credentials. In the browser, select Service Bus as shown here:

servicebus

Step 2: The following page will be displayed. Select the Create option:

managesb

Step 3: Clicking on Create will bring up a dialog box where we need to enter details for the namespace like name, region etc.

servicebus-namespace

The created namespace will be displayed as shown here:

servicebus-namespace-details

Step 4: To perform operations like creating queues on this namespace, we must retrieve management credentials for the namespace. This information will then further be made available with the applications who want to make use of the Service Bus queues for communicating with each other. To obtain these credentials, select the namespace created and click on the Connection Information at the bottom of the page.

connection-info

The Access Connection Information dialog box will be displayed as shown here:

access-connection-info

Copy the Connection string and paste it in notepad for further use.

Section 2 - Creating Queue

The next step is to create a queue so that our applications can use it for communication. Alternatively, we can create a queue using an application with the help of Windows Azure API which we can download using NuGet package.

Step 1: On the Windows Azure Portal, click on the Service Bus and click on the namespace we created in Section 1. The following page will be displayed:

queuepage

Step 2: In this page, click on ‘Queues’ and create a queue:

create-queue

Enter the queue details as shown here:

queuename

The created queue will be displayed as shown below:

queueinfo

Section 3: Understanding Service Bus Queue

Queues offers First-In-First-Out (FIFO) message delivery. The sender sends the message through the queue and receiver receives the message and processes it. The message is received and processed by only one message consumer. This mechanism provides decoupling across the applications. The following diagram depicts the behavior of the Service Bus Queue.

servicebus-queue

The above diagram clearly explains the working behavior of the Service Bus Queue. It provides brokered message communication model. This means that the sender and receiver application do not communicate directly, instead they exchange messages via queue. The sender application sends message to the queue and continues its work. The receiver application on the other hand pulls the message asynchronously from the queue and processes it. The receiver application processes message with the order in which they are put in the queue.

Using Service Bus Queue in an ASP.NET MVC application

In real world applications, Service Bus Queues are used for the disconnected communication between components of the distributed applications. In the following implementation, we will be implementing two ASP.NET MVC 5 Applications. The first application OrderPlacer, will be used by customers to place orders for medicines. The second application OrderPorcessor will be used by the Medicine provider to process orders.

The OrderPlacer application will place orders for medicine and the information will be stored in the queue. The OrderProcessor application will pull all orders from queue and process it.

Section 4: Designing the database required for the application

Step 1: Login on to the Windows Azure portal

Step 2: In the portal from the database, create a new database server with credentials and create a database as shown here:

create-new-database

Step 3: After creating the database, click on Manage at the bottom of the page. In the Manage page, enter database credentials. Select Design from the Manage page and create tables as shown here: (Alternatively Sql Server Management Studio can also be used)

CustomerMaster

customermaster

ItemMaster

itemmaster

OrderMaster

ordermaster

ProcessOrder

processororder

Add some test data in CustomerMaster and OrderMaster so that we can use it during the application development.

All tables are related as shown here:

tabledependencies

Section 5: Create MVC 5 applications and make use of Service Bus queues for offline communications

 

Step 1: Open Visual Studio and create a new Empty ASP.NET MVC application of the name ‘OrderPlacer’. In this project, in the Models folder, add a new ADO.NET Entity Data Model of the name CompanyEDMX and start the wizard. Select the Sql Azure Database which we have created in Section 4. In the wizard, select CustomerMaster, ItemMaster and OrderMaster. After the completion of the wizard, the table mapping will be displayed as follows:

table-mapping-orderplacer

Since we need to send order information to Service Bus Queue, we need to make the following changes in the OrderMaster:

[DataContract(Name="OrderMaster",Namespace="OrderMaster")]
public partial class OrderMaster
{
    [DataMember]
    public int OrderId { get; set; }
    [DataMember]
    public int ItemId { get; set; }
    [DataMember]
    public int Quantity { get; set; }
    [DataMember]
    public System.DateTime OrderDate { get; set; }
    [DataMember]
    public int CustomerId { get; set; }

    public virtual CustomerMaster CustomerMaster { get; set; }
    public virtual ItemMaster ItemMaster { get; set; }
}

Since Service Bus Queue requires objects to be serialized, we need to make use of the DataContract attribute on the object.

Build the project.

Step 2: To make use of the Windows Azure Service Bus, add the necessary package in the project from the NuGet Package Manager. Add the following package in the MVC Project:

azure-servicebus

After completion the Package installation, the following references will be added in the project:

· Microsoft.ServiceBus

· Microsoft.WindowsAzure.Configuration

The web.config file of the application will have necessary references added in it under <system.serviceModel>.

Step 3: In the web.config file of the application, we need to add the Service Bus endpoints in <appSettings> as shown here:

<add key="Microsoft.ServiceBus.ConnectionString" 
   value="Endpoint=sb://myqueue.servicebus.windows.net; SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=[Shared Access Key]" />

Step 4: In the Models folder, add a new class file and add the following code in it:

using Microsoft.ServiceBus;
using Microsoft.ServiceBus.Messaging;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;

namespace OrderPlacer.Models
{

/// <summary>
/// Class for Combining Customer, Order and Item Information with
/// CustomerId and ItemId
/// </summary>
/// 
public class CustomerOrderItemMaster
{
    public int CustomerId { get; set; }
    public OrderMaster Order { get; set; }
    public int ItemId { get; set; }

}

/// <summary>
/// Class cotaining method for placind order
/// </summary>
public class PlaceOrder
{
    CompanyEntities ctx;
    public PlaceOrder()
    {
        ctx = new CompanyEntities(); 
    }

    public List<ItemMaster> GetItems()
    {
        return ctx.ItemMasters.ToList();
    }

    public List<CustomerMaster> GetCustomers()
    { 
        return ctx.CustomerMasters.ToList();
    }

    /// <summary>
    /// Method to Create Order and Storing it in Queue
    /// </summary>
    /// <param name="coi"></param>
    public void CreateOrder(CustomerOrderItemMaster coi)
    {
        OrderMaster ord = new OrderMaster()
        {
             OrderId = coi.Order.OrderId,
             CustomerId= coi.CustomerId,
             Quantity = coi.Order.Quantity,
             ItemId = coi.ItemId,
              OrderDate = DateTime.Now
        }; 

        ctx.OrderMasters.Add(ord);
        ctx.SaveChanges();  
        
         //Code for Adding Message in Queue
        
        //S1: The NamespaceManager object used to manage queues, topics etc.
        string connString = ConfigurationSettings.AppSettings["Microsoft.ServiceBus.ConnectionString"];
        NamespaceManager namespaceManager = NamespaceManager.CreateFromConnectionString(connString);

        //S2: The Queue Description using QueueName
        QueueDescription queue = new QueueDescription("orderqueue");
        queue.MaxSizeInMegabytes = 5120; //5 MB
        queue.DefaultMessageTimeToLive = new TimeSpan(0, 30, 0); //For 30 Mins
        
        
        //S3: Send the Message
        QueueClient client = QueueClient.CreateFromConnectionString(connString, "orderqueue");
        BrokeredMessage message = new BrokeredMessage(ord);
        client.Send(message);
        //Ends Here

        
    }

}
}

The above code has the following specifications:

The class CustomerOrderItemMaster defines properties which are used to scaffold view.

The class PlaceOrder, contains logic for accessing data from Sql Azure and Placing Order

  • The method GetItems() contains logic to read all items.
  • The method GetCustomers() contains logic to read all customers.
  • The CreateOrder() method receives data from CustomerOrderItemMaster and passes it to the OrderMaster. The method also contains logic for creating queue client object using QueueClient class. The class NamespaceManager is used to manage Queues, Topics, Subscriptions, etc. The class QueueDescription is used to set the queue description such as size of the queue and the time for the message to live. The class BrokeredMessage represents the unit of messages communication between ServiceBus Clients.

 

Step 5: In the Controllers folder, add a new empty MVC Controller of the name OrderManagerController. Add the following action methods code in it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using OrderPlacer.Models;

namespace OrderPlacer.Controllers
{
public class OrderManagerController : Controller
{


    PlaceOrder obj;

    public OrderManagerController()
    {
        obj = new PlaceOrder();
    }

    // GET: OrderManager
    public ActionResult Index()
    {
        var info = new CustomerOrderItemMaster() { 
         Order =new OrderMaster() 
        };  
        //Customers Info
        ViewBag.CustomerId = new SelectList(obj.GetCustomers(), "CustomerId", "CustomerName");
        //Items Info
        ViewBag.ItemId = new SelectList(obj.GetItems(), "ItemId", "ItemName");
        return View(info);
    }

    [HttpPost]
    public ActionResult Index(CustomerOrderItemMaster c)
    {
        obj.CreateOrder(c);
        ViewBag.CustomerId = new SelectList(obj.GetCustomers(), "CustomerId", "CustomerName");
        ViewBag.ItemId = new SelectList(obj.GetItems(), "ItemId", "ItemName");
        return View(c);
    }


}
}

Step 6: Scaffold the Empty Index View (with model) from the Index Http Get method and add the following Html helpers in it:

@model OrderPlacer.Models.CustomerOrderItemMaster

@{
    ViewBag.Title = "Index";
}

<h2>Place Order</h2>


@using(Html.BeginForm())
{
<table>
    <tr>
        <td>
            Customer Name:
        </td>
        <td>
            @Html.DropDownList("CustomerId")
        </td>
    </tr>
    <tr>
        <td>
            Item Name:
        </td>
        <td>
            @Html.DropDownList("ItemId")
        </td>
    </tr>
    <tr>
        <td>
            Quantity:
        </td>
        <td>
            @Html.EditorFor(m => m.Order.Quantity)
        </td>
    </tr>
</table>
    <input type="submit" value="Submit"/>
}

Step 6: Run the Application, the Index View will get displayed:

placeorder

After clicking on the Submit, the Order will be stored in the queue, which we can see from the portal:

queue-data

Create Order Processor application

 

Step 1: Open another instance of Visual Studio and create a new empty MVC application, name it as OrderProcessor. In this project in the Models folder, add a new ADO.NET Entity Data Model of the name CompanyEDMX. This will start the wizard, select the Sql Azure Database created in Section 4. In the wizard select all tables and after the completion of the wizard, the table mapping will be displayed as shown here:

orderprocessor-maping

Since we need to send order information to Service Bus Queue, we need to make the following changes in the OrderMaster:

[DataContract(Name="OrderMaster",Namespace="OrderMaster")]
public partial class OrderMaster
{
    [DataMember]
    public int OrderId { get; set; }
    [DataMember]
    public int ItemId { get; set; }
    [DataMember]
    public int Quantity { get; set; }
    [DataMember]
    public System.DateTime OrderDate { get; set; }
    [DataMember]
    public int CustomerId { get; set; }

    public virtual CustomerMaster CustomerMaster { get; set; }
    public virtual ItemMaster ItemMaster { get; set; }
}

Since Service Bus Queue requires objects to be serialized, we need to make use of the DataContract attribute on the object, the same way we used previously.

Step 2: Add the NuGet Package for Windows Azure Service Bus in this project as we did in the OrderPlacer project.

Step 3: In the Models folder, add a new class file to contain the class managing Order Processing Information:

public class ProcessOrderInfo
{
    public int ProcessOrderId { get; set; }
    public int OrderId { get; set; }

    public int CustomerId { get; set; }
    public string CustomerName { get; set; }
    public int ItemId { get; set; }
    public string ItemName { get; set; }
    public int Quantity { get; set; }
    public int AvailableQuantity { get; set; }
    public string OrderStatus { get; set; }
}

Step 4: In the Models folder, add a new class file with the following code:

using System.Collections.Generic;

using Microsoft.ServiceBus;
using Microsoft.ServiceBus.Messaging;
using System.Configuration;

namespace OrderProcessor.Models
{
/// <summary>
/// The Class Constructor Receive Messages from Service Bus Queue
/// </summary>
public class QueueMessageReceiver
{
    string connStr = "";
    NamespaceManager nameSpaceManager;

    public QueueMessageReceiver()
    {
        connStr = ConfigurationSettings.AppSettings["Microsoft.ServiceBus.ConnectionString"];

        nameSpaceManager = NamespaceManager.CreateFromConnectionString(connStr);


    

    }

    /// <summary>
    /// Read All messages from Queue and Put it in the List of OrderMaster
    /// </summary>
    /// <returns></returns>
    public List<OrderMaster> GetOrdersFromQueue()
    {
        QueueClient qClient = QueueClient.CreateFromConnectionString(connStr, "orderqueue", ReceiveMode.PeekLock);
        //BrokeredMessage message = qClient.Receive();

       

        var messages = qClient.ReceiveBatch(1);

        List<OrderMaster> Orders = new List<OrderMaster>();

        
        foreach (var item in messages)
        {
            Orders.Add(item.GetBody<OrderMaster>());
            item.Complete(); //Remove Message from queue
        }

        return Orders;
    }
}
}

The above class makes a call to the Service Bus Queue and receives message from it. Messages received from the Queue will be stored in the list of OrderMaster. Once the message is received it will be removed from the queue.

Step 5: In the Controllers folder, add a new OrderMasterController with the following methods:

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web.Mvc;
using OrderProcessor.Models;

namespace OrderProcessor.Controllers
{
public class OrderMastersController : Controller
{
    private CompanyEntities1 db = new CompanyEntities1();

    // GET: OrderMasters
    public ActionResult Index()
    {
        List<OrderMaster> Orders = new List<OrderMaster>(); 

        //Get Message from Queue
        var msgs = new QueueMessageReceiver().GetOrdersFromQueue();

        if (msgs!=null)
        {
            var orderMasters = db.OrderMasters.Include(o => o.CustomerMaster).Include(o => o.ItemMaster);

            foreach (var msg in msgs)
            {
                foreach (var ord in orderMasters)
                {
                    if (msg.OrderId == ord.OrderId)
                    {
                        Orders.Add(ord);
                        break;
                    }

                }
            }

            return View(Orders);
        }
        else
        {
            return View("Index");
        }
    }

    /// <summary>
    /// Process Order View
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public ActionResult ProcessOrder(int id)
    {
        var Ord = db.OrderMasters.Find(id);

        ProcessOrderInfo ordProcess = new ProcessOrderInfo();
        ordProcess.OrderId = Ord.OrderId;
        ordProcess.CustomerId = Ord.CustomerId;
        ordProcess.CustomerName = db.CustomerMasters.Find(Ord.CustomerId).CustomerName;
        ordProcess.ItemId = Ord.ItemId;
        ordProcess.ItemName = db.ItemMasters.Find(Ord.ItemId).ItemName;
        ordProcess.Quantity = Ord.Quantity;
        ordProcess.AvailableQuantity = db.ItemMasters.Find(Ord.ItemId).AvailableQuantity;
        ordProcess.OrderStatus = "Processing";
        ViewBag.OrderStatus = new SelectList(new List<string>() {"Approved","Rejected"});
        return View("ProcessOrder",ordProcess);
    }

    [HttpPost]
    public ActionResult ProcessOrder(ProcessOrderInfo p)
    {
        ProcessOrder OrdToProcess = new ProcessOrder();

        OrdToProcess.OrderId = p.OrderId;
        OrdToProcess.CustomerId = p.CustomerId;
        OrdToProcess.ItemId = p.ItemId;
        OrdToProcess.OrderStatus = p.OrderStatus;

        //Save th processor Order Data
        db.ProcessOrders.Add(OrdToProcess);
        db.SaveChanges();

        if(OrdToProcess.OrderStatus=="Approved")
        { 
            //Update Item Quantity
            var itemUpdated = db.ItemMasters.Find(OrdToProcess.ItemId);
            itemUpdated.AvailableQuantity -= p.Quantity;
            db.SaveChanges();
        }

        //Delete the Order if it is Approved
        var order = db.OrderMasters.Find(OrdToProcess.OrderId);
        db.OrderMasters.Remove(order);
        db.SaveChanges();
        return View("ProcessedOrders", db.ProcessOrders.ToList());

    }
   
}
}

The above code has the following specifications:

  • The Index method will make a call to the GetOrdersFromQueue() method and check these orders with the OrderMaster Database table. If the order is found, it will be added in the list of OrderMaster and this list will be passed to the Index.cshtml view.
  • The ProcessOrder method will display the selected order from the Index View to be processed and will be displayed in the ProcessOrder.cshtml view.
  • The HttpPost ProcessOrder method will complete the Order Processing. Once the order is Approved, the Item’s AvailableQuantity will be reduced from the ItemMaster table. The Order will be removed from the OrderMaster table irrespective of Approved or Rejected. Processed Orders will be displayed in the ProcessedOrders.cshtml view.

Index.cshtml

@model IEnumerable<OrderProcessor.Models.OrderMaster>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.OrderId)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Quantity)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.OrderDate)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.CustomerMaster.CustomerName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ItemMaster.ItemName)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.OrderId)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Quantity)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.OrderDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.CustomerMaster.CustomerName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.ItemMaster.ItemName)
        </td>
        <td>
            @Html.ActionLink("Select Order to Process", "ProcessOrder", new { id = item.OrderId }) |
        </td>
    </tr>
}

</table>

ProcessOrder.cshtml

@model OrderProcessor.Models.ProcessOrderInfo

@{
    ViewBag.Title = "ProcessOrder";
}

<h2>ProcessOrder</h2>
@using (Html.BeginForm())
{

    <table>
        <tr>
            <td>
                Order Id:
            </td>
            <td>
                @Html.EditorFor(m => m.OrderId)
            </td>
        </tr>
        <tr>
            <td>
                Customer Id:
            </td>
            <td>
                @Html.EditorFor(m => m.CustomerName)
                @Html.Hidden("CustomerId", Model.CustomerId)
            </td>
        </tr>
        <tr>
            <td>
                Item Id:
            </td>
            <td>
                @Html.EditorFor(m => m.ItemName)
                @Html.Hidden("ItemId", Model.ItemId)
            </td>
        </tr>

        <tr>
            <td>
                Ordered Quantity:
            </td>
            <td>
                @Html.EditorFor(m => m.Quantity)
            </td>
        </tr>

        <tr>
            <td>
                Available Quantity:
            </td>
            <td>
                @Html.EditorFor(m => m.AvailableQuantity)
            </td>
        </tr>
        <tr>
            <td>
                Order Status:
            </td>
            <td>
                @Html.DropDownList("OrderStatus")
            </td>
        </tr>
    </table>
    <input type="submit" value="Save Process Order" />
}

ProcessedOrders.cshtml

@model IEnumerable<OrderProcessor.Models.ProcessOrder>

@{
    ViewBag.Title = "PrecessedOrders";
}

<h2>PrecessedOrders</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.OrderProcessId)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.OrderStatus)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.CustomerMaster.CustomerName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.ItemMaster.ItemName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.OrderMaster.OrderId)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.OrderProcessId)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.OrderStatus)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.CustomerMaster.CustomerName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.ItemMaster.ItemName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.OrderMaster.OrderId)
        </td>
        
    </tr>
}

</table>

Run and Test the application. The List of Orders will be displayed:

orders

Click on the ‘Select Order to Process’ and the order details will be displayed

orderdetails

Select Order to Approve from the List and click on ‘Save Process Order’. The Processed Orders List will be displayed as shown here:

processed-orders

Visit the Portal and see the queue length, it will be zero.

Conclusion

The Windows Azure Service Bus Queue is one of the best approaches for developing decoupled distributed applications.

Download the entire source code of this article (Github)

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Mahesh Sabnis is a DotNetCurry author and Microsoft MVP having over 17 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). Follow him on twitter @maheshdotnet


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by Mahesh on Wednesday, January 21, 2015 10:42 AM
Thanks Sally Wood.
Comment posted by Rick Schrader on Thursday, January 22, 2015 1:22 PM
Good article. I did want to point out that there are likely some safeguards missing to ensure that messages are not "lost". You are removing the messages from the bus as soon as they're being displayed to a user, but nothing guarantees that the user actually processes the message at that point. If the user simply closes the processor application, I believe the order message would be lost. This is a great example to get started with, but I wouldn't use this directly in production without handling scenarios like this.
Comment posted by Suprotim Agarwal on Friday, January 23, 2015 9:11 PM
@Rick Good suggestion! Mahesh can we update this article?
Comment posted by Mahesh Sabnis on Tuesday, February 10, 2015 3:56 AM
Rick Schrader,

  Thanks a lot. I am going to post the next article on the SB Message Handling soon where the solution will explain about use of SB.
Regards
Mahesh

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

FREE .NET MAGAZINES

Free DNC .NET Magazine

Tags

JQUERY COOKBOOK

jQuery CookBook