2

I have roles on my ASP.NET application. I have figure the problem (I think). The problem every page in the applications uses role and permission. Therefore it uses the following function in page load

if (Roles.IsUserInRole("Admin")) { // display the page } else { // No }

I found a solution to my problem from this question Poor Performance with WindowsTokenRoleProvider

But there are a couple of differences 1. The above question uses WindowsTokenRoleProvider, I am using SqlRoleProvider

Because of the above problem, the above solution does not exactly work for me.

What I have done so far, and I am partially successful, I have derived a class from SqlRoleProvider and include this function which is the same from above question but modified. I changed web.config so that it looks like this

<roleManager enabled="true" cacheRolesInCookie="true" cookieName=".ASPR0L3S" cookieTimeout="117" cookieSlidingExpiration="true" cookieProtection="All" createPersistentCookie="false" defaultProvider="CustomSqlRoleProvider">
            <providers>
                <add name="CustomizedRoleProvider" type="CustomSqlRoleProvider" connectionStringName="PEGConn" applicationName="/CRM"/>
            </providers>
        </roleManager>

This is the function inside my class, which does get (executed only when a user login)

public override string[] GetRolesForUser(string username)
    {
        // Will contain the list of roles that the user is a member of
        List<string> roles = null;

        // Create unique cache key for the user
        string key = String.Concat(username, ":", base.ApplicationName);

        // Get cache for current session
        Cache cache = HttpContext.Current.Cache;

        // Obtain cached roles for the user
        if (cache[key] != null)
        {
            roles = new List<string>(cache[key] as string[]);
        }

        // Was the list of roles for the user in the cache?



        if (roles == null)
        {
            string[] AllRoles = GetAllRoles();
            roles = new List<string>();

            // For each system role, determine if the user is a member of that role
            foreach (String role in AllRoles)
            {
                if (base.IsUserInRole(username, role))
                {
                    roles.Add(role);
                }
            }

            // Cache the roles for 1 hour
            cache.Insert(key, roles.ToArray(), null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration);
        }

        // Return list of roles for the user
        return roles.ToArray();
    }

The problem is when Roles.IsUserInRole function calls the same old

System.Web.Security.Roles.IsUserInRole

function. I have even overloaded this function in my new class but it never gets executed. I am basically caching all the roles so that on each page refresh the application does not go search for all roles right from the start.

Do I need to derive another class from System.Web.Security.Roles.IsUserInRole? Has anyone done it.

Each page takes about 4-8 seconds on fresh which is too long. Code is in VS 2008, C# 3.5

Community
  • 1
  • 1
TheTechGuy
  • 16,560
  • 16
  • 115
  • 136
  • Are you sure that's the problem? Have you tried cutting out the role check and timing your application? – MNGwinn Jul 09 '12 at 21:24
  • 1
    Why are you calling base.IsUserInRole(username, role) explicitly? This will always call the base class implementation. If you've implmented IsUserInRole on your derived class, call that instead! Have you profiled your app to determine which method is taking the most time? – dash Jul 09 '12 at 21:27
  • 1
    Are you sure this is really the problem? I've got plenty of pages that check security and they don't take 4 - 8 seconds to load. This should be one database query, max. That shouldn't take 4 seconds. Before putting a lot of effort into this, I'd try commenting out your security check so you can be sure it's taking 0 time, and see how fast the page loads. – Jay Jul 09 '12 at 21:27
  • I did profile my app and all the time it was here that it took the most time. Disabling it makes it considerably fast. But i can double check. Did that a while ago. – TheTechGuy Jul 09 '12 at 21:29
  • @dash this is my question. On each page `using System.Web.Security;` is used, should I change it to my own custom class file name? This is where I need help. – TheTechGuy Jul 09 '12 at 21:31
  • If IsUserInRole is indeed the limiting factor (and you do seem to have identified it as such) then yes, you are going to have to override the implmentation of it. However, it's worth trying to find out *why* it is so slow; the SqlRoleProvider is usually pretty fast for a user query. Are all queries against the database the Roles db is hosted on slow, for example? Is the query making it to the database or is it taking a long time to connect? Lots to investigate! How many users do you have? How many roles? – dash Jul 09 '12 at 21:33
  • Well my App is not really fast, but this is one main culprit. I have about 15 roles. The problem is definitely there. There are lots of complains about roles being slow. I did not run SQL profiler so can't tell you that. But remove `IsUserInRole` speeds everything up. – TheTechGuy Jul 09 '12 at 21:35

5 Answers5

1

I think this may also be hiding a fundamental problem with your application design. You should subscribe to the DRY principle. Do not repeat yourself, i.e. don't repeat the same lookup/code on every page. I would suggest using session variables so that you can "Cache" these expensive Role lookups of yours. Here is a quick guide to using session variables:

http://msdn.microsoft.com/en-us/library/ms178581.aspx

On side note. I see you're using cookies to store these 'Roles' of yours. That doesn't sound very secure, hence I'll assume that security is not the main goal of this exercise.

Zoran Pavlovic
  • 1,166
  • 2
  • 23
  • 38
1

If Roles.IsUserInRole("Admin") is what takes time, you can check the role of the user once (upon logging in) and save the value is the session object.

const string IS_ADMIN_KEY; //this can be on a base class of page / master page

Session[IS_ADMIN_KEY] = Roles.IsUserInRole("Admin"); // do this when logging in

//Add this to page load
bool isAdmin = Session[IS_ADMIN_KEY]; 
if(isAdmin)) 
{ 
   // display the page 
} else 
{ 
   // don't display the page 
}
Avi Turner
  • 10,234
  • 7
  • 48
  • 75
  • I would advice against storing and retrieving the roles in the session in this manner. Especially if you use the default web controls for user creation and management. You can still store the results in the session but there would be no need to cache the results in a cookie if you do. I would implement a Role Provider that derived from the one you wish to introduce session caching on. There you would check the session, if the result is present return it, otherwise call the base, store the result in the session, and then return the result. – JG in SD Jul 10 '12 at 20:41
1
  1. I suggest you leverage the base class implementation of GetRolesForUser, rather than repeatedly calling IsUserInRole, which will access the database on each call

    public override string[] GetRolesForUser(string username)
    {
        // Create unique cache key for the user
        string key = String.Concat(username, ":", base.ApplicationName);
    
        string[] roles = HttpRuntime.Cache[key] as string[];
    
        if (roles != null)
        {
            roles = base.GetRolesForUser(username);
    
            HttpRuntine.Cache.Insert(key, roles.ToArray(), ...);
        }
    
    }
    
  2. I would also consider using a sliding expiration, whose value could be configurable and default to around 30 minutes - similar to the default RoleManagerSection.CookieTimeout). This could be in addition to your absolute expiration of 1 hour.

  3. Finally, you should override IsUserInRole to use your cache. It could be implemented as follows:

    public override bool IsUserInRole(string username, string roleName)
    {
        // Case-insensitive comparison for compatibility with RolePrincipal class
        return thisGetRolesForUser(username).Contains(roleName, 
                            StringComparer.OrdinalIgnoreCase);
    }
    
Joe
  • 122,218
  • 32
  • 205
  • 338
0

By default there are a couple of providers added to your application besides those your specify. Removing them from the list of providers could certainly help out your performance, as you'll only be using the provider(s) that you want.

<system.web>
  <membership defaultProvider="MyMembershipProvider">
    <providers>
      <clear />
      <add name="MyMembershipProvider" type="MyMembershipProvider" applicationName="My App" />
    </providers>
  </membership>
  <roleManager defaultProvider="MyRoleProvider" enabled="true" cacheRolesInCookie="true" cookieTimeout="60">
    <providers>
      <clear />
      <add name="MyRoleProvider" type="MyRoleProvider" applicationName="My App" />
    </providers>
  </roleManager>
</system.web>

Also it is my understanding if you want to cache the results in a cookie you don't need to implement anything extra in your provider. The setting of the cacheRolesInCookie attribute to True directs .NET to handle the caching for you. As long as you are calling into the role provider using the System.Web.Security.Roles, .NET will handle the caching and putting the roles in the cookie.

I would try:

<system.web>
  <roleManager enabled="true" cacheRolesInCookie="true" cookieName=".ASPR0L3S" cookieTimeout="117" cookieSlidingExpiration="true" cookieProtection="All" createPersistentCookie="false" defaultProvider="SqlRoleProvider">
    <providers>
      <clear />
      <add name="SqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="PEGConn" applicationName="/CRM"/>
    </providers>
  </roleManager>
</system.web>
JG in SD
  • 5,427
  • 3
  • 34
  • 46
0

First, you're doing it wrong. You shouldn't be placing anything in Page_Load to do with this. You should instead be using the authorization keys web.config if your roles are static, if they're dynamic you should be doing this in the global.asax Application_AuthorizeRequest event.

This will happy much further up the page lifecycle, making it more secure and less wasteful of resources.

Second, you need to investigate the reason why this is slow. You can get around it by caching roles in a cookie (JG mentions how to do this), But in reality, look into the database itself. Role lookups should not take long. Execute the stored procedures that back the role lookup (they're in the database) and see if it is slow executing that.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291