Slowness with Custom Properties on .NET Security Provider

If you ever used the standard .NET Security Provider with Sitecore, you may love how easy it is to create and use Custom Profile Properties, where data can be easily saved at user profiles. But a huge issue can emerge if you attempt to retrieve users at your database by one of these custom properties.

The Problem

Let’s say you have a custom property called Document ID, and you wish for some reason to retrieve the user that has a certain number on it – for instance, if your users can login to your website both using their Logins or Document IDs – then you may have something like this at your code:

var userFound = UserManager.GetUsers().FirstOrDefault(f => f.Profile["Document ID"] == inputDocumentId);

This simple code can bring you a big headache, because of the way .NET Security Provider builds the SQL Query responsible for executing your LINQ expression. Since all Custom Properties are stored as meta data, it is simply not directly queriable. Then what Security Provider does is one SELECT query for each user at your database, deserializing the Custom Properties on memory so it can be compared to the user input.

If you have few users at your database, which is usually the case when you are in development phase, you’ll probably not notice any issue. But after your go-live, as your user base starts growing, it will gradually get slower and slower. At the project that inspired this blog post, we had a sudden data load of more than 20k users, and as a consequence the system got impracticably slow overnight.

The Solution

One of the possible technical solutions, the one we used at the project in question, was to extend the table aspnet_Users at our Core database. That is the main table used by .NET Security Provider to store users. What we do is to create a new column Document ID, where this data will be stored in a clean format:

aspnet_Users

After that, we need to attach some code to both “user:created” and “user:updated” pipelines. This code will be responsible for updating the Document ID column when users are created or updated.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
 <sitecore>
 <events>
 <event name="user:created">
 <handler type="MyProject.Pipelines.UpdateUserProfileCache, MyProject" method="OnUserCreatedUpdated" />
 </event>
 <event name="user:updated">
 <handler type="MyProject.Pipelines.UpdateUserProfileCache, MyProject" method="OnUserCreatedUpdated" />
 </event>
 </events>
 </sitecore>
</configuration>

Then your code will probably look like the following. The class CustomProfileIndexManager, responsible for doing the actual Update and Select at the database is not there, but you can easily guess how to build yours.

namespace MyProject.Pipelines 
{
 public class UpdateUserProfileCache
 {
   public void OnUserCreatedUpdated(object sender, EventArgs args)
   {
     var scArgs = (SitecoreEventArgs)args;
     if (!scArgs.Parameters.Any())
       return;

     var user = (System.Web.Security.MembershipUser)scArgs.Parameters.First();

     // Will only act for "extranet" users
     var username = user.UserName;
     if (!username.StartsWith("extranet"))
       return;

     var scUser = Sitecore.Security.Accounts.User.FromName(username,false);

     // Following method is responsible for saving "Document ID" 
     // into the respective column at the database
     CustomProfileIndexManager.SaveUserToCache(scUser);
   }
 }
}

So your problematic code would be replaced by something like this:

// Will do a simple "SELECT * FROM aspnet_Users WHERE Document ID = 'xxxx'" 
var userFound = CustomProfileIndexManager.GetUserByDocumentId(inputDocumentId);

 

What about the existent base of users?

Of course that will only cover users that are new or updated. If you already have a certain base of users, you can build a simple script to “touch” all your users, such as this:

 var startProcess = DateTime.Now;
 Response.Write("<font color="red">Loading users...</font>");
 Response.Flush();

 // Get all users
 var users = DomainManager.GetDomain("extranet").GetUsers();
 Response.Write(string.Format("OK! ({0}ms)<hr>", DateTime.Now.Subtract(startProcess).TotalMilliseconds));
 Response.Flush();

 // Loop into all users
 var counter = 0;
 foreach (var user in users)
 {
   var startUserProcess = DateTime.Now;

   counter++;
   if ((counter % 10) == 0)
   {
     Response.Write(string.Format("--- Total time: {0} minutes<br>", DateTime.Now.Subtract(startProcess).TotalMinutes));
     Response.Flush();
   }
   Response.Write(string.Format("User #{0} - Email: {1} - Processing...", counter, user.Profile.Email));

   // Following method is responsible for saving "Document ID" 
   // into the respective column at the database
   CustomProfileIndexManager.SaveUserToCache(user);

   Response.Write(string.Format(" OK! ({0}ms)<br/>", DateTime.Now.Subtract(startUserProcess).TotalMilliseconds));
 }
 Response.Write(string.Format("<h3>TOTAL TIME: {0} minutes</h3>", DateTime.Now.Subtract(startProcess).TotalMinutes));
 Response.Flush();
 Response.End();

 

Publicado em Development, Security Provider
Um comentário sobre “Slowness with Custom Properties on .NET Security Provider
  1. Leonardo Cunha says:

    That was useful!

    Thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

  Am Not Spammer

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>