ASP.NET Web Pages (aka Razor) lets you build logon capabilities into any website with relative ease. However, it's not necessarily obvious how to go about doing it. The ASP.NET Web Pages tutorial does include a chapter about how to add security to a website by using membership (logon) capability. But the chapter doesn't show you how to add security to an existing site.

What I want to show you is an absolutely bare-bones way to add logon to your site, and how to do it all manually from scratch. I'll try to keep the discussion straightforward, and I want to emphasize the use of simple APIs to implement security. Along the way, I'll maintain this three-pronged approach to implementing security for each procedure:

  • Must do: This describes the absolute minimum required processing.
  • Nice to do: These are the steps that you're almost certainly going to take in even the most basic implementation. For example, prompt the user for a password two times. I'll show these steps and note them.
  • Do in a real application: These are steps that you would (actually, must) take in a production application. For example, use SSL to protect password pages. This list is not intended to be exhaustive, and I won't go into detail about the steps because you might actually do many different things in a real application.

A Little Background

Razor has an infrastructure for security and logon that automates much of the process. For starters, membership information (usernames, passwords, and so on) are stored in a database in your site. Razor includes a membership provider, a component that handles the details of managing this database. You never have to think about how or where the logon information is being handled. For this article, I'll assume that you'll be happy with whatever the membership provider is doing under the hood, although I will note a few details about the database.

Razor also includes the WebSecurity helper and the Membership and Roles objects. Among them, these components include the methods you need to manage membership. These include a create-user method, a login method, a logout method, and so on.

This next exercise covers just a few of these basic capabilities. I'll show you how to perform the following tasks to implement security:

  • Initialize the membership system.
  • Create a public home page.
  • Create a registration page on which people can sign in.
  • Create a logon page.
  • Create a logout page. (You don't necessarily need a separate logout page, but I'll show how that's done.)
  • Create some protected content that is viewable only by people who are logged in.
  • Provide a way to manage roles (groups), such as creating and deleting roles or adding and removing users from roles.
  • Protect content by role (i.e., making content available only to users in a specific role).

Figure 1 shows the site layout.

Figure 1: Simple membership site structure

If you've ever worked with an ASP.NET membership tutorial, these tasks will be familiar because they are, essentially, the things you do with any membership system. The difference here is that I will, as noted, get this up and running in the sparsest way possible.

Initialize the Membership System

Before you use the membership system, the system must be initialized. Technically, you can do this at any time, as long as you do it before you start interacting with the system. In practice, you should initialize the membership system as soon as the application starts, which typically means that you do this in the site's _AppStart.cshtml file. Here is the complete code for the _AppStart.cshtml page:

@{
    WebSecurity.InitializeDatabaseConnection("TestMembership", "UserProfile",
        "UserId", "Email", true);
}

Must do

  • Use the WebMatrix tool to create a new, empty database in your site. By default, the name of the database will match the name of the site, but it will have the .sdf file-name extension.
  • In the root of the site, create a file named _AppStart.cshtml. As the name implies, this page contains code that runs when the site starts in response to the first request.
  • In the _AppStart.cshtml file, call the WebSecurity.InitializeDatabaseConnection method, as follows:
@{
  WebSecurity.InitializeDatabaseConnection("<nameOfDatabase>",
     "UserProfile", "UserId", "Email", true);
}

For the nameOfDatabase placeholder, use the name of the database that you just created. Make sure that the name is in quotation marks; that is, pass the name as a string. Important point: The database you refer to in the initialization method must already exist, even if the database is empty. Details about these values will follow.

Do in a real application

  • Point to an existing users table, if you have one.

Initialization Values

In the example, the code passes the following values to the initialization method:

  • nameOfDatabase: This is the name of an existing database. (Alternatively, this can be the name of a connection string in the web.config file that points to the database that you want to use.)
  • UserProfile: This is the name of the table in which user information (profile data) is stored.
  • UserId: This is the name of the primary-key column for user information in the user-profile table. This must be typed as a SQL Server int data type.
  • Email: This is the name of the column in the user table that holds the username (and is presumed to be an email address).
  • true: Passing true for the fifth parameter tells ASP.NET to create the membership tables in the specified database if the tables don't already exist.

Given the circumstances here (simplest possible membership), you needn't worry about these values. Just copy what you see in the example, and substitute your own database name. For more information about these values, see the sidebar "The Purpose of Initialization Values," at the end of this article.

Create a Home Page

The home page is what users see when they first visit the site. If the user has not yet logged on, the home page displays a logon link (see Figure 2).

Figure 2: Simple membership home page

If the user has already logged on, the page displays the username and a logout link (see Figure 3).

Figure 3: Simple membership home page: logged on

Figure 4 shows the complete code for the Home.cshtml page.

Figure 4: The Home.cshtml file in the root folder
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Home</title>
  </head>
  <body>
    <h1>Home Page</h1>
    <p>This is the home page.</p>
    <p>
      @if(WebSecurity.IsAuthenticated){
          @:<p>Welcome, @WebSecurity.CurrentUserName</p>
          @:<p><a href="@Href("~/Logout")">Log out</a></p>
      }
      else{
          @:<a href="@Href("~/Login")">Log in</a> |
          @:<a href="@Href("~/Register")">Register</a>
      }
    </p>
    <p>
        <a href="@Href("~/Members/Members")">Members area</a>
    </p>
    <p>
      <a href="@Href("~/Admin/ManageRoles")">Manage Roles</a> (must be logged in as the Admin user MikeP)
    </p>
  </body>
</html>

Must do

  • Create the Home.cshtml page in the website root.
  • Add a link to the logon page (Login.cshtml), which you'll create shortly.

Nice to do

  • Add a link to the Register page (Register.cshtml).
  • Add a link to a page in the Members folder.
  • On the Register page, call WebSecurity.IsAuthenticated to determine whether the user is already logged in. If this method returns true, display the current username (WebSecurity.CurrentUserName) and a link to the logout page (Logout.cshtml). Otherwise, just display a link to the logon page, as follows:
@if(WebSecurity.IsAuthenticated)
{
  <p>Welcome, @WebSecurity.CurrentUserName</p>
  <p><a href="@Href("~/logout")">Log out</a></p>
}
else
{
  <p><a href="@Href("~/Login")">Log in</a> |
  <a href="@Href("~/Register")">Register</a></p>
}

Do in a real application

  • Use layout pages and other methods to display the logon link and the current name in reusable chunks. (This actually applies to all pages in this article.)

Create a Register Page

A simple registration (register) page contains text boxes for the username (which can be an email address) and for the password. Typically, you make users enter the password two times because they can't see what they're typing. Figure 5 shows the registration page.

Figure 5: Registration page

Figure 6 shows the complete code for the Register.cshtml page.

Figure 6: The Register.cshtml file in the root folder, which lets users register on your site
@{
  var username = "";
  var password = "";
  var confirmPassword = "";
  var errorMessage = "";
 
  if(!IsPost){
    if(WebSecurity.IsAuthenticated){
      errorMessage = String.Format("You are already logged in. (User name: {0})", WebSecurity.CurrentUserName);
    }
  }
   
  if(IsPost){
    username = Request["username"];
    password = Request["password"];
    confirmPassword = Request["confirmPassword"];

    if(username.IsEmpty() || password.IsEmpty()){
        errorMessage = "All fields are required!";
    }
    else{
      if(password != confirmPassword){
        errorMessage = "Passwords don't match.";
      }
      else {
        if(WebSecurity.UserExists(username)){
          errorMessage = String.Format("User '{0}' already exists.", username);
        }
        else{
          WebSecurity.CreateUserAndAccount(username,password,null,false);
          WebSecurity.Login(username, password, true);
          errorMessage = String.Format("{0} created.", username);
        }
      }
    }
  }
}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title></title>
  </head>
  <body>
    <h1>Register</h1>
    <form method="post">
      <p>
        @if(errorMessage != ""){
            <span class="errorMessage">@Html.Raw(errorMessage)</span>
        }
      </p>
 
      <p>
        <label for="username">Username (email):</label><br/>
        <input type="text" name="username" id="username" value='@Request["username"]' />
      </p>
      <p>
        <label for="password">Password:</label><br/>
        <input type="password" name="password" id="password" value="" />
      </p>  
      <p>
        <label for="confirmPassword">Confirm password:</label><br/>
        <input type="password" name="confirmPassword" id="confirmPassword" value="" />
      </p>  
 
      <p>
        <input type="submit" value="Submit" />
      </p>  
      <p>
        <a href="@Href("~/Logout")">Log out</a>
        <br/>
        <a href="@Href("~/Home")">Home</a>
        </p>
      </form>
    </body>
</html>

Must do

  • Create Register.cshtml in the website root.
  • Add text boxes (input elements) for the username and password, plus a submit button.
  • On postback, check WebSecurity.UserExists to make sure that the username isn't already in use. If it isn't in use, call WebSecurity.CreateUserAndAccount to actually create the membership entry.
if(WebSecurity.UserExists(username))
{
    errorMessage = String.Format("User '{0}' already exists.",
        username);
}
else
{
    WebSecurity.CreateUserAndAccount(username, password,
        null, false);
    WebSecurity.Login(username, password, true);
    errorMessage = String.Format("{0} created.", username);
}

Nice to do

  • Make sure that the user has entered all required information.
  • Compare password entries to make sure that they're the same.
  • On submit, check WebSecurity.IsAuthenticated first to see whether the user is already logged in. If the user is logged in, display an error, as follows:
if(WebSecurity.IsAuthenticated)
{
   errorMessage = String.Format("You are already logged in." +
       " (User name: {0})", WebSecurity.CurrentUserName);
}
  • After you create the membership user, call WebSecurity.Login to automatically log in the user.
  • Redisplay the user's entry in the username text box. This is useful if an error occurs so that the user can see what was entered. Because of HTML constraints, you can't perform this step on the password text box.

Do in a real application

  • Use SSL to encrypt communication between browser and server. For more information, see the ASP.NET article "Securing Web Communications: Certificates, SSL, and https://."
  • Add a ReCaptcha test to make sure that it's a human being who is actually registering. For more information, see the "Preventing Automated Programs from Joining Your Website" section of the "Adding Security and Membership" tutorial.
  • Create a membership user, but don't activate it yet. Call Membership.GeneratePasswordResetToken to generate a token and to send the token in email. Users can follow that link to activate the membership. This requires that your website be configured to send email messages. For more information about how to do this, see the "Letting Users Generate a New Password" section in the "Adding Security and Membership" tutorial.
  • Redirect users to the home page or to the page from which they came.

Create a Logon Page

The login page lets the user enter credentials. Figure 7 shows the logon page.

Figure 7: Logon page

Figure 8 shows the complete code for the Login.cshtml page.

Figure 8: The Login.cshtml file in the root folder
@{
    var username = "";
    var password = "";
    var errorMessage = "";
   
    if(IsPost){
        username = Request["username"];
        password = Request["password"];
        if(WebSecurity.Login(username,password,true)){
            Response.Redirect("~/Home");
        }
        else
        {
            errorMessage = "Login was not successful.";
        }
    }
}
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>Login</title>
    </head>
    <body>
        <h1>Login</h1>
        <form method="post">
            @if(WebSecurity.IsAuthenticated){
                <p>You are currently logged in as @WebSecurity.CurrentUserName.
                  <a href="@Href("~/Logout")">Log out</a>
                </p>
            }
          <p>
            <label for="username">Username:</label><br/>
            <input type="text" name="username" id="username" value="" />
          </p>
          <p>
            <label for="password">Password:</label><br/>
            <input type="password" name="password" id="password" value="" />
          </p>  
          <p>
            <input type="submit" value="Submit" />
          </p>  
           
          <p>No user name? <a href="@Href("~/Register")">Register</a></p>
          <p><a href="@Href("~/Home")">Return to home page</a></p>
           
        </form>
        <p>
            @if(errorMessage != ""){
                <span class="errorMessage">@errorMessage</span>
            }
        </p>
   </body>
</html>

Must do

  • Create the Login.cshtml page in the website root.
  • Add text boxes for the username and password, and add a submit button.
  • On submit, call WebSecurity.Login to log in the user. If this method returns true, redirect users to the home page. Otherwise, display an error, as follows:
if(IsPost)
{
  username = Request["username"];
  password = Request["password"];
  if(WebSecurity.Login(username,password,true))
    {
      Response.Redirect("~/Home");
    }
  else
  {
     errorMessage = "Login was not successful.";
  }
}

Nice to do

  • Call WebSecurity.IsAuthenticated to determine whether the user is already logged on. If this property returns true, display the user's username (WebSecurity.CurrentUserName) and a link to the logout page, as follows:
@if(WebSecurity.IsAuthenticated)
{
  <p>You are currently logged in as @WebSecurity.CurrentUserName.
    <a href="@Href("~/Logout")">Log out</a>
 </p>
}
  • Include a link in the page to the Register page.

Do in a real application

  • Include a "Remember me" check box, and pass the value (true/false) to the overload of the WebSecurity.Login method that accepts this parameter.
  • Limit password length and strength by using the following code:
Membership.MinRequiredNonAlphanumericCharacters
Membership.MinRequiredPasswordLength
Membership.PasswordStrengthRegularExpression
  • Limit the number of times that you'll let users try to log in. To do this, use the following:
Membership.MaxInvalidPasswordAttempts
WebSecurity.GetPasswordFailuresSinceLastSuccess

Typically, you send users to an "account locked" page after a certain number of failed login attempts.

  • Redirect the user to the home page or to the page from which they came.
  • Create a password-recovery page. See the "Letting Users Generate a New Password" section in the "Adding Security and Membership" tutorial referenced earlier. Note that your site must be configured to send email messages.

Create a Logout Page

The logout page logs out the user. Under the covers, this page removes the browser cookie on the user's computer that lets ASP.NET know that the user is authenticated. Figure 9 shows the logout page.

Figure 9: Logout page

Figure 10 shows the complete code for the Logout.cshtml page.

Figure 10: The Logout.cshtml file in the root folder
@{
  WebSecurity.Logout();
}
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Logout</title>
    </head>
    <body>
        <h1>Logout</h1>
        <p>You are now logged out!</p>
        <p><a href="@Href("~/Home")">Return to home page</a></p>
    </body>
</html>

Must do

  • Create the Logout.cshtml page in the website root.
  • As soon as the page runs, call WebSecurity.Logout. You don't have to first verify whether that user is logged on.

Nice to do

  • Add some informative text.
  • Include a link to the home page.

Do in a real application

  • Call WebSecurity.Logout and immediately redirect users to the home page or to the page from which they came.

Protect Content

Protected content can be viewed only by people who are logged in. Typically, you put pages into a folder that's guarded by code that lets users through only if they're logged in (authenticated). Protection does not depend on what username is used. The only criteria here is that the user be logged in. Figure 11 shows a subfolder in a site that can contain protected content.

Figure 11: Site structure listing protected content

Here is the complete code for the _PageStart.cshtml page:

@{
    Response.CacheControl = "no-cache";

    if (!WebSecurity.IsAuthenticated) {
        Response.Redirect("~/Login");
    }
}

Must do

  • Create a subfolder named Members (although you can use any name).
  • Put files that you want to protect into this subfolder.
  • In the Members subfolder, create a file named _PageStart.cshtml. When any page in the subfolder is requested, this page runs first.
  • In the _PageStart.cshtml file, call WebSecurity.IsAuthenticated to determine whether the user is logged on. If the user is not logged on, redirect the user to the logon page, as follows:
@if (!WebSecurity.IsAuthenticated)
{
    Response.Redirect("~/Login");
}

Do in a real application

  • Code the redirect so that it passes the requested page to the logon page. The logon page can then redirect the user to the requested page after the user logs on.

Create a Page to Manage Roles

Roles are a convenient way to group users. This is handy if you want to grant different logged-on users access to different pages. The following conditions represent a typical example in which roles are used:

  • All users can access pages in the root.
  • Logged-on users can access pages in a members folder and can also access all public pages.
  • Users in a specific role (for example, "manager") can access pages in yet another subfolder, and they can also access member pages and public pages.

There are no built-in roles; a role is just a name that you create. You can think of it as a tag that you assign to a username. You can then look for that tag as a way to determine whether you'll allow someone access to pages.

Typically, you don't let users manage roles themselves. Unlike the other pages that are covered in this article, the page that you'll create here is one that should be available only to an administrator or to a super user (e.g., you). The page for managing roles should be protected so that only users in some sort of admin role can access it. In this example, the page is assumed to be in an administrator folder that you'll protect. (Because there's a chicken-and-egg problem with this scenario, you should protect the folder only after you've added your own username to the administrator role.)

The page that I describe here is just one of many methods you can use to manage roles. However, this page does illustrate the fundamental tasks in managing roles, which include creating and deleting roles, and adding users to and removing users from roles. Figure 12 shows what this page looks like:

Figure 12: SimpleMembership_ManageRoles

Figure 13 shows the complete code for the ManageRoles.cshtml page.

Notice that in this case, there's no "must do" category. This is because there's no one way to manage roles. For example, you could do everything by directly editing the database in WebMatrix. So this method simply shows some ways in which you can use APIs to manage roles.

Nice to do

  • Create a subfolder (for example, Admin) in the website.
  • In the Admin folder, create a page named ManageRoles.cshtml.

Everything that's listed in this "Nice to do" section happens in this role management page. I'll break down the procedure into separate tasks because this page is a little more complex than the other pages.

Display existing roles (and users in roles)

  • Call Roles.GetAllRoles to return a list, then loop through the list and display it in the page.
  • Optionally, list the users who are in a role. For each role, call Roles.GetUsersInRole. This step also returns a list through which you can loop (nested list) to display the names for that role. This code includes both tasks:
<ul>
  @foreach(var role in Roles.GetAllRoles())
  {
    <li>@role</li>
    <ul>
    @foreach(var user in Roles.GetUsersInRole(role))
    {
      <li>@user</li>
    }
   </ul>
  }
</ul>

Create and delete roles

  • Add a text box for the role names, a Create Role button to create a role, and a Delete Role button to delete the role.
  • On form submit, determine which button was clicked. If it was the Create Role button or the Delete Role button, get the role name from the request, and use the appropriate method.
  • Create the role. To create the role, call Roles.RoleExists to determine whether the name already exists. If it doesn't, and if the role name isn't empty, call Roles.CreateRole, as follows:
// Create new role
if(!Request["buttonCreateRole"].IsEmpty())
{
  roleName=Request["textRoleName"];
  if(!Roles.RoleExists(roleName) && !roleName.IsEmpty())
  {
    Roles.CreateRole(roleName);
  }
}
  • Delete the role. To delete the role, call Roles.GetUsersInRole to determine whether the role contains users. If it doesn't, and if the role name isn't empty, call Roles.DeleteRole, as follows:
// Delete role
if(!Request["buttonDeleteRole"].IsEmpty())
{
  roleName=Request["textRoleName"];
  if(Roles.GetUsersInRole(roleName).Length == 0 &&
    !roleName.IsEmpty())
  {
    // true means throw if any users are in this role
    Roles.DeleteRole(roleName, true);
  }
}

In this case, you don't have to see whether the role exists. If you call Roles.DeleteRole for a non-existent role, no error occurs.

Add and delete users in roles

  • Display users in a list box. To do this, connect to the database and query the UserProfile table. Then, loop through the list and add the names to a select element so that you can pick one, as follows:
var db = Database.Open("nameOfDatabase ");
var selectQueryString =
    "SELECT UserId, Email FROM UserProfile";
// ...
<label for="selectUserName">Users:</label>
<select name="selectUserName">
  @foreach(var row in db.Query(selectQueryString))
  {
    <option>@row.Email</option>
  }
</select>

(A Membership.GetAllUsers method exists that should do this, but that method doesn't work correctly. Therefore, you have to manually query the database.) Note that you must specify the same name for the database (nameOfDatabase in the previous example) that you used previously when you initialized the membership system.

  • List the roles in a list box. To do this, call Roles.GetAllRoles again; but this time, put all the roles in a select element, as follows:
<label for="selectRoleName">Roles:</label>
<select name="selectRoleName">
  @foreach(var role in Roles.GetAllRoles())
  {
    <option>@role</option>
  }
</select>
  • Add an Add User to Role button and a Delete User from Role button.
  • On form submit, determine which button was clicked. If it was the Add User to Role button or the Delete User from Role button, use the appropriate method.

Add a user to a role

  • To add a user to a role, get the username from the user list box and the role name from the roles list box. If the user is not already in that role, call Roles.AddUsersToRoles. Note the plural in the method name. The method takes arrays of users and roles because it can add multiple users to multiple roles at the same time. Therefore, you must create one-element arrays, then add the username and role to the arrays before you call the method, as follows:
// Add user to role
if(!Request["buttonAddUserToRole"].IsEmpty()){
  userName = Request["selectUserName"];
  roleName = Request["selectRoleName"];
  if(!Roles.IsUserInRole(userName, roleName)){
    Roles.AddUsersToRoles(
        new [] { userName }.ToArray(),
        new [] { roleName }.ToArray()
    );
  }
} // if(buttonAddUserToRole)

Delete a user from a role

·         To delete a user from a role, get the name and role. If the user is in that role, call Roles.RemoveUsersFromRoles. This method takes arrays as arguments. Therefore, just as you did with Roles.AddUsersToRoles, you must put the username and role name into one-element arrays, as follows:

// Delete user from role
if(!Request["buttonDeleteUserFromRole"].IsEmpty())
{
  userName = Request["selectUserName"];
  roleName = Request["selectRoleName"];

  if(Roles.IsUserInRole(userName, roleName))
  {
    Roles.RemoveUsersFromRoles(
      new [] { userName }.ToArray(),
      new [] { roleName }.ToArray()
    );
  }
} // if(buttonDeleteUseFromRole)

Do in a real application

  • Use SSL to encrypt communication between browser and server. See "Securing Web Communications: Certificates, SSL, and https://" on the ASP.NET site, mentioned earlier in the Create a Register Page section.
  • Limit the roles to just a few that are needed for the application instead of allowing arbitrary roles to be created. In fact, you might create only the one or two roles that must be in the database directly, then, likewise, assign roles to the few users who must be in a specific role.
  • When you list roles, don't try to list every user in every role or to even simply list every user. A real application can involve thousands of users.

Protect Content by Role

The point of roles is to protect content so that only users in certain roles can see the content. This is almost exactly like protecting content by limiting it to authenticated users. As noted earlier, you should add this protection after you've added yourself to the admin role. Otherwise, you'll never be able to access this page. Here is the complete code for the _PageStart.cshtml page for roles:

@{
    Response.CacheControl = "no-cache";
    // Determine whether current user is in Admin role
    if (!Roles.IsUserInRole(WebSecurity.CurrentUserName, "Admin")){
      Response.SetStatus(HttpStatusCode.Forbidden);
    }
}

Must do

  • Create a subfolder (for example, create Admin) in the website. (Note that you did this already for the ManageRoles.cshtml file.)
  • Put files that you want to protect into this subfolder.
  • In the Admin subfolder, create a _PageStart.cshtml file. You can have different _PageStart.cshtml files in different folders.
  • In the _PageStart.cshtml file, call Roles.IsUserInRole. Pass to this method the current username (WebSecurity.CurrentUserName) and the name of the role that you want to check. If the current user is not in that role, redirect the user to the home page, as follows:
if (!Roles.IsUserInRole(WebSecurity.CurrentUserName,
      "Admin"))
{
  Response.Redirect("~/Home");
}

Nice to do

  • Instead of redirecting the user to the home page, return an HTTP "Forbidden" code (403). The only advantage of doing this is that it explicitly tells users that they're not allowed to view that content. To do this, use the following code:
     
if (!Roles.IsUserInRole(WebSecurity.CurrentUserName,
      "Admin"))
{
  Response.SetStatus(HttpStatusCode.Forbidden);
}

A Handy Website Security Technique

As you can see, you can set up basic security to your website by adding about eight pages to your site (even fewer if you don't care about roles) and a few pages' worth of code. Most of the work is handled by the membership provider together with a few helpers. The real trick lies is in knowing how to wield the dozen or so methods and properties that do all this work.

Obviously, there are many niceties that you can add to this system, both for the requirements for real-world applications (some of which are mentioned here) and just to make everything look nice. For example, you don't necessarily need a separate logon page. Instead, you can integrate the logon process in your home page. Even so, you'll still work with the APIs that you've seen here and with others that are exposed by the membership system. Considering how much work it might take to implement all this by yourself, the ASP.NET membership system is a very handy feature to get to know.

This article is adapted from a blog entry written and published by the author on his blog (mikepope.com/blog).

Mike Pope (mpope@microsoft.com) is a technical writer and editor on the Microsoft ASP.NET documentation team. He is a specialist in .NET Framework, ASP.NET, and Visual Basic technologies.