Home General

Async flavors of `Util.CacheAsync` and `Util.Cache` can throw `RuntimeBinderException`

This issue seems to be very similar to this one from last year.

I've run into a case where Util.Cache and Util.CacheAsync will throw RuntimeBinderExceptions if the query is modified at all between runs, with the message "'object' does not contain a definition for 'Task'".

The issue seems to crop up when I'm attempting to cache a generic dictionary that has a non-built in type as the value type.

Reproduction:

async Task Main()
{
    var val = await Util.CacheAsync(async () => await GetDictFoo(), "cache_key", out bool cacheHit);

    // issue does occur if using the method to allow 'faulted task' caches, too
    //var val = Util.Cache(async () => await GetDictFoo(), "cache_key", out bool cacheHit);

    // issue does not seem to occur with built in types
    //var val = await Util.CacheAsync(async () => await GetDictString(), "cache_key", out bool cacheHit);

    (cacheHit ? "hit" : "miss").Dump();

    // add any characters to this comment and re-run:  
}

[Serializable]
class Foo { }

async Task<Dictionary<string, Foo>> GetDictFoo() => new();
async Task<Dictionary<string, string>> GetDictString() => new();

The first run will print "miss", and each subsequent run will print "hit". Until the query is modified, when the exception will be thrown. (Any change that gets it to recompile is enough, just adding or removing a character in a comment will get it done.

If you comment out the first and uncomment the second example there, and Kill Process and Execute, the same behavior is reproducible with the Util.Cache method as it was with Util.CacheAsync.

Switching to the last example, the behavior does not occur when the type being returned only has built-in types. It will continue to print "hit" as expected.

I can't see the link between what causes the Dictionary<string, string> to be happy, while Dictionary<string, Foo> doesn't want to play ball.

Thanks as always Joe! Hope you're well.

Comments

  • This occurs because I've not written a cache converter for dictionaries - it's on the TODO list.

  • Has this been updated? I'm getting the same error but for simple records.

    async Task Main()
    {
        var id = 1;
        var response = await Util.CacheAsync(async () => await GetResponseAsync(id), $"{id}");
        response.Dump();
    
        async Task<Info?> GetResponseAsync(long id)
        {
            await Task.Delay(1000);
            return new Info(id, "test");
        }
    }
    
    [Serializable]
    record Info(long Id, string Name);
    
  • edited March 23

    This one still fails (execution result is inconsistent, restart of LINQPad is required):

    using System.Threading.Tasks;
    
    async Task Main()
    {
        var val = await Util.CacheAsync(async () => await GetDictFoo(), "cache_key", out bool cacheHit);
    
        // issue does occur if using the method to allow 'faulted task' caches, too
        //var val = Util.Cache(async () => await GetDictFoo(), "cache_key", out bool cacheHit);
    
        // issue does not seem to occur with built in types
        //var val = await Util.CacheAsync(async () => await GetDictString(), "cache_key", out bool cacheHit);
    
        (cacheHit ? "hit" : "miss").Dump();
    
        // add any characters to this comment and re-run:  
    }
    
    [Serializable]
    class Foo { }
    
    async Task<Dictionary<string, Foo>> GetDictFoo() => new();
    async Task<Dictionary<string, string>> GetDictString() => new();
    
  • edited March 23

    @i2van said:
    No error on 8.8.8.

    You have to run it, change the code somewhere (e.g., rename a variable, add a comment, etc.) then run once more. The error only occurs when there's a cache hit after a code change. Also on 8.8.8.

  • This is still on the TODO list - a cache converter hasn't yet been written for dictionaries (and most collection types other than lists and arrays). Handling the type parameters correctly is quite a lot of work.

Sign In or Register to comment.