Dynamic Cache (OrchardCore.DynamicCache)

Purpose

Dynamic Cache allows you to cache sections of markup.
Each cached section of markup can contain other (child) cached sections of markup.

Cached sections can all have their own cache policies, which allows for finer configuration options than a page level cache would have.

Cached values are stored using the IDynamicCache service.
Its default implementation is based on IDistributedCache which is itself based on IMemoryCache.

Example

Layout (not cached)

  • Section A
  • Section A1 (varies by role)
  • Section A2
  • Section B
  • Section B1 (varies by query string)
  • Section B2

Rendering cached sections

When this page is rendered for the first time, all shapes will be evaluated. Any blocks of markup that have been identified as cacheable will be stored in the IDynamicCache service.

On subsequent requests, if a cacheable section has already been cached (and the cache entry is still valid) then it won't be processed (Processing event in the ShapeMetadata). The markup will be retrieved from the cache and returned as part of the response.

Invalidating cached sections

If a cached section is invalidated, the markup for the section will be regenerated on the next request, and then placed back into the cache for subsequent requests to take advantage of.

  • If its children are still cached, then their cached value will be used.
  • Invalidating a block will also invalidate all parent blocks.

For instance, if Section B2 is invalidated, Section B will also be invalidated. When the Layout is rendered, the Section B code will run again, as will Section B2, but the cached content of Section B1 will be reused.

Cached sections can define dependencies, which allows the cache to know when the cached value should be invalidated.

For example, if a cache section includes the body of a content item, you may want to automatically invalidate this cache section whenever that content item changes. You can do this by adding the dependencies contentitemid:{ContentItemId} to the cache section.

Cached sections can also be configured with a sliding expiration window, an absolute expiration window, or both.
If no expiration window is provided, a default sliding window of one minute will be used.
If both types of expiration windows are supplied, the sliding policy will be used, up to the maximum absolute time allowed by the absolute expiration window.

Well-known Cache dependencies

Here is a list of common cache dependency values that can be used to invalidate cache entries.

Dependency Description
contentitemid:{ContentItemId} Invalidated when a content item described with its unique id ({ContentItemId}) is Published, Unpublished or Removed.
alias:{Alias} Invalidated when a content item with a specific alias ({Alias}) is Published, Unpublished or Removed.

You can create your own dependencies by calling RemoveTagAsync() on ITagCache in response to events.

Varying cached sections (Contexts)

You may have a cached section that needs to be varied depending on the context of the request.
An example of this would be a header section that is included on every page on the site, but contains different markup for each user (e.g. a log in form, or the currently logged in user's username etc...').

You can do this by adding 'vary by' values (called contexts) to the cache policy of a cached section.

Adding a user context to the header example given above would create a unique cache item for each user that logs in to your site.

Contexts are hierarchical. For instance if a shape varies by user and user.roles contexts, only the user value will be used as it's more specialized than the user.roles one.

Contexts can be parameterized, for instance query:age will pick the age value of the query string.

Available Contexts

Context Description
features The list of enabled features.
features:{featureName} The specified feature name.
query The list of querystring values.
query:{queryName} The specified query name value.
user The current user.
user.roles The roles of the current user.
route The current request path.

You can create your own Contexts by implementing ICacheContextProvider.

Fallback Contexts

Sometimes you may want to vary by a known value that is not an available context.

For example: You may wish to cache all your blog posts so that you can quickly display lists of your posts throughout your site. If the cache ID for the cache block was blog-post, you can use a known value as a context to vary the cache item for each blog post. In this case, you could use the Content Item ID as a context:

{% cache "blog-post-summary", vary_by: Model.ContentItem.ContentItemId %}
    ...
{% endcache %}

Usage

Cached sections can be configured to encompass a shape, or they can be explicitly added to markup with the cache liquid block, or the cache razor tag helper:

Caching a shape

ShapeMetadata.Cache(string cacheId)

When called on a shape instance, marks the shape as being cached. Returns a CacheContext object.

Example: myShape.Cache("myshape")

CacheContext members

Method Description
WithDuration(Timespan) Cache the shape for the specified amount of time.
WithSlidingExpiration(Timespan) Cache the shape for a specific amount of time with a sliding window.
AddContext(params string[]) Varies the cached content on the specified context values.
RemoveContext(string) Removes the specified context.
AddDependency(params string[]) Defines the context values that will invalidate the cache entry.
RemoveDependency(string) Removes the specified dependency.
AddTag(string) Adds a tag to the cache entry to that it can be invalidated by this tag value.
RemoveTag(string) Removes the specified tag.

Note

AddDependency differs from AddContext in that it doesn't store multiple values for each context, but invalidates the cached shape content when the value of the context varies. Internally they share the same implementation, as the physical cache key will contain the dependency context value.

Shape Tag Helper Attributes

When using shape tag helpers, the following attributes can be used:

Razor Attribute Liquid Attribute Description Required
cache-id cache_id The identifier of the cached shape. Yes
cache-context cache_context A set of space/comma-separated context values. No
cache-dependency cache_dependency A set of space/comma-separated dependency values. No
cache-tag cache_tag A set of space/comma-separated tag values. No
cache-fixed-duration cache_fixed_duration The cache duration of the entry, e.g. "00:05:00" for 5 minutes. No
cache-sliding-duration cache_sliding_duration The sliding cache duration of the entry, e.g. "00:05:00" for 5 minutes. No

For example, to cache the menu shape in a liquid template, you would use this markup:

{% shape "menu", alias: "alias:main-menu", cache_id: "main-menu", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu" %}

To cache a content item shape in a liquid template, you could use this markup:

{% contentitem alias: "alias:main-menu", cache_id: "main-menu", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu" %}

Liquid cache block

The liquid cache block can be used to cache sections of markup. cache blocks can be nested.

Arguments

Liquid Attribute Description Required
id The identifier of the cached shape. Yes (this is the default first argument --- no need to explicitly specify the name of this argument.)
contexts A set of space/comma-separated context values. No
dependencies A set of space/comma-separated dependency values. No
expires_after The cache duration of the entry, e.g. "00:05:00" for 5 minutes. No
expires_sliding The sliding cache duration of the entry, e.g. "00:05:00" for 5 minutes. No

Examples

Simple block:

{% cache "my-cache-block" %}
    ...
{% endcache %}

Nested blocks:

{% cache "a" %}
    A {{ "now" | date: "%T" }} (No Duration) <br />
    {% cache "a1", dependencies: "a1", vary_by: "user", expires_after: "0:5:0" %}
        A1 {{ "now" | date: "%T" }} (5 Minutes) <br />
    {% endcache %}
    {% cache "a2", dependencies: "a2", expires_after: "0:0:1" %}
        A2 {{ "now" | date: "%T" }} (1 Second) <br />
        {% cache "a2a", dependencies: "a2a", contexts: "route", expires_sliding: "0:0:5" %}
            A2A {{ "now" | date: "%T" }} (5 Seconds) <br />
        {% endcache %}
    {% endcache %}
{% endcache %}

Altering a cache scope

You may not yet know all the child dependencies, or even how long the cache block should be cached for when you enter a cache block.
An example might be a cache block around a list of content items from a query --- because you do not know which content items will be displayed by the query, you cannot define the correct dependencies when you enter the cache block.

There are four tags that allow you to alter the current cache scope. It's safe to use these tags even if you don't necessarily know if you're inside a cache block:

Liquid Tag Description Example
cache_dependency Adds a dependency to the current cache scope. {% cache_dependency "alias:{Alias}" %}
cache_expires_on Sets a fixed date and time that the cache item will expire. The most restrictive cache policy (i.e. the one with the shortest life) will win in the event of multiple expiry policies being defined for a single block. {% cache_expires_on {A DateTime or DateTimeOffset instance %} (e.g. from a date/time field on a content item)
cache_expires_after Sets a timespan relative to when the item was cached that the cache item will expire. The most restrictive cache policy (i.e. the one with the shortest life) will win in the event of multiple expiry policies being defined for a single block. {% cache_expires_after "01:00:00" %} (One hour)
cache_expires_sliding Sets a sliding window for the expiry of the cache item. The most restrictive cache policy (i.e. the one with the shortest life) will win in the event of multiple expiry policies being defined for a single block. {% cache_expires_sliding "00:01:00" %} (One minute)

Example:

Displaying content items from a query:

{% cache "recent-blog-posts"}
    {% assign recentBlogPosts = Queries.RecentBlogPosts | query %}
    {% for item in recentBlogPosts %}
        {{ item | display_text }}

        {% assign cacheDependency = "contentitemid:" | append: Model.ContentItem.ContentItemId %}
        {% cache_dependency cacheDependency %}
    {% endfor %}
{% endcache %}

Each item that is displayed by the query will now add its own cache dependency to the recent-blog-posts cache block.

Razor cache tag

This has not yet been implemented. If you feel up to it, and you'd like to help out, then please raise a pull request.