Website Review - ASP.NET

Table of Contents [Hide/Show]


Pitfalls




Usability and Administrative


Security

General






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


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


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


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


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




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



[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