Output Caching

Requires Spark version 1.1

Caching part of a template

Output from a Spark template may be cached by wrapping it in a <cache> element. Let's assuming you have a ShowConversionTable macro that does some expensive data acquisition through a service class.

<viewdata Conversion="IConversionRateService"/>
<h1>A cached section</h1>
<p>Current conversion rate ${Conversion.CurrentRate}</p>
!{ShowConversionTable(Conversion)}

You could cache the results of this html output by wrapping the section.

<viewdata Conversion="IConversionRateService"/>
<h1>A cached section</h1>
<cache>
  <p>Current conversion rate ${Conversion.CurrentRate}</p>
  !{ShowConversionTable(Conversion)}
</cache>

The second and subsequent times this template renders it will reuse the output from the first time it ran. This output would be stored in the ASP.NET cache the first time the cached segment had run.

Note! It's generally considered a bad practice for the view to acquire the data or model which it is exposing. In further examples below we will see a way to keep data acquisition in your controller's action in a way that caching will still avoid the data acquisition costs.

Caching that varies by parameters

Frequently you'll want to cache things which don't show the same data on every request, or don't appear the same to all users. For example, let's say our conversion table has a parameter to determine the currency to use as the baseline denomination. In that case you would provide the additional key values that would make the output to cache unique.

<viewdata 
    Conversion="IConversionRateService" 
    Baseline="string"/>
<h1>A cached section</h1>
<cache key="Baseline">
  <p>Current rate over Euro ${Conversion.GetCurrentRate(Baseline, "EUR")}</p>
  !{ShowConversionTable(Conversion, Baseline)}
</cache>

Several parameters may be delimited by commas. This works because attribute value is used as a params object[] keys argument, so if commas are present it create a multiple-field key for the cache entry.

<viewdata 
    Conversion="IConversionRateService" 
    Baseline="string"/>
<h1>A cached section</h1>
<cache key="Baseline, CurrentUserProfile.CountryOfOrigin.Currency">
  <var userCurrency="CurrentUserProfile.CountryOfOrigin.Currency"/>
  <p>${Baseline} rate over ${userCurrency} ${Conversion.GetCurrentRate(Baseline, userCurrency)}</p>
  !{ShowConversionTable(Conversion, Baseline)}
</cache>

Giving cache a specific time-to-live

In the first example the same information could be returned for the life of the web host's app domain. In a lot of cases there's a certain point where you would consider the age of the data to be acceptable. To ensure the same output will only be used for five minutes from when it was originally capured you would provide an expires attribute with a DateTime coordinate, preferable in Utc.

<viewdata 
    Conversion="IConversionRateService" 
    Baseline="string"/>
<h1>A cached section</h1>
<cache key="Baseline" expires="DateTime.UtcNow.AddMinutes(5)">
  <p>Current rate over Euro ${Conversion.GetCurrentRate(Baseline, "EUR")}</p>
  !{ShowConversionTable(Conversion, Baseline)}
</cache>

The expires attribute may be used with or without a key. In this example having both present would be correct.

Expiring when unused over time

You may have part of a view that isn't particularly time sensitive, but you don't want the entries to remain in memory when their key value hasn't been used for a while. That could be a case to consider using a TimeSpan instead of a DateTime for the expires parameter.

<h1>Product Details</h1>
<cache key="productId" expires="TimeSpan.FromMinutes(20)">
  <p>${product.Name}</p>
  <div class="productimage">${ShowProductImage(product.Id)}</div>
  <p>${product.Description}</p>
  <div class="orderhistory">
    ${ShowThirtyDayOrderingHistory(product.Id)}
  </div>
</cache>

For convenience a numeric value will be treated as the value of a TimeSpan in seconds.

<cache expires="600">
  <p>
    This will remain cached until you stop 
    using the page for ten minutes.
  </p>
</cache>

Note! This example is again implying the view is acquiring data. A preferred method would be to have this information represented by a model added to the ViewData by the controller's action. Next we will look at avoiding that problem.

Using ValueHolder for clean views

As often as possible you will want to ensure the controller's action is gathering all of the model information needed for the view to render itself. This causes a problem when you are attempting to utilize caching, because it's often the gathering of the model's data where the real cost occurs.

One way to ensure caching will avoid this cost is to have the view gather the data needed for the portion of the template which is cached. That way then a cache-hit occurs you also avoid the cost of making WCF or database calls to acquire the data.

A better way however would be to take advantage of a class like the ValueHolder provided by the spark.dll assembly. It's a class with a generic typed Value property, and the constructor accepts a lambda expression which will be called at most once the first time the value is used. The lambda may also reference local variables and action arguments even though it's technically being called when the view result is executing.

public ActionResult Details(int id)
{
    var data = new NorthwindDataContext();
 
    ViewData["employeeId"] = id;
 
    ViewData["employee"] = ValueHolder.For(
        () => data.Employees.Single(x => x.EmployeeID == id));
 
    return View();
}

One of the ways you could use this in a view is like this. Because the value property is only used in a cached section, the database not called if the same employee is viewed within three minutes.

<viewdata 
    employeeId="int" 
    employee="Spark.ValueHolder[[Employee]]"/>
<cache key="employeeId" expires="180">
  <p>${employee.Value.Name}</p>
  !{ShowSalesHistory(employee.Value)}
</cache>

There's also an interesting trick with the <viewdata> element you can use if you want to take advantage of Eval's ability to access properties directly. In this case it will add a public Employee employee { get } property which will return (Employee)ViewData.Eval("employee.Value");.

The net result is that you can use the property employee directly without adding the .Value onto it.

<viewdata 
    employeeId="int" 
    employee.Value="Employee employee"/>
<cache key="employeeId" expires="180">
  <p>${employee.Name}</p>
  !{ShowSalesHistory(employee)}
</cache>

You can also create a ValueHolder with a typed Key property, and use other class methods to acquire the data instead of a lambda expression.

public ActionResult Details(int id)
{
    ViewData["employee"] = ValueHolder.For<int, Employee>(id, FetchEmployee);
 
    return View();
}
 
private static Employee FetchEmployee(int employeeID)
{
    var data = new NorthwindDataContext();
    return data.Employees.Single(x => x.EmployeeID == employeeID);
}

And you can use this key property from the ValueHolder<TKey, TValue> class like this.

<viewdata employee="Spark.ValueHolder[[int, Employee]]"/>
<cache key="employee.Key" expires="180">
  <p>${employee.Value.Name}</p>
  !{ShowSalesHistory(employee.Value)}
</cache>

Or you could use the Eval trick again like this.

<viewdata 
    employee.Key="int employeeId"
    employee.Value="Employee employee"/>
<cache key="employeeId" expires="180">
  <p>${employee.Name}</p>
  !{ShowSalesHistory(employee)}
</cache>

Signaling dependent data changed

Expiring the cached output when it reaches a certain age, or when unused for a certain period, always involves a trade-off. With a shorted expiration you're losing cache efficiency, because it's being re-generated more frequently, and with longer expiration you're accepting the fact the web site will return information that's out of date for a certain period. That can be especially noticeable if your web site is on a server farm, because the different nodes may return different cached results if you refresh a page repeatedly.

To have ultimate control over your cached data you may provide an ICacheSignal that will fire an event when the data changes. Say you have a component that has a RegionSalesBuilder.GetFigures(int regionId) method, and you also implement a RegionSalesWatcher.GetSignalForRegion(int regionId) that will return an ICacheSignal.

public ActionResult SalesFigures(int id)
{
  var worker = new RegionSalesBuilder();
  var watcher = new RegionSalesWatcher();
  return View(new {
    regionId = id,
    figures = ValueHolder.For<int, RegionSales>(id, worker.GetFigures),
    salesChanged = watcher.GetSignalForRegion(id),
    });    
}

<viewdata 
  regionId="int" 
  figures="ValueHolder[[RegionSales]]" 
  salesChanged="ICacheSignal"/>
<cache key="regionId" signal="salesChanged">
  All of the ${figures.Value.Stuff} shown here
</cache>

The ICacheSignal contains only a single event named Changed, and you may implement that interface in any way you would like. For example you can use a single instance of that object for your entire database, or you could have a distinct instance for each piece of data you want to expire individually.

There is also a CacheSignal class you may use which implements ICacheSignal. It has a public method named FireChanged you may call at any time to expire all cached output which used that instance as a signal.

public class RegionSalesWatcher
{
  static CacheSignal _allDataSignal = new CacheSignal();
 
  ICacheSignal GetSignalForRegion(int regionId)
  {
    return _allDataSignal;
  }
 
  public static void AppStarted()
  {
    //obvious pseudo-code
    var conn = opendatabase();
    conn.selecttableswithnotification(OnTablesChanged);
  }
 
  static void OnTablesChanged()
  {
    _allDataSignal.FireChanged();
  } 
}

Yet way of using CacheSignal would be as a base class. In this case you would override the Enabled and Disabled methods to build up and tear down the change watching mechanism, and call the base FireChanged when a change occurs.

public class FileWatcherSignal : ChangeSignal
{
  string _path;
  public FileWatcherSignal(string path)
  {
    _path = path;
  }
 
  protected override void Enable()
  { 
     pseudo_code_start_file_watcher(FileWatcher_ChangedEventHandler);
  }
 
  protected override void Disable()
  { 
     pseudo_code_stop_file_watcher();
  }
 
  void FileWatcher_ChangedEventHandler()
  {
    FireChanged();
  }
}

Although Spark provides support for signaling cache expiration, and the ICacheSignal interface and CacheSignal class, there are no built-in implementations of file or database change notifiers. The assumption is that databases and their access libraries are so varied there's a vanishingly small change canned implementations would be useful.

Or how does this sound... Send me a patch and I'll add them. :)

Using cache with xml attributes

The <cache> element can be implied by adding certain attributes to other elements.

<div cache.key="employeeId" 
     cache.expires="TimeSpan.FromMinutes(20)" 
     cache.signal="MyApp.Signals.Employees">
  This div element is cached.
</div>

The attribute cache="xxx" is an alias for cache.key="xxx"

<div cache="employeeId" cache.expires="180">
  <p>This has a 3 minute sliding expiration per employee id</p>
  <p>${employeeId}</p>
</div>