Jasinski Technical Wiki

Navigation

Home Page
Index
All Pages

Quick Search
»
Advanced Search »

Contributor Links

Create a new Page
Administration
File Management
Login/Logout
Your Profile

Other Wiki Sections

Software

PoweredBy

Website Review - ASP.NET

RSS
Modified on Fri, Dec 11, 2009, 9:27 AM by Administrator Categorized as ASP·NET Web Forms

Table of Contents [Hide/Show]


Pitfalls

  • Multi-line textboxes don't enforce the MaxLength property. To compensate, use the TextBox Class.

  • Apostrophes — When (1) the target of a hyperlink or (2) any portion of JavaScript code is generated from user-provided data, be sure to compensate for apostrophes in the data by following these guidelines.

  • JavaScript confirm function — Fix a quirk with the JavaScript confirm function by replacing it with the confirmExt function found here.

Usability and Administrative

  • Remove files that aren't used.
  • Ensure the site degrades gracefully without JavaScript support.
  • Ensure the logo at the top of the page is a hyperlink to the home page.
  • Ensure every page has a control with the initial focus.
  • Ensure the submit button on every page responds to the Enter key.

Security

General

  • Encrypt the connection string in the web.config file, according to this procedure.

  • Write stored procedures using the WITH EXECUTE AS DBO clause.

  • Grant the user in the connection string minimal rights to the database: no SELECT rights on tables or views; no VIEW DEFINITION on any code objects; just EXECUTE rights on all stored procedures.

  • Handle exceptions on every page with a Page_Error() event handler
    1. Use Server.GetLastError to get the exception object
    2. Use Server.ClearError to avoid the bubbling up of exceptions

  • In global.asax, write an Application_Error() procedure.

Preventing Denial-of-Service Attacks

Denial-of-service attacks can be prevented by using the ActionValidator Class.

Performance Tweaks

Adapted from this article on The Code Project's website.

General

  • Disable viewstate on any page not needing it.

Pipeline

The ASP.NET engine by default includes several modules which you may not be using. If you don't use them, they're still in the pipeline, executing unnecessary code for every server request. Remove them via your WEB.CONFIG file.

<httpModules>
    <remove name="OutputCache" />
    <remove name="Session" />
    <remove name="WindowsAuthentication" />
    <remove name="FormsAuthentication" />
    <remove name="PassportAuthentication" />
    <remove name="UrlAuthorization" />
    <remove name="FileAuthorization" />
    <remove name="ErrorHandlerModule" />
    <remove name="AnonymousIdentification" />
</httpModules>

Process Configuration

The default configuration in MACHINE.CONFIG is often too limiting. Refer to this http://msdn.microsoft.com/en-us/library/ms998549.aspx article for details on these settings.]

<system.net>
    <processModel 
        enable="true" 
        timeout="Infinite" 
        idleTimeout="Infinite" 
        shutdownTimeout="00:00:05" 
        requestLimit="Infinite" 
        requestQueueLimit="5000" 
        restartQueueLimit="10" 
        memoryLimit="60" 
        webGarden="false" 
        cpuMask="0xffffffff" 
        userName="machine" 
        password="AutoGenerate" 
        logLevel="Errors" 
        clientConnectedCheck="00:00:05" 
        comAuthenticationLevel="Connect" 
        comImpersonationLevel="Impersonate" 
        responseDeadlockInterval="00:03:00" 
        responseRestartDeadlockInterval="00:03:00" 
        autoConfig="false" 
        maxWorkerThreads="100" 
        maxIoThreads="100" 
        minWorkerThreads="40" 
        minIoThreads="30" 
        serverErrorMessageFile="" 
        pingFrequency="Infinite" 
        pingTimeout="Infinite" 
        asyncOption="20" 
        maxAppDomains="2000" 
     />
     <connectionManagement>
          <add address="*" maxconnection="100" />
     </connectionManagement>
     

SettingDescription
maxWorkerThreads This is default to 20 per process. On a dual core computer, there'll be 40 threads allocated for ASP.NET. This means at a time ASP.NET can process 40 requests in parallel on a dual core server. I have increased it to 100 in order to give ASP.NET more threads per process. If you have an application which is not that CPU intensive and can easily take in more requests per second, then you can increase this value. Especially if your Web application uses a lot of Web service calls or downloads/uploads a lot of data which does not put pressure on the CPU. When ASP.NET runs out of worker threads, it stops processing more requests that come in. Requests get into a queue and keeps waiting until a worker thread is freed. This generally happens when site starts receiving much more hits than you originally planned. In that case, if you have CPU to spare, increase the worker threads count per process.
maxIOThreads This is default to 20 per process. On a dual core computer, there'll be 40 threads allocated for ASP.NET for I/O operations. This means at a time ASP.NET can process 40 I/O requests in parallel on a dual core server. I/O requests can be file read/write, database operations, web service calls, HTTP requests generated from within the Web application and so on. So, you can set this to 100 if your server has enough system resource to do more of these I/O requests. Especially when your Web application downloads/uploads data, calls many external webservices in parallel.
minWorkerThreads When a number of free ASP.NET worker threads fall below this number, ASP.NET starts putting incoming requests into a queue. So, you can set this value to a low number in order to increase the number of concurrent requests. However, do not set this to a very low number because Web application code might need to do some background processing and parallel processing for which it will need some free worker threads.
minIOThreads Same as minWorkerThreads but this is for the I/O threads. However, you can set this to a lower value than minWorkerThreads because there's no issue of parallel processing in case of I/O threads.
memoryLimit Specifies the maximum allowed memory size, as a percentage of total system memory, that the worker process can consume before ASP.NET launches a new process and reassigns existing requests. If you have only your Web application running in a dedicated box and there's no other process that needs RAM, you can set a high value like 80. However, if you have a leaky application that continuously leaks memory, then it's better to set it to a lower value so that the leaky process is recycled pretty soon before it becomes a memory hog and thus keep your site healthy. Especially when you are using COM components and leaking memory. However, this is a temporary solution, you of course need to fix the leak.
system.net/connectionManagementSpecifies the maximum number of outbound requests that can be made to a single IP. Default is 2, which is just too low. This means you cannot make more than 2 simultaneous connections to an IP from your Web application. Sites that fetch external content a lot suffer from congestion due to the default setting. Here I have set it to 100. If your Web applications make a lot of calls to a specific server, you can consider setting an even higher value.

Profile/Membership Provider

  • Add applicationName attribute in Profile Provider.

<profile enabled="true"> 
<providers> 
<clear /> 
<add name="..." type="System.Web.Profile.SqlProfileProvider" 
connectionStringName="..." applicationName="YourApplicationName" 
description="..." /> 
</providers> 

  • Turn off automatic profile saving, and do saves manually via Profile.Save();.

<profile enabled="true" automaticSaveEnabled="false" >

  • Cache roles. (This won't work for users with many roles, which exceeds the 2KB cookie limit.)

<roleManager enabled="true" cacheRolesInCookie="true" >

  • When querying Membership tables, do NOT use Email or UserName in the WHERE clause; instead, use LoweredEmail and LoweredUserName, which are indexed.

  • When querying Membership tables, ALWAYS include an ApplicationId in your WHERE clause to avoid table scans.

  • Rework the aspnet_Profile_GetProfiles procedure. It doesn't need a temp table (which spikes I/O usage), and could probably do without the LIKE LOWER(@UserNameToMatch) in the WHERE clause.

CREATE PROCEDURE [dbo].[aspnet_Profile_GetProfiles]
   @ApplicationName nvarchar(256),
    @ProfileAuthOptions int,
    @PageIndex  int,
    @PageSize   int,
    @UserNameToMatch   nvarchar(256) = NULL,
    @InactiveSinceDate datetime = NULL
AS

IF @UserNameToMatch IS NOT NULL 
BEGIN
        SELECT u.UserName, u.IsAnonymous, u.LastActivityDate, p.LastUpdatedDate,
      DATALENGTH(p.PropertyNames)
        + DATALENGTH(p.PropertyValuesString) + DATALENGTH(p.PropertyValuesBinary)
      FROM    dbo.aspnet_Users u
        INNER JOIN dbo.aspnet_Profile p ON u.UserId = p.UserId
      WHERE u.LoweredUserName = LOWER(@UserNameToMatch)
   
        SELECT @&#64;ROWCOUNT
END

ELSE
    BEGIN -- Do the original bad things

    DECLARE @ApplicationId uniqueidentifier
    SELECT @ApplicationId = NULL
    SELECT @ApplicationId = ApplicationId
                FROM aspnet_Applications
                    WHERE LOWER(@ApplicationName)
                            = LoweredApplicationName
    
   IF (@ApplicationId IS NULL)
        RETURN
 
   -- Set the page bounds
    DECLARE @PageLowerBound int
    DECLARE @PageUpperBound int
    DECLARE @TotalRecords   int
   SET @PageLowerBound = @PageSize * @PageIndex
   SET @PageUpperBound = @PageSize - 1 + @PageLowerBound
 
    -- Create a temp table TO store the select results
    CREATE TABLE #PageIndexForUsers
    (
      IndexId int IDENTITY (0, 1) NOT NULL,
      UserId uniqueidentifier
    )
 
    -- Insert into our temp table
   INSERT INTO #PageIndexForUsers (UserId)
       
      SELECT u.UserId 
        FROM    dbo.aspnet_Users
            u, dbo.aspnet_Profile p 
      WHERE   ApplicationId = @ApplicationId 
            AND u.UserId = p.UserId       
                AND (@InactiveSinceDate
                IS NULL OR LastActivityDate
                        <= @InactiveSinceDate)
                AND (   
                    (@ProfileAuthOptions = 2)
                OR (@ProfileAuthOptions = 0 
                        AND IsAnonymous = 1)
                OR (@ProfileAuthOptions = 1 
                        AND IsAnonymous = 0)
                    )
                AND (@UserNameToMatch
                IS NULL OR LoweredUserName
                    LIKE LOWER(@UserNameToMatch))
        ORDER BY UserName
 
    SELECT u.UserName, u.IsAnonymous, u.LastActivityDate,
      p.LastUpdatedDate, DATALENGTH(p.PropertyNames)
      + DATALENGTH(p.PropertyValuesString) 
      + DATALENGTH(p.PropertyValuesBinary)
    FROM    dbo.aspnet_Users
                    u, dbo.aspnet_Profile p, #PageIndexForUsers i
    WHERE   
      u.UserId = p.UserId 
      AND p.UserId = i.UserId 
      AND i.IndexId >= @PageLowerBound 
      AND i.IndexId <= @PageUpperBound
 
    DROP TABLE #PageIndexForUsers
 
    END
END

Use AJAX

  • Use AJAX to load content on an as-needed basis.

  • Cache AJAX calls, remembering that only HTTP GET calls can be cached; HTTP POST calls can't be cached.

[WebMethod][ScriptMethod(UseHttpGet=true)]
public string CachedGet()
{
    TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
    Context.Response.Cache.SetCacheability(HttpCacheability.Public);
    Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
    Context.Response.Cache.SetMaxAge(cacheDuration);
    Context.Response.Cache.AppendCacheExtension(
           "must-revalidate, proxy-revalidate");

    return DateTime.Now.ToString();
}

Browser Cache

  • Cache static content for as long as possible (e.g., 30 days) via IIS Manager > website properties > HTTP Headers tab > Expire after 30 days.

  • Reuse common graphics files from the same folder, rather than multiple folders.

  • Store the URL for each static file in a configuration setting, using querystring parameter to indicate the date/version of a static file. For example, to access ~/images/logo.png, have a configuration setting point to ~/images/logo.png?ver=20091029 for the 10/29/2009 version. When you update the logo.png file, update the configuration setting with the new date, and browsers will be forced to get a new copy of the image.

  • Store static files in a different domain. (The browser can open two concurrent connections; and you won't be sending cookies for all the static files in the dynamic content requests.)

  • Minimize SSL usage, as it is not cached.

  • Generate Content-Length response headers. Otherwise, the browser keeps the connection open as long as it's getting data till the connection closes.

ScrewTurn Wiki version 3.0.1.400. Some of the icons created by FamFamFam. Except where noted, all contents Copyright © 1999-2018, Patrick Jasinski.