Chase Florell
Chase Florell

Reputation: 47417

Need help profiling .NET caching extension method

I've got the following extension

Public Module CacheExtensions
    Sub New()
    End Sub

    Private sync As New Object()
    Public Const DefaultCacheExpiration As Integer = 1200 ''# 20 minutes

    <Extension()> 
    Public Function GetOrStore(Of T)(ByVal cache As Cache, ByVal key As String, ByVal generator As Func(Of T)) As T
        Return cache.GetOrStore(key, If(generator IsNot Nothing, generator(), Nothing), DefaultCacheExpiration)
    End Function

    <Extension()> 
    Public Function GetOrStore(Of T)(ByVal cache As Cache, ByVal key As String, ByVal generator As Func(Of T), ByVal expireInSeconds As Double) As T
        Return cache.GetOrStore(key, If(generator IsNot Nothing, generator(), Nothing), expireInSeconds)
    End Function

    <Extension()> 
    Public Function GetOrStore(Of T)(ByVal cache As Cache, ByVal key As String, ByVal obj As T) As T
        Return cache.GetOrStore(key, obj, DefaultCacheExpiration)
    End Function

    <Extension()> 
    Public Function GetOrStore(Of T)(ByVal cache As Cache, ByVal key As String, ByVal obj As T, ByVal expireInSeconds As Double) As T
        Dim result = cache(key)

        If result Is Nothing Then

            SyncLock sync
                If result Is Nothing Then
                    result = If(obj IsNot Nothing, obj, Nothing)
                    cache.Insert(key, result, Nothing, DateTime.Now.AddSeconds(expireInSeconds), cache.NoSlidingExpiration)
                End If
            End SyncLock
        End If

        Return DirectCast(result, T)

    End Function

End Module

From here, I'm using the extension is a TagService to get a list of tags

    Public Function GetTagNames() As List(Of String) Implements Domain.ITagService.GetTags
        ''# We're not using a dynamic Cache key because the list of TagNames
        ''# will persist across all users in all regions.
        Return HttpRuntime.Cache.GetOrStore(Of List(Of String))("TagNamesOnly",
                                                                Function() _TagRepository.Read().Select(Function(t) t.Name).OrderBy(Function(t) t).ToList())
    End Function

All of this is pretty much straight forward except when I put a breakpoint on _TagRepository.Read(). The problem is that it is getting called on every request, when I thought that it is only to be called when Result Is Nothing

Am I missing something here?

EDIT: and for you guys, here's the C# equivalent

public static class CacheExtensions
{

    private static object sync = new object();
    public const int DefaultCacheExpiration = 20;

    public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator ) {
        return cache.GetOrStore( key, generator != null ? generator() : default( T ), DefaultCacheExpiration );
    }

    public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator, double expireInMinutes ) {
        return cache.GetOrStore( key, generator != null ? generator() : default( T ), expireInMinutes );
    }

    public static T GetOrStore<T>( this Cache cache, string key, T obj ) {
        return cache.GetOrStore( key, obj, DefaultCacheExpiration );
    }

    public static T GetOrStore<T>( this Cache cache, string key, T obj, double expireInMinutes ) {
        var result = cache[key];

        if ( result == null ) {

            lock ( sync ) {
                if ( result == null ) {
                    result = obj != null ? obj : default( T );
                    cache.Insert( key, result, null, DateTime.Now.AddMinutes( expireInMinutes ), Cache.NoSlidingExpiration );
                }
            }
        }

        return (T)result;

    }

}

and the call

    return HttpRuntime.Cache.GetOrStore<List<string>>("TagNamesOnly", () => _TagRepository.Read().Select(t => t.Name).OrderBy(t => t).ToList());

Upvotes: 0

Views: 505

Answers (2)

Chris Haas
Chris Haas

Reputation: 55457

The problem is that you're calling an overloaded method that's invoking your method and as @Simon Svensson pointed out the method that you're calling isn't the method that you posted. This is the C# version of the method that you're actually calling:

return cache.GetOrStore( key, generator != null ? generator() : default( T ), DefaultCacheExpiration )

Right away you should see that the second argument calls generator() if its passed a non-null value.

UPDATED

The problem isn't anything to do with caching, its that the overloaded method that you're calling is executing your lambda method. If you're really set on calling the method as you outlined you'll have to change it to something like this:

''# Fix StackOverflow Code Coloring Bug     
<Extension()>
Public Function GetOrStore(Of T)(ByVal cache As Cache, ByVal key As String, ByVal generator As Func(Of T)) As T
    ''# Null value to pass to first call. We can not use Nothing because VB can not infer the overload type
    Dim NV As Object = Nothing
    ''# Call the primary method passing a null value
    Dim Ret = cache.GetOrStore(key, NV, DefaultCacheExpiration)
    ''# If that call returns nothing call our generator() method and then re-call the main method
    If (Ret Is Nothing) AndAlso (generator IsNot Nothing) Then
        Ret = cache.GetOrStore(key, generator(), DefaultCacheExpiration)
    End If
    Return Ret
End Function

I haven't tested this but it or something very close should get you what you want. This is also why I'm not a big fan of lambdas. The idea is great, but people tend to lump so many together and create code that's not really readable. I'd rather have 10 lines that I can read than 1 line that does the same thing.

Upvotes: 1

sisve
sisve

Reputation: 19791

The signature of GetOrStore, and it's implementation, contains no evaluation of the Func you're sending in. I dont really know what's happening at the moment (or if it's really working). It seems you're adding a Func into your cache.

public const int DefaultCacheExpiration = 20;
private static readonly Object SyncRoot = new Object();
public static T GetOrStore<T>(this Cache cache, String key, Func<T> itemGenerator, Double expireInSeconds = DefaultCacheExpiration) {
    var item = cache[key];
    if (item != null)
        return (T)item;

    lock (SyncRoot) {
        // Fetch a second time to check if anyone have
        // added it while we blocked waiting for the lock.
        item = cache[key];
        if (item != null)
            return (T)item;

        // Invoke the almighty itemGenerator to execute,
        // and generate, the item that should be inserted
        // into the cache.
        item = itemGenerator.Invoke();
        cache.Insert(key, item, null, DateTime.Now.AddSeconds(expireInSeconds), Cache.NoSlidingExpiration);
        return (T)item;
    }
}

public static T GetOrStore<T>(this Cache cache, String key, T newItem, Double expireInSeconds = DefaultCacheExpiration) {
    return cache.GetOrStore(key, () => newItem, expireInSeconds);
}

If this doesn't do it, look into your asp.net caching settings and amount of free memory on the server. The cache wont work in low-memory conditions.

Upvotes: 1

Related Questions