ASP.Net Multithreading in a Web Farm Environment

by Gong Liu July 06, 2009 08:04

This article is also published on The Code Project

In one of my recent projects I have a situation where I need to generate two files on the server based on user input. Both files can be quite big (in terms of mobile devices), and take long time to generate by the server and to download by the user. The two files must be generated sequentially, as the second file depends on the first. One way to implement this would be to generate both files and then return to the user the download URLs in one request-response. The problem with this approach is that the user has to wait very long time before the server finishes the job. An obvious improvement is to return immediately after the first file is generated, and at the same time to spawn a new thread to generate the second file. This way, upon the request responded, the user can start to download the first file while the server is working on the generation of the second file. One problem with this approach is that the user will not know when the second file is ready for download. My solution is to create another page that allows the user to check the status of the second file. If it is ready, the response will include the download URL so the user can start to download it. If it is not ready, the user can either wait or do something else and check the status later. This works fine in my case, as the downloading time for the first file almost always longer than the time needed to generate the second file.

Another problem with this approach is that my ASP.Net application is running in a web farm environment in which I have a shared file store for the generated files and the newly spawned thread does not have proper permissions to write to the shared file store. The following diagram shows my simple web farm of two web servers, WS1 and WS2, and the file store. The file store is just a shared folder on one of the two web servers.

 

I use the steps below to configure the web servers so they can access to the shared folder. These steps apply to Windows 2000 (Advanced) Server OS. Yes! I'm still using Windows 2000 servers Smile

  1. Create a Windows user account, say "SharedUsr", on both WS1 and WS2. 
  2. Add <identity impersonate="true" userName="SharedUsr" password="xxxxxx"/> to web.config on both servers.
  3. Run these commands on both servers to grant "SharedUsr" access to the shared folder used by my ASP.Net application: 
    C:\WINNT\Microsoft.NET\Framework\v2.0.50727>aspnet_regiis -ga WS1\SharedUsr
    C:\WINNT\Microsoft.NET\Framework\v2.0.50727>aspnet_regiis -ga WS2\SharedUsr
  4. Make sure "SharedUsr" has write permission on the shared folder

This, however, does not work if the current thread spawns a new thread to write to the shared folder, because the new thread does not automatically impersonate "SharedUsr". I have to write code to force the impersonation. The following is the code. I have left out some details for clarity.  

......
using System.IO;
using System.Security.Principal;
using System.Threading;

public partial class GenerateFiles : System.Web.UI.Page
{
    private WindowsIdentity wid = null
    protected void Page_Load(object sender, EventArgs e)
    { 
        //Generate file1 ......

        wid = WindowsIdentity.GetCurrent(); //This is "SharedUsr" 
        try
        {
            //Use a thread from the threadpool to generate file2
            if (ThreadPool.QueueUserWorkItem(new WaitCallback(File2Callback),
                new File2Generator(file2Location)))
            {
                //Return file1 URL to user so he can start to download file1
                Response.Write(file1URL); 
            }
            else
            {
                Response.Write("Server too busy. The work item could not be queued.");
            }
        }
        catch (Exception ex)
        {
            Response.Write(ex.Message);
        }  
    }

    private void File2Callback(object o)
    {
        //Set the pooled worker thread's principal to that of wid   
        WindowsPrincipal principal = new WindowsPrincipal(wid);
        Thread.CurrentPrincipal = principal;

        //Make the the pooled worker thread to impersonate 'SharedUsr'
        WindowsImpersonationContext wic = wid.Impersonate();

        //Generate file2
        File2Generator f2gen = (File2Generator)o;
        f2gen.Generate();

        //Undo impersonation before return to threadpool
        wic.Undo();
    }
}

In the begining of Page_Load, we assume file1 has been generated. Then we use a worker thread from the threadpool to queue the generation of file2 for execution. The QueueUserWorkItem method has two parameters. The first parameter specifies the function to be called when the worker thread has its turn. The second parameter is the data to be passed to the function. In our case, it's an instantce of File2Generator class, which encompasses necessary data and functionality to generate file2. The implementation of File2Generator is not listed here because the detail is not important for making the point.

The callback function, File2Callback, will be executed on the worker thread that has the Windows identity "ASPNET", not "SharedUsr". So in it we need to change the worker thread's impersonation before calling the Generate method of File2Generator.

Using Formatting Functions with GridView/Template Field

by Gong Liu February 05, 2009 15:32

This article is also published on The Code Project

A TemplateField of a GridView control offers certain level of flexibility to combine data fields and format them for display. For example, we can use a TemplateField to display FirstName and LastName in a single GridView column, and even add a link to the combined column. However, if the formatting is too complex, using a formatting function with a TemplateField is the way to go.

Let's explore the ways to use formatting functions in the following example, where our data source is a table of POIs (points of interest) resulted from a spatial search, and the table includes these data fields:

  • POIName
  • Address
  • City
  • County
  • State
  • Zip
  • Country
  • Phone
  • CatCode (Category Code)
  • Latitude
  • Longitude
  • Distance (Distance from search center)

What we want to display to a user is (see screenshot at the end of this post):

  • The POIName field with a link to show the POI on Google Maps based on the POI's Latitude and Longitude fields
  • A single Address field which combines Address, City, County, State, Zip, and Country fields
  • The Phone field
  • The CatCode field
  • The Distance field

The first two fields are of particular interest. To display the POIName field with a link to Google Maps, we can use the following TemplateField:

<asp:GridView ID="gvwResults" runat="server" AutoGenerateColumns="False"
  EmptyDataText="No result found." CellPadding="2" EnableViewState="False">
  <Columns>
    <asp:TemplateField HeaderText="POI Name">
      <itemtemplate>
        <asp:LinkButton ID="LinkButton1" runat="server" title="map it"
          PostBackUrl="<%# GoGoogleMaps(Eval(&quot;Latitude&quot;),
          Eval(&quot;Longitude&quot;)) %>">
          <%# Eval("POIName") %>
        </asp:LinkButton>
      </itemtemplate>
    </asp:TemplateField>
    ......
  </Columns>
</asp:GridView>

The TemplateField contains a LinkButton server control. The LinkButton control's text is the POIName field, and its PostBackUrl attribute is bound to the formatting function GoGoogleMaps with Latitude and Longitude fields as input parameters. Notice that we have to use the xml entity &quot; to surround a data field name because we can't have double quotes within double quotes. The formatting function GoGoogleMaps looks like this in C#:

protected string GoGoogleMaps(object lat, object lon)
{
    if (Convert.IsDBNull(lat) || Convert.IsDBNull(lon))
        return null;
    else
        return string.Format(
            "http://maps.google.com/maps?f=q&hl=en&geocode=&q={0},{1}&z=15", lat, lon);
}

The Eval method of DataBinder class always evaluates a data field to an object and that's why we declare so for all parameters of the formatting function. The latitude and longitude fields of a POI could be null, and if so, we simply don't link it to Google Maps. Formatting rules like this would be hard to implement without a formatting function.

Instead of using a LinkButton control, we can also use a Literal control and construct the link ourselves within the formatting function. This way we have more control over the link's target and other aspects. 

<asp:TemplateField HeaderText="POI Name">
  <itemtemplate>
    <asp:Literal ID="Literal1" runat="server" Text=
      "<%# GetGoogleMapsLink(Eval(&quot;POIName&quot;),
      Eval(&quot;Latitude&quot;),
      Eval(&quot;Longitude&quot;)) %>">
    </asp:Literal>
  </itemtemplate>
</asp:TemplateField>

The corresponding formatting function GetGoogleMapsLink looks like:

protected string GetGoogleMapsLink(object poiName, object lat, object lon)
{
    string poi = (Convert.IsDBNull(poiName)) ? "Unknown POI" : (string)poiName;
    if (Convert.IsDBNull(lat) || Convert.IsDBNull(lon))
        return poi;
    else
        return string.Format(
            "<a href=\"#\" title=\"map it\" onclick=\"window.open(
            'http://maps.google.com/maps?f=q&hl=en&geocode=&q={0},{1}&z=15', 'GM')\">
            {2}</a>", lat, lon, poi);
}

The formatting function creates and returns a link that opens Google Maps in a separate browser window. It also handles the possibility of a null POIName field. If we compare the server generated pages from the above two ways (in a browser, right click the page and select View Source), we will see that the result of the second way is much more readable and less clustered.

Now let's see how we do about the combined Address field. The traditional way is to have a TemplateField with several Label controls, one for each address element, such as:

<asp:TemplateField HeaderText="Address">
  <itemtemplate>
    <asp:Label ID="Label1" runat="server" Text='<%# Bind("Address") %>'></asp:Label>,
    <asp:Label ID="Label2" runat="server" Text='<%# Bind("City") %>'></asp:Label>,
    <asp:Label ID="Label3" runat="server" Text='<%# Bind("County") %>'></asp:Label>,
    <asp:Label ID="Label4" runat="server" Text='<%# Bind("State") %>'></asp:Label>,
    <asp:Label ID="Label5" runat="server" Text='<%# Bind("Zip") %>'></asp:Label>,
    <asp:Label ID="Label6" runat="server" Text='<%# Bind("Country") %>'></asp:Label>
  </itemtemplate>
</asp:TemplateField>

Notice that we use commas to separate address elements from each other. This is all fine until we want a little bit formatting logic - we want to trim off any excess commas at both ends of the combined address string due to possible missing address elements. However, if an address element in the middle is missing, we'll still keep the extra comma. To do this we use a single Label control and bind its Text attribute to a formatting function that takes all the address fields as input parameters:

<asp:TemplateField HeaderText="Address">
  <itemtemplate>
    <asp:Label ID="Label1" runat="server" Text="<%# FormatAddress(
      Eval(&quot;Address&quot;),
      Eval(&quot;City&quot;),
      Eval(&quot;County&quot;),
      Eval(&quot;State&quot;),
      Eval(&quot;Zip&quot;),
      Eval(&quot;Country&quot;)) %>">
    </asp:Label>
  </itemtemplate>
</asp:TemplateField> 

where, the formatting function FormatAddress is:

protected string FormatAddress(object address, object city, object county,
    object state, object zip, object country)
{
    string addr = string.Format("{0},{1},{2},{3},{4},{5}", address,
        city, county, state, zip, country);
    return addr.Trim(",".ToCharArray());

An improvement over the above use of a formatting function with a TemplateField is that we can pass the entire data row to the formatting function in stead of individual data fields, especially when more than one or two data fields are involved. For instance, instead of passing 6 data fields in the above case, we can pass the current data row to FormatAddress:

<asp:TemplateField HeaderText="Address">
  <itemtemplate>
    <asp:Label ID="Label1" runat="server"
      Text="<%# FormatAddress(((System.Data.DataRowView)Container.DataItem).Row) %>">
    </asp:Label>
  </itemtemplate>
</asp:TemplateField>

Here, Container.DataItem returns a DataRowView object corresponding to the DataSource record bound to the GridViewRow. Its Row property returns a DataRow object. The new formatting function FormatAddress now looks like:

protected string FormatAddress(DataRow dr)
{
    string addr = string.Format("{0},{1},{2},{3},{4},{5}",
        dr["Address"], dr["City"], dr["County"], dr["State"], dr["Zip"], dr["Country"]);
    return addr.Trim(",".ToCharArray());

This is obviously a much cleaner way than passing multiple data fields. The following screenshot shows the GridView with search result of some airports and airport terminals near Los Angeles.

In conclusion, a formatting function is a great way to extent the power of TemplateField. It allows developers to implement complex formatting rules that usually involve multiple data fields and their interrelationships.

About

A seasoned computer professional. A tofu culture evangelist...
more >>

Tag Cloud

Calendar

<<  April 2017  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar
Copyright © 2008-2011 Gong Liu. All rights reserved. | credits | contact me
The content on this site represents my own personal opinions, and does not reflect those of my employer in any way.