I could imagine you have some Virtual Roles in your Episerver.Framework configuration. If you try removing these, how would that work?
I think you're using Default Role Provider (System.Web.Providers.DefaultRoleProvider) in your EPiServer site and your implementation makes too many call to Principal.IsInRole() method. This method is not cached with Default Role Provider and it will hit database everytime a call is made.
We ran into the same problem with one of our customer sometime ago and we solved it with a custom implementation of role provider. Basically you can inherit your role provider from System.Web.Providers.DefaultRoleProvider and just override a couple of methods to provide caching mechanism. The caching should be implemented using EPiServer Synchronized Object Cache to make sure they will work on load balanced setup. The cache entry need to be cleared everytime an user is added or removed from a role.
This is the sample implementation of Role Provider with caching:
/// <summary> /// This is the custom role provider for [your project] to allow caching of user's roles. /// [your project] site use too many call to user.IsInRole() and this call is not cached. /// This problem result in too many calls to database and it exceeds the DTU quota. /// </summary> /// <seealso cref="System.Web.Providers.DefaultRoleProvider" /> /// <inheritdoc cref="DefaultRoleProvider"/> public class MyCustomRoleProvider : DefaultRoleProvider { /// <summary> /// The cache handler instance /// </summary> private readonly ICacheHandler _cacheHandler = ServiceLocator.Current.GetInstance<ICacheHandler>(); /// <summary> /// The user role cache key format (store roles for a single user). /// </summary> private const string UserRoleCacheKey = "MyCustom_Roles_Cache_User:{0}"; /// <summary> /// The role users cache key format (store users in a single role). /// </summary> private const string RoleUserCacheKey = "MyCustom_Users_Cache_Role:{0}"; /// <summary> Gets the roles for a user </summary> public override string[] GetRolesForUser(string username) { var cacheKey = string.Format(UserRoleCacheKey, username); if (!(_cacheHandler.Get(cacheKey) is string[] roles)) { roles = base.GetRolesForUser(username); // This cache entry never expire unless user is added or removed from a role _cacheHandler.Insert(cacheKey, roles, CacheEvictionPolicy.Empty); } return roles; } /// <summary> Gets all the users in a given role </summary> public override string[] GetUsersInRole(string roleName) { var cacheKey = string.Format(RoleUserCacheKey, roleName); if (!(_cacheHandler.Get(cacheKey) is string[] users)) { users = base.GetUsersInRole(roleName); // This cache entry never expire unless user is added or removed from a role _cacheHandler.Insert(cacheKey, users, CacheEvictionPolicy.Empty); } return users; } /// <summary> Adds all of the usernames to all of the roleNames </summary> public override void AddUsersToRoles(string[] usernames, string[] roleNames) { base.AddUsersToRoles(usernames, roleNames); // Invalidate cache keys foreach (var username in usernames) { var cacheKey = string.Format(UserRoleCacheKey, username); _cacheHandler.Remove(cacheKey); } foreach (var roleName in roleNames) { var roleUsersCacheKey = string.Format(RoleUserCacheKey, roleName); _cacheHandler.Remove(roleUsersCacheKey); } } /// <summary> Checks if a user is in a role </summary> public override bool IsUserInRole(string username, string roleName) { var roles = GetRolesForUser(username); if (roles.Any(t => string.Equals(t, roleName))) return true; return false; } /// <summary> Removes usernames from rolenames </summary> public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) { base.RemoveUsersFromRoles(usernames, roleNames); // Invalidate cache keys foreach (var username in usernames) { var cacheKey = string.Format(UserRoleCacheKey, username); _cacheHandler.Remove(cacheKey); } foreach (var roleName in roleNames) { var roleUsersCacheKey = string.Format(RoleUserCacheKey, roleName); _cacheHandler.Remove(roleUsersCacheKey); } } }
Then, you just need to register it in web.config
<roleManager enabled="true" defaultProvider="MyCustomRoleProvider" cacheRolesInCookie="true"> <providers> <clear /> <add name="MyCustomRoleProvider" type="[full path to your role provider class], [your assembly name]" connectionStringName="EPiServerDB" applicationName="/" /> </providers> </roleManager>
Please let us know if this is helpful.
We are seeing hundreds of calls within seconds of each other from EpiServer CMS looking for a user's role name. Here is the query we see in Profiler.
exec sp_executesql N'SELECT
[Project1].[RoleName] AS [RoleName]
FROM ( SELECT
[Extent1].[RoleName] AS [RoleName]
FROM [dbo].[Roles] AS [Extent1]
INNER JOIN [dbo].[UsersInRoles] AS [Extent2] ON [Extent1].[RoleId] = [Extent2].[RoleId]
INNER JOIN [dbo].[Users] AS [Extent3] ON [Extent2].[UserId] = [Extent3].[UserId]
INNER JOIN [dbo].[Applications] AS [Extent4] ON [Extent1].[ApplicationId] = [Extent4].[ApplicationId]
WHERE (((LOWER([Extent4].[ApplicationName])) = (LOWER(@p__linq__0))) OR ((LOWER([Extent4].[ApplicationName]) IS NULL) AND (LOWER(@p__linq__0) IS NULL))) AND (((LOWER(@p__linq__1)) = (LOWER([Extent3].[UserName]))) OR ((LOWER(@p__linq__1) IS NULL) AND (LOWER([Extent3].[UserName]) IS NULL)))
) AS [Project1]
ORDER BY [Project1].[RoleName] ASC',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',@p__linq__0=N'/',@p__linq__1=N'UserName'
The calls start out a 3 milliseconds and then start taking 500+ milliseconds.
Can anyone tell me why there are so many calls to get a users's role name?
Thanks,
Mark