3
\$\begingroup\$

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;
            }
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

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 elses. Just return what you have in the ifs:

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);            
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.