I have written an efficient way to control data to be cached/not-cached in HttpCache using JSON and C# controlling it.
The reason for this implementation is to make use of existing application servers to store cache to save network latency and call across processes. another reason was to minimize code change after we find memory or performance issues (as you can control data from JSON).
The implementation uses C# code to interact with HttpCache and uses JSON object to find what to cache in a similar way we have seen form authentication used to work in web application projects.
The JSON format looks like:
{
Key: "Cache",
Allow: "*",
Deny: "",
Keys: [
{
Key: "CacheKey",
Allow: "*",
Deny: "",
Keys: [
{
Key: "Organization",
Allow: "*",
Deny: "",
Keys: [
{
Key: "Department",
Allow: "*",
}
]
}
]
},
{
Key: "SomeOtherCacheKey",
Allow: "*",
Deny: ""
}
]
}
I am taking example of an Employee
class which is searched by Organization and Department.
Although the solution is not fully dynamic, it works closely with repository.
To implement it, I have created a generic CacheBase
class. This class must be derived from a facade that will be called to get data and the facade will call the GetFromCache
method to get data from the cache. If it is not allowed by JSON to manage the cache for the given cacheKey along with the parameters (group keys and values), facade will call the repository directly. I am assuming that required parameter will be injected to constructor.
Edits and code review is welcome.
public class EmployeeFacade: CacheBase<IEnumerable<Employee>>
{
private readonly IEmployeeRepository _repository;
public EmployeeFacade(IEmployeeRepository employeeRepository) : base("3600","Employee_Cache",cacheConfigurationJSON)
{
_repository = employeeRepository;
}
public method GetEmployee(string organization, string department)
{
var groupKeyAndValues = new List<Tuple<string, string>>();
groupKeyAndValues.Add(new Tuple<String, string>("Organization", organization));
groupKeyAndValues.Add(new Tuple<String, string>("Department", department));
bool isCacheAllowed;
var employeeInfos = base.GetFromCache(() => _repository.GetEmployee(organization, department), groupKeyAndValues, out isCacheAllowed);
//if cache is not allowed for this combination , return direct from repository.
if (!isCacheAllowed)
return _repository.GetEmployee(organization, department);
}
}
public abstract class CacheBase<T> where T : class
{
private string _cacheTimeoutInSeconds;
private string _cacheConfiguration;
private static readonly object CacheLockObject = new object();
string _cacheKey;
public CacheBase(string cacheTimeoutInSeconds, string cacheKey, string cacheConfigurationJson)
{
_cacheTimeoutInSeconds= cacheTimeoutInSeconds;
_cacheConfiguration = cacheConfigurationJson;
_cacheKey = cacheKey;
}
public T GetFromCache(Func<T> methodToFillCache, IEnumerable<Tuple<string, string>> groupKeyAndValues, out bool isCacheAllowed)
{
var baseCacheKey = _cacheKey;
var detailedCacheKey = GenerateCacheKey(groupKeyAndValues);
isCacheAllowed = IsSaveToCacheAllowed(baseCacheKey, groupKeyAndValues);
if (!isCacheAllowed) return null;
var result = HttpRuntime.Cache[detailedCacheKey] as T;
if (result == null)
{
lock (CacheLockObject)
{
if (result == null)
{
result = methodToFillCache() as T;
SetToCache(result, detailedCacheKey);
}
}
}
return result;
}
private void SetToCache(T data, string detailedCacheKey)
{
HttpRuntime.Cache.Insert(detailedCacheKey, data, null,
DateTime.Now.AddSeconds(Convert.ToInt32(cacheTimeoutInSeconds)), TimeSpan.Zero);
}
private string GenerateCacheKey(IEnumerable<Tuple<string, string>> groupKeyAndValues)
{
string detailedCacheKey = _cacheKey;
if (groupKeyAndValues == null) return detailedCacheKey;
var groupKeys = groupKeyAndValues.Select(x => x.Item1).ToList();
if (groupKeys != null && groupKeys.Count > 0)
{
foreach (var groupKey in groupKeys)
{
if (!String.IsNullOrWhiteSpace(groupKey))
detailedCacheKey += "_" + groupKey;
}
}
return detailedCacheKey;
}
private bool IsSaveToCacheAllowed(string cacheKey, IEnumerable<Tuple<string, string>> groupKeyAndValues)
{
cacheKey = cacheKey.ToUpper();
if (_cacheConfiguration == null) return false;
string valuesAllowed = CacheConstant.All; string valuesDenied = CacheConstant.All;
//CACHE
if (_cacheConfiguration == null) return true;
valuesAllowed = _cacheConfiguration.Allow;
valuesDenied = _cacheConfiguration.Deny;
var matchingCacheConfig = _cacheConfiguration.Keys.FirstOrDefault(x => x.Key.ToUpper() == cacheKey);
if (matchingCacheConfig == null) return IsAllowed(cacheKey, valuesAllowed, valuesDenied); ;
valuesAllowed = matchingCacheConfig.Allow;
valuesDenied = matchingCacheConfig.Deny;
bool isAllowed = IsAllowed(cacheKey, valuesAllowed, valuesDenied);
foreach (var groupKeyAndValue in groupKeyAndValues)
{
var key = groupKeyAndValue.Item1.ToUpper();
var value = groupKeyAndValue.Item2.ToUpper();
matchingCacheConfig = matchingCacheConfig.Keys.FirstOrDefault(x => x.Key.ToUpper() == key);
if (matchingCacheConfig == null) return IsAllowed(key, valuesAllowed, valuesDenied);
valuesAllowed = matchingCacheConfig.Allow;
valuesDenied = matchingCacheConfig.Deny;
isAllowed = IsAllowed(value, matchingCacheConfig.Allow, matchingCacheConfig.Deny);
if (!isAllowed) break;
}
return isAllowed;
}
bool IsAllowed(string value, string allowedValues, string deniedValues)
{
//Prerequisites: among allowed and denied values , one should contain * and other should contain value or empty string
var allowed = allowedValues.ToUpper().Split(',');
var denied = deniedValues.ToUpper().Split(',');
if (denied.Contains(CacheConstant.All))
{
if (allowed.Contains(value) || allowed.Contains(CacheConstant.All))
{
return true;
}
else
{
return false;
}
}
else if (denied.Contains(value))
{
if (allowed.Contains(value))
{
return true;
}
else
{
return false;
}
}
else //denied contains nothing
{
if (allowed.Contains(value) || allowed.Contains(CacheConstant.All) || allowed.Count() == 0)
{
return true;
}
else
{
return false;
}
}
}
}
1 Answer 1
EmployeeFacade
This class shouldn't be called like that because it's not a facade. It's more an EmployeeCache
.
What is a Facade?
A facade is an object that provides a simplified interface to a larger body of code, such as a class library.
The EmployeeFacade
doesn't do that. If at all it could be an EmployeeRepositoryFacade
.
see: Facade pattern
CacheBase and inheritance
Inheritance doesn't work like that. The abstract CacheBase
does not have any abstract methods that need to be overriden in derived types. There is no point of having it abstract.
This design would be better for composition where you pass an instance of CacheBase
to the EmployeeCache
but let's stick to the inheritance for the sake of an excercise.
A good candidate for a virtual method would be the GetFromCache
method that should have the following signature:
public abstract class CacheBase<T> where T : class
{
protected T GetFromCache(Func<T> methodToFillCache, IEnumerable<Tuple<string, string>> groupKeyAndValues, out bool isCacheAllowed)
{
...
}
pubic abstract T GetFromCache(string organization, string department);
}
Notice that the first GetFromCache
method should now be protected. It's not of any use in the public context.
The EmployeeCache
should override it:
public class EmployeeFacade : CacheBase<IEnumerable<Employee>>
{
public override Employee GetFromCache(string organization, string department)
{
...
}
}
If other caches don't support the abstract T GetFromCache
then you should drop inheritance and change to composition because if there is no common interface the abstract design is pointless and violates the Liskov's Substitution Principle(LSP) that says:
Derived types must be completely substitutable for their base types.
IsAllowed
You don't need any of those else
s. Just return what you have in the if
s:
bool IsAllowed(string value, string allowedValues, string deniedValues)
{
//Prerequisites: among allowed and denied values , one should contain * and other should contain value or empty string
var allowed = allowedValues.ToUpper().Split(',');
var denied = deniedValues.ToUpper().Split(',');
if (denied.Contains(CacheConstant.All))
{
return (allowed.Contains(value) || allowed.Contains(CacheConstant.All));
}
else if (denied.Contains(value))
{
return (allowed.Contains(value));
}
else //denied contains nothing
{
return (allowed.Contains(value) || allowed.Contains(CacheConstant.All) || allowed.Count() == 0);
}
}