The Problem
I'm working on a web portal that loads a WebUser
object when a user logs in via EF. WebUser
has a non-trivial object graph and loading it via EF can take 2-3 seconds (optimizing that load time is a separate issue).
In order to improve perceived performance, I want to load the WebUser
as soon as the user logs on to the system, on a separate thread. However, my current attempt runs synchronously for reasons that I do not understand.
The Code
static private ConcurrentDictionary<string, WebUser> userCache =
new ConcurrentDictionary<string, WebUser>();
static public void CacheProfile(string userName)
{
if (!userCache.ContainsKey(userName))
{
logger.Debug("In CacheProfile() and there is no profile in cache");
Task bg = GetProfileAsync(userName);
logger.Debug("Done CacheProfile()");
}
}
static public async Task<WebUser> GetProfileAsync(string userName)
{
logger.Debug("GetProfileAsync for " + userName);
await currentlyLoading.NotInSet(userName); // See NOTE 1 below
if (userCache.ContainsKey(userName))
{
logger.Debug("GetProfileAsync from memory cache for " + userName);
return userCache[userName];
}
else
{
currentlyLoading.Add(userName);
logger.Debug("GetProfileAsync from DB for " + userName);
using (MembershipContext ctx = new MembershipContext())
{
ctx.Configuration.LazyLoadingEnabled = false;
ctx.Configuration.ProxyCreationEnabled = false;
ctx.Configuration.AutoDetectChangesEnabled = false;
var wu = GetProfileForUpdate_ExpensiveMethod(ctx, userName);
userCache[userName] = wu;
currentlyLoading.Remove(userName);
return wu;
}
}
}
NOTE 1: currentlyLoading
is a static instance of ConcurrentWaitUntil<T>
. The intent is to cause a second request for a given user's profile to block if the first request is still loading from the database. Perhaps there is a better way to accomplish this? Code:
public class ConcurrentWaitUntil<T>
{
private HashSet<T> set = new HashSet<T>();
private Dictionary<T, TaskCompletionSource<bool>> completions = new Dictionary<T, TaskCompletionSource<bool>>();
private object locker = new object();
public async Task NotInSet(T item)
{
TaskCompletionSource<bool> completion;
lock (locker)
{
if (!set.Contains(item)) return;
completion = new TaskCompletionSource<bool>();
completions.Add(item, completion);
}
await completion.Task;
}
public void Add(T item)
{
lock (locker)
{
set.Add(item);
}
}
public void Remove(T item)
{
lock (locker)
{
set.Remove(item);
TaskCompletionSource<bool> completion;
bool found = completions.TryGetValue(item, out completion);
if (found)
{
completions.Remove(item);
completion.SetResult(true); // This will allow NotInSet() to complete
}
}
}
}
The Question
Why does CacheProfile()
seem to wait until GetProfileAsync()
has completed?
SIDE NOTE: I know that the ConcurrentDictionary
does not scale well and that I should use ASP.Net's cache.