One of the challenges of any old Web Forms application is that it encouraged you to overlap concerns and abstractions. For example in old web form code you might be collecting data and also checking the thread for HttpContext details, all within the same method, making it almost impossible to reuse that code anywhere else.

This was certainly the case for the caching mechanism employed bythe RSS feed of DasBlog, I found that the data collection and the Caching were so intertwined that neither were useful or reusable outside of a Web Forms context. What that forced me to do was separate those concerns by pushing the code for the data collection behind a repository pattern, simultaneously it allowed me take advantage of some rather straightforward middleware caching at the Controller available in ASP.NET Core.

ASP.NET Core In-Memory Cache

I started by installing the caching extension called Microsoft.Extensions.Caching.Memory from Nuget and as we have come to expect the middleware was enabled with a single line in Startup.cs code:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMemoryCache();
        services.AddMvc();
    }
}

This allows my class, FeedController, to use dependency injection to pull in a reference to IMemoryCache and I assign that to a private member variable for use in any of my Actions (ISubscriptionRepository is simply where I am pulling data from):

[Produces("text/xml")]
[Route("feed")]
public class FeedController : Controller
{
    private IMemoryCache _cache;
    private ISubscriptionRepository _subscriptionRepository;
    private const string RSS_CACHE_KEY = "RSS_CACHE_KEY";

    public FeedController(ISubscriptionRepository subscriptionRepository, IMemoryCache memoryCache)
    {
        _subscriptionRepository = subscriptionRepository;
        _cache = memoryCache;
    }
}

So now we have the Memory Cache but in order to actually cache an object in memory for, let's say, 5 minutes we must define a key to both create and subsequently retrieve the cache object. In the following example then, the object last for 5 minutes, any request that comes in after that 5 minutes will be forced to retrieve and refresh the cache.

[HttpGet]
[Route("")]
[Route("rss")]
public IActionResult Rss()
{
    RssRoot rss = null;
    if (!_cache.TryGetValue(RSS_CACHE_KEY, out rss))
    {
        rss = _subscriptionRepository.GetRss();

        var cacheEntryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(5));

        _cache.Set(RSS_CACHE_KEY, rss, cacheEntryOptions);
     }
     return Ok(rss);
}

My Rss Action simply checks the cache based on a key (in this case RSS_CACHE_KEY), if I am successful retrieving the object I simply return it. However, if the object does not exist (new startup or cache has been released) I then am forced to retrieve the data from my repository layer and cache it. In the above example I set a sliding expiration time of 5 minutes to ensure that the data is not too stale. One interesting side note is that if your site is under memory pressure you could lose your cache without realizing it, forcing you to go back to your data source every time. You can circumvent this useful behavior by changing the cache priority to CacheItemPriority.NeverRemove.

That was fairly straightforward, caching is now removed from data retrieval and caching is conveniently, and correctly tied to the controllers.

As an aside one of the things that was not obvious to me is that HTTP responses in XML format are not enabled by default in ASP.NET Core. To actually produce an XML (RSS) response from any of your controller actions you need to explicitly add the middleware to support it. I used the Formatters.Xml nuget package as follows:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddXmlSerializerFormatters();
        services.AddMemoryCache();
        services.AddMvc();
    }
}

That little gem had me scratching my head for a while!