When creating new applications or functionality on top of the Verint Community platform, you'll need to consider caching for performance. Verint Community provides a powerful, optimized caching framework.
When to use the Verint Community Caching Framework
When you create functionality that exposes data that is costly to load/calculate and/or accessed frequently.
Cache keys and tags
All items in the cache will have an associated key that can be used to identify it. Keys are case sensitive.
In addition to a key, you can further identify an item by placing one or more tags on it. Like keys, tags are case-sensitive.
Tags are useful when grouping data into logical partitions. Using tags, you can expire a group of items in the same logical partition without knowing all the individual keys. While tags are powerful in this regard, they should never be used in substitution of a good key generating algorithm. Tag operations are slower in comparison to single key lookups and removals and most costly to store, so be very selective about how and when you use them.
Choosing a cache scope
All cache operations require one or more CacheScope values. When defining a cache layer, each layer will identify what kind of cache it is by using a cache scope.
Before caching an item, you'll need to decide what scope(s) to operate in. Here are some general guidelines that you may follow:
| Name | Description |
| None | No location specified. |
| Context | The Context location is specified. This scope is for storing items for a short amount of time and is typically private to single request or job run. |
| Process | The Process location is specified. This scope stores items inside of the running process and is shared among users. |
| Distributed | The Distributed location is specified. This scope is used when items are available to all running processes. Typically, distributed-only caches will be the slowest cache types. Distributed cache storage requires an appropriate IDistributedCacheSerializationProvider Plugin is enabled to support serialization/deserialization of the data being sent/read from the distributed cache. |
| All | All possible scope values are specified. |
For most cached data, it is recommended to store in the Process and Distributed caches, if possible.
Cache Timeouts
By default, cached data stays in the cache indefinitely. Each cache layer has its own configuration on the maximum amount of data or maximum number of items that can be stored. By default, cached data that is accessed less frequently is expired to make room for data that is new or frequently accessed.
It is also possible to cache data for a maximum period of time. In this case, regardless of how frequently the data is accessed, it will expire from the cache after the configured amount of time.
Samples
The following sample defines a plugin that adds a Scripting API (via IScriptedContentFragmentExtension ) to provide access to a custom add/get/update/delete API and a serialization provider (via IDistributedCacheSerializationProvider ) to support caching the custom data in the distributed cache.
This example demonstrates the following best practices:
- Cached data should be simple data-storage classes, often defined internally. This ensures efficient cache data storage.
- Cached data should always be cloned before making edits to prevent contaminating/poisoning the cache.
- Scripting APIs should return classes that inherit from ApiEntity to support handling/exposing errors and prevent editing of internal/cached data.
- When adding or editing cached data, the data should not be put into the cache. Instead, the data's associated cache key should be expired. Expirations are propagated to all application components but puts are not. Putting modified data into the cache will result in cache contamination/poisoning.
- When serializing data to the distributed cache layer, also support the in-process layer for faster cache access.
- Custom classes put into the distributed cache layer must have a corresponding IDistributedCacheSerializationProvider plugin defined and enabled. Without it, data cannot be stored in the distributed cache and an exception will be logged (but the distributed cache call will otherwise return 'not found' to defer to lower-layer caches).
using System;
using Telligent.Evolution.Extensibility.Api.Entities.Version1;
using Telligent.Evolution.Extensibility.Caching.Version1;
using Telligent.Evolution.Extensibility.UI.Version1;
namespace PluginSamples
{
public class CachePlugin : IDistributedCacheSerializationProvider, IScriptedContentFragmentExtension
{
#region IPlugin Implementation
public string Name => "Cache Plugin Sample";
public string Description => "Exposes a scripting API (samples_v1_cache) to demonstrate simple cache access.";
public void Initialize()
{
}
#endregion
#region IDistributedCacheSerializationProvider Implementation
public IDistributedCacheSerializer<T> GetSerializer<T>()
{
if (typeof(T) == typeof(SampleCachedDataInternal))
return (IDistributedCacheSerializer<T>)new SampleCachedDataInternalSerializer();
return null;
}
#endregion
#region IScriptedContentFragmentExtension Implementation
public string ExtensionName => "samples_v1_cache";
public object? Extension
{
get
{
return new CacheScriptApi();
}
}
#endregion
}
#region Script API
public class CacheScriptApi
{
const CacheScope _scope = CacheScope.Process | CacheScope.Distributed;
public CacheScriptApi()
{
}
public SampleCachedData? Get(int id)
{
try
{
var sampleCachedDataInternal = CacheService.Get<SampleCachedDataInternal>(GetCacheKey(id), _scope, () =>
{
// TODO: Load SampleCacheData from database or external service.
return new SampleCachedDataInternal();
});
if (sampleCachedDataInternal == null)
return new SampleCachedData(new Exception("Not found"));
return new SampleCachedData(sampleCachedDataInternal);
}
catch (Exception ex)
{
return new SampleCachedData(ex);
}
}
public SampleCachedData Update(int id)
{
try
{
// Get the existing item
var sampleCachedDataInternal = Get(id)?._internalData;
if (sampleCachedDataInternal == null)
return new SampleCachedData(new Exception("Not found"));
// never modify an object from the cache -- it will poison the cache.
sampleCachedDataInternal = sampleCachedDataInternal.Clone();
// modify the object
sampleCachedDataInternal.Count++;
// TODO: Save the object to the database or external service.
// Expire the cache so that this item is not stored in any layer of cache and will be
// reloaded on next access. Note that we are not putting the object into the cache
// since Put operations are not propagated to all application instances. Instead
// it is important to remove the data by its key which is propagated and will ensure
// that all application instances are synchronized with this change.
CacheService.Remove(GetCacheKey(id), _scope);
return new SampleCachedData(sampleCachedDataInternal);
}
catch (Exception ex)
{
return new SampleCachedData(ex);
}
}
public SampleCachedData Add(int id)
{
try
{
// TODO: Save SampleCacheData to database or external service
var sampleCacheDataInternal = new SampleCachedDataInternal
{
Id = id,
Count = 0,
LastUpdatedUtc = DateTime.UtcNow
};
// Expire the cache so that this item is not stored in any layer of cache and will be
// reloaded on next access. Note that we are not putting the object into the cache
// since Put operations are not propagated to all application instances. Instead
// it is important to remove the data by its key which is propagated and will ensure
// that all application instances are synchronized with this change.
CacheService.Remove(GetCacheKey(id), _scope);
return new SampleCachedData(sampleCacheDataInternal);
}
catch (Exception ex)
{
return new SampleCachedData(ex);
}
}
public AdditionalInfo Delete(int id)
{
try
{
// TODO: Delete SampleCacheData to database or external service
// expire the cache so that this item is not stored in any layer of cache
CacheService.Remove(GetCacheKey(id), _scope);
return new AdditionalInfo();
}
catch (Exception ex)
{
return new AdditionalInfo(ex);
}
}
private string GetCacheKey(int id)
{
return $"samplecachedata_{id}";
}
}
#endregion
// Internal data-storage classes should be used for caching to
// completely control the amount of data persisted in the
// cache
public class SampleCachedDataInternal
{
public int Id { get; set; }
public DateTime LastUpdatedUtc { get; set; }
public int Count { get; set; }
public SampleCachedDataInternal Clone()
{
return new SampleCachedDataInternal
{
Id = this.Id,
LastUpdatedUtc = this.LastUpdatedUtc,
Count = this.Count
};
}
}
// API-exposed classes should not allow editing of cached data.
public class SampleCachedData : Telligent.Evolution.Extensibility.Api.Entities.Version1.ApiEntity
{
internal SampleCachedDataInternal? _internalData;
public SampleCachedData(SampleCachedDataInternal internalData) : base()
{
_internalData = internalData;
}
public SampleCachedData(Exception ex) : base(new AdditionalInfo(ex))
{
_internalData = null;
}
public int? Id => _internalData?.Id;
public DateTime? LastAccessedUtc => _internalData?.LastUpdatedUtc;
public int? Count => _internalData?.Count;
}
// This serializer supports serializing/deserializing the internal
// storage class, SampleCachedDataInternal, and is registered via
// an IDistributedCacheSerializationProvider plugin.
public class SampleCachedDataInternalSerializer : IDistributedCacheSerializer<SampleCachedDataInternal>
{
public SampleCachedDataInternal Deserialize(byte[] data)
{
// Normally a serialization framework would be used. This is for demonstration purposes only.
var components = System.Text.Encoding.UTF8.GetString(data)?.Split(new char[] { ',' }, 3);
if (components != null
&& components.Length == 3
&& int.TryParse(components[0], out int id)
&& int.TryParse(components[1], out int count)
&& DateTime.TryParse(components[2], out DateTime lastUpdatedUtc))
return new SampleCachedDataInternal
{
Id = id,
Count = count,
LastUpdatedUtc = lastUpdatedUtc
};
return null;
}
public byte[] Serialize(SampleCachedDataInternal obj)
{
// Normally a serialization framework would be used. This is for demonstration purposes only.
return System.Text.Encoding.UTF8.GetBytes($"{obj.Id},{obj.Count},{obj.LastUpdatedUtc:O}");
}
}
}