LINQ2Vodka
LINQ2Vodka

Reputation: 3036

LINQ driven cache items invalidation

I'm making an ASP.NET MVC project and i would like to implement business data caching at repository (LINQ2SQL) layer. Since entities are related to each other, i need to invalidate related ones when i'm invalidating some base entity. Say i have Blog/Post/Comments relation and when user makes a new comment then i need to invalidate cached Post entity since it has outdated TotalComments field. Sometimes there is as more complicated logic for invalidation other entities.
Well, i need to implement flexible invalidation mechanism for that purpose. What i've found before:
- SQL notifications service. It notifies the app each time table has changed. Since i'll have high-loaded application, changes on some tables gonna be very often. All cached comments to any post will drop each time a new comment is added.
- Caching LINQ(or SQL) queries. In this case the rendered query is placed to the cache using its hash as a key. Not bad but it will be impossible to drop "all comment entities having BlogPostId = deletedBlogPostId"

And now what my idea is. I wanna use LINQ queries over HttpRuntime.Cache items to find items to be deleted by their properties (e.g. in case of deleting a blog post i look for

cachedItem => cachedItem.GetType() == typeof(Comment) 
       && ((Comment)cachedItem).BlogPostId == deletedBlogPostId

to delete all related comments). But i can't find in google this approach is widely used. Isn't it a good way to manipulate with cached related entities? Query performance over 1M cached items is 600 ms on my notebook.
Thanks!

Upvotes: 1

Views: 508

Answers (2)

LINQ2Vodka
LINQ2Vodka

Reputation: 3036

Well, after a while i've made extension methods for accessing my cache:

public static class MyExtensions
{
    // temlplate for cache item key name
    private const string CacheKeyTemplate = "{0}_{1}";

    private static string GetCachePrefixByType(Type type)
    {
        // this is just sample, implement it any way you want
        switch (type.Name)
        {
            case "Blog": return "b"; break;
            case "BlogPost": return "bp"; break;
            case "Comment": return "c"; break;
            default: throw new NotImplementedException();
        }
    }

    // insert with key containing object type custom prefix and object id
    public static void Put(this Cache cache, object obj, long id)
    {
        cache.Insert(String.Format(CacheKeyTemplate, GetCachePrefixByType(obj.GetType()), id), obj);
    }

    // get by object type and id
    public static T Get<T>(this Cache cache, long id)
    {
        return (T)cache[String.Format(CacheKeyTemplate, GetCachePrefixByType(typeof(T)), id)];
    }

    // get objects by WHERE expression
    public static List<object> Get(this Cache cache, Func<object, bool> f)
    {
        return cache.Cast<DictionaryEntry>().Select(e => e.Value).Where(f).ToList();
    }

    // remove by object type and id
    public static void Remove<T>(this Cache cache, long id)
    {
        cache.Remove(String.Format(CacheKeyTemplate, GetCachePrefixByType(typeof(T)), id));
    }

    // remove cache items by WHERE expression against stored objects
    public static void Remove(this Cache cache, Func<object, bool> f)
    {
        foreach (string key in cache.Cast<DictionaryEntry>().Where(de => f.Invoke(de.Value)).Select(de => de.Key))
        {
            cache.Remove(key);
        }
    }
}

and this is my classes to test:

private class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }
}

private class BlogPost
{
    public int PostId { get; set; }
    public int BlogId { get; set; }
    public string Text { get; set; }
}

private class Comment
{
    public int PostId { get; set; }
    public int CommentId { get; set; }
    public string Text { get; set; }
}

and the test code itself:

    // a blog
    Blog blog = new Blog{ BlogId = 1, Name = "Jim" };
    // two blog posts
    BlogPost post1 = new BlogPost { PostId = 1, BlogId = 1, Text = "Aaaaaaaa" };
    BlogPost post2 = new BlogPost { PostId = 2, BlogId = 1, Text = "Bbbbbbbbbb" };
    // two comments to the 1st blog post
    Comment comment11 = new Comment { CommentId = 11, PostId = 1, Text = "qwerty" };
    Comment comment12 = new Comment { CommentId = 12, PostId = 1, Text = "asdfg" };
    // one comment to the 2nd blog post
    Comment comment21 = new Comment { CommentId = 21, PostId = 2, Text = "zxcvbn" };

    // put it all to cache
    HttpRuntime.Cache.Put(blog, blog.BlogId);
    HttpRuntime.Cache.Put(post1, post1.PostId);
    HttpRuntime.Cache.Put(post2, post2.PostId);
    HttpRuntime.Cache.Put(comment11, comment11.CommentId);
    HttpRuntime.Cache.Put(comment12, comment12.CommentId);
    HttpRuntime.Cache.Put(comment21, comment21.CommentId);

    // get post #2 by its id
    BlogPost testPost = HttpRuntime.Cache.Get<BlogPost>(2); // testPost.Text = "Bbbbbbbbbb"

    // get all comments for post #1
    IEnumerable<Comment> testComments = HttpRuntime.Cache.Get(
        x => (x is Comment) && ((Comment)x).PostId == 1).Cast<Comment>(); // comments 11 and 12 are in the list

    // remove comment 21
    HttpRuntime.Cache.Remove<Comment>(21);
    // test if it was removed
    comment21 = HttpRuntime.Cache.Get<Comment>(21); // null

    // remove anything having text property = "qwerty"
    HttpRuntime.Cache.Remove(x => x.GetType().GetProperty("Text") != null && ((dynamic)x).Text == "qwerty");
    // test if comment 11 it was removed
    comment11 = HttpRuntime.Cache.Get<Comment>(11); // null

    // but comment 12 should still exist
    comment12 = HttpRuntime.Cache.Get<Comment>(12); // it's there

    // remove anything from cache
    HttpRuntime.Cache.Remove(x => true);
    // cache items count should be zero
    int count = HttpRuntime.Cache.Count; // it is!

Upvotes: 0

Justin Helgerson
Justin Helgerson

Reputation: 25551

I would just call a cache clearing method in your controller for the corresponding object. As an example, if a user is editing an individual post, then in the controller method that handles that POST request you should clear the cache for that post.

Using SQL Server Notification Services seems backwards to me. Your server-side web application is the first point of entry for users; the database comes after. You know when you need to clear the cache in your MVC application, so why not clear the cache from there?

Edit:

One other option for storing your data in cache and having access to it via key (so you don't have to iterate over the entire cache collection):

HttpRuntime.Cache.Insert(string.Format("CommentsForPost-{0}", postId), value);

Where value is a List<Comment> and postId is the id of your post. This way you could easily look up your comment collection and your cache keys are dynamic. I've used this approach across many applications (though I would actually write it to be more generic for less code duplication).

Upvotes: 1

Related Questions