Home

Unexpected LINQPad cache behavior when runtime type doesn't match compile time type

I have some long running queries I'm trying to utilize LINQPad's caching mechanisms with, but LINQPad is often crashing unexpectedly or ignoring the cache all together.

In this script, it's querying web apis (YouTube in my case) for some data and caching the responses. However when I update my script for some changes I need to make and rerun. I would always get an UncacheableObjectException and need to rerun everything.

I tried to reproduce the problem and came up with the below script:

http://share.linqpad.net/gmjaev.linq

//#load "FooBar"

void Main()
{
    var cache = Util.Cache(() => GetFooBar());
    //cache = cache;
    cache.Dump();

    FooBar GetFooBar()
    {
        var returnValue = new FooBar();
        Util.HorizontalRun(true, "recalculating cache:", returnValue).Dump();
        return returnValue;
    }
    Foo GetFoo()
    {
        var returnValue = new FooBar();
        Util.HorizontalRun(true, "recalculating cache:", returnValue).Dump();
        return returnValue;
    }
    FooBar GetFooBar2()
    {
        var returnValue = new BooBar();
        Util.HorizontalRun(true, "recalculating cache:", returnValue).Dump();
        return returnValue;
    }
}

// Also contents of "FooBar.linq"
[Serializable]
public abstract record Foo();
[Serializable]
public record FooBar() : Foo();
[Serializable]
public record BooBar() : FooBar();

If you change the cache line to use the different GetFoo*() methods, then run, then make a trivial change (toggle line 6), you'll see whether or not it's recalculating or not. With GetFooBar() it will work as expected. GetFoo() and GetFooBar2() will always recalculate. And it doesn't seem to matter if the types are all defined in the script or in a #loaded script.

However in my script, I tried working around the caching error and tried caching differently (a dictionary) and still the same problem. However with the repro script, that triggers the UncacheableObjectException. Changing the cache lines to this:

    var cache = Util.Cache(() => new Dictionary<string, Foo>());
    cache["foobar"] = GetFooBar(); // comment out and rerun

Comments

  • edited July 2023

    In my actual script, the cache lines are async and I was assuming it was the Task that was causing the problem.

        async Task<ContentInfo> GetVideoInfoAsync(string videoId)
        {
            return await Util.Cache(async () => await client.GetYouTubeVideoInfoAsync(videoId), videoId);
        }
    

    The types are:

    public async Task<ContentInfo> GetYouTubeVideoInfoAsync(string videoId, CancellationToken cancellationToken = default);
    
    [Serializable]
    public abstract record ContentInfo(string VideoId, string _type);
    [Serializable]
    public record ErrorInfo(string VideoId, string Message) : ContentInfo(VideoId, "error");
    [Serializable]
    public record VideoInfo(
       ...
    );
    

    Also tried switching things around to make it so I can keep the runtime and compile time types the same but still the same problem.

        async Task<ContentInfo> GetVideoInfoAsync(string videoId)
        {
            try
            {
                return await Util.Cache(async () =>
                {
                    var result = await client.GetYouTubeVideoInfoAsync(videoId);
                    return result switch
                    {
                        VideoInfo v => v,
                        ErrorInfo e => throw new YouTubeErrorException(e),
                    };
                });
            }
            catch (YouTubeErrorException ex)
            {
                return ex.ErrorInfo;
            }
            catch (Exception ex)
            {
                ex.Data[nameof(videoId)] = videoId;
                throw; 
            }
        }
    
    public class YouTubeErrorException : Exception
    {
        public YouTubeErrorException(ErrorInfo errorInfo) => ErrorInfo = errorInfo;
        public ErrorInfo ErrorInfo { get; }
    }
    

  • I've released a new beta that is more flexible with typing, and that also allows the caching of tasks. Note that I've not fully tested all scenarios for tasks whose result type is in the query. Let me know how you get on.

  • Not sure whether to bump or create a new topic, but it seems like this is is causing issue again. When you make a change to the script after the result is cached, certain values are not restored.

    void Main()
    {
        var result = Util.Cache(() => new Result(new IntResult(42)), "result", out var fromCache);
        result.Dump($"fromCache: {fromCache}");
    }
    
    [Serializable]
    record Result(InnerResult InnerResult);
    [Serializable]
    abstract record InnerResult(InnerType? Type);
    [Serializable]
    record IntResult(int Value) : InnerResult(InnerType.Int);
    
    enum InnerType
    {
        Unset,
        Int,
    }
    

    Run it once it will output what you expect, fromCache = false. Run again, same result, fromCache = true. Touch the file (add a space somewhere) and run, the Type value of the inner result is null, fromCache = true.



  • Thanks - this is in fact a different issue. I'll flag it for the 8.4.x builds, most likely next week.

  • This should now be fixed in 8.4.1.

Sign In or Register to comment.