//
        //====================================================================================================
        /// <summary>
        /// save cacheDocument to memory cache
        /// </summary>
        /// <param name="serverKey">key converted to serverKey with app name and code version</param>
        /// <param name="cacheDocument"></param>
        public void storeCacheDocument_MemoryCache(string serverKey, CacheDocumentClass cacheDocument)
        {
            ObjectCache     cache  = MemoryCache.Default;
            CacheItemPolicy policy = new CacheItemPolicy {
                AbsoluteExpiration = cacheDocument.invalidationDate // core.dateTimeMockable.AddMinutes(100);
            };

            cache.Set(serverKey, cacheDocument, policy);
        }
 //
 //====================================================================================================
 /// <summary>
 /// save object directly to cache.
 /// </summary>
 /// <param name="key"></param>
 /// <param name="cacheDocument">Either a string, a date, or a serializable object</param>
 /// <param name="invalidationDate"></param>
 /// <remarks></remarks>
 private void storeCacheDocument(string key, CacheDocumentClass cacheDocument)
 {
     try {
         //
         if (string.IsNullOrEmpty(key))
         {
             throw new ArgumentException("cache key cannot be blank");
         }
         string typeMessage = "";
         string serverKey   = createServerKey(key);
         if (core.serverConfig.enableLocalMemoryCache)
         {
             //
             // -- save local memory cache
             typeMessage = "local-memory";
             storeCacheDocument_MemoryCache(serverKey, cacheDocument);
         }
         if (core.serverConfig.enableLocalFileCache)
         {
             //
             // -- save local file cache
             typeMessage = "local-file";
             string serializedData = SerializeObject(cacheDocument);
             using (System.Threading.Mutex mutex = new System.Threading.Mutex(false, serverKey)) {
                 mutex.WaitOne();
                 core.privateFiles.saveFile("appCache\\" + FileController.encodeDosFilename(serverKey + ".txt"), serializedData);
                 mutex.ReleaseMutex();
             }
         }
         if (core.serverConfig.enableRemoteCache)
         {
             typeMessage = "remote";
             if (remoteCacheInitialized)
             {
                 //
                 // -- save remote cache
                 if (!cacheClient.Store(Enyim.Caching.Memcached.StoreMode.Set, serverKey, cacheDocument, cacheDocument.invalidationDate))
                 {
                     //
                     // -- store failed
                     LogController.logError(core, "Enyim cacheClient.Store failed, no details available.");
                 }
             }
         }
         //
         LogController.logTrace(core, "cacheType [" + typeMessage + "], key [" + key + "], expires [" + cacheDocument.invalidationDate + "], depends on [" + string.Join(",", cacheDocument.dependentKeyList) + "], points to [" + string.Join(",", cacheDocument.keyPtr) + "]");
         //
     } catch (Exception ex) {
         LogController.logError(core, ex);
     }
 }
 //
 //====================================================================================================
 /// <summary>
 /// set a key ptr. A ptr points to a normal key, creating an altername way to get/invalidate a cache.
 /// ex - image with id=10, guid={999}. The normal key="image/id/10", the alias Key="image/ccguid/{9999}"
 /// </summary>
 /// <param name="CP"></param>
 /// <param name="keyPtr"></param>
 /// <param name="data"></param>
 /// <remarks></remarks>
 public void storePtr(string keyPtr, string key)
 {
     try {
         keyPtr = Regex.Replace(keyPtr, "0x[a-fA-F\\d]{2}", "_").ToLowerInvariant().Replace(" ", "_");
         key    = Regex.Replace(key, "0x[a-fA-F\\d]{2}", "_").ToLowerInvariant().Replace(" ", "_");
         CacheDocumentClass cacheDocument = new CacheDocumentClass(core.dateTimeNowMockable)
         {
             saveDate         = core.dateTimeNowMockable,
             invalidationDate = core.dateTimeNowMockable.AddDays(invalidationDaysDefault),
             keyPtr           = key
         };
         storeCacheDocument(keyPtr, cacheDocument);
     } catch (Exception ex) {
         LogController.logError(core, ex);
     }
 }
 //
 //====================================================================================================
 /// <summary>
 /// save an object to cache, with invalidation date and dependentKeyList
 ///
 /// </summary>
 /// <param name="key"></param>
 /// <param name="content"></param>
 /// <param name="invalidationDate"></param>
 /// <param name="dependentKeyList">Each tag should represent the source of data, and should be invalidated when that source changes.</param>
 /// <remarks></remarks>
 public void storeObject(string key, object content, DateTime invalidationDate, List <string> dependentKeyList)
 {
     try {
         key = Regex.Replace(key, "0x[a-fA-F\\d]{2}", "_").ToLowerInvariant().Replace(" ", "_");
         var cacheDocument = new CacheDocumentClass(core.dateTimeNowMockable)
         {
             content          = content,
             saveDate         = core.dateTimeNowMockable,
             invalidationDate = invalidationDate,
             dependentKeyList = dependentKeyList
         };
         storeCacheDocument(key, cacheDocument);
     } catch (Exception ex) {
         LogController.logError(core, ex);
     }
 }
 //
 //====================================================================================================
 // <summary>
 // invalidates a tag
 // </summary>
 // <param name="tag"></param>
 // <remarks></remarks>
 public void invalidate(string key, int recursionLimit = 5)
 {
     try {
         Controllers.LogController.logTrace(core, "invalidate, key [" + key + "], recursionLimit [" + recursionLimit + "]");
         if ((recursionLimit > 0) && (!string.IsNullOrWhiteSpace(key.Trim())))
         {
             key = Regex.Replace(key, "0x[a-fA-F\\d]{2}", "_").ToLowerInvariant().Replace(" ", "_");
             // if key is a ptr, we need to invalidate the real key
             CacheDocumentClass cacheDocument = getCacheDocument(key);
             if (cacheDocument == null)
             {
                 // no cache for this key, if this is a dependency for another key, save invalidated
                 storeCacheDocument(key, new CacheDocumentClass(core.dateTimeNowMockable)
                 {
                     saveDate = core.dateTimeNowMockable
                 });
             }
             else
             {
                 if (!string.IsNullOrWhiteSpace(cacheDocument.keyPtr))
                 {
                     // this key is an alias, invalidate it's parent key
                     invalidate(cacheDocument.keyPtr, --recursionLimit);
                 }
                 else
                 {
                     // key is a valid cache, invalidate it
                     storeCacheDocument(key, new CacheDocumentClass(core.dateTimeNowMockable)
                     {
                         saveDate = core.dateTimeNowMockable
                     });
                 }
             }
         }
     } catch (Exception ex) {
         LogController.logError(core, ex);
         throw;
     }
 }
        //
        //====================================================================================================
        /// <summary>
        /// get a cache object from the cache. returns the cacheObject that wraps the object
        /// </summary>
        /// <typeparam name="returnType"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        private CacheDocumentClass getCacheDocument(string key)
        {
            CacheDocumentClass result = null;

            try {
                // - verified in createServerKey() -- key = Regex.Replace(key, "0x[a-fA-F\\d]{2}", "_").ToLowerInvariant().Replace(" ", "_");
                if (string.IsNullOrEmpty(key))
                {
                    throw new ArgumentException("cache key cannot be blank");
                }
                string serverKey   = createServerKey(key);
                string typeMessage = "";
                if (remoteCacheInitialized)
                {
                    //
                    // -- use remote cache
                    typeMessage = "remote";
                    try {
                        result = cacheClient.Get <CacheDocumentClass>(serverKey);
                    } catch (Exception ex) {
                        //
                        // --client does not throw its own errors, so try to differentiate by message
                        if (ex.Message.ToLowerInvariant().IndexOf("unable to load type") >= 0)
                        {
                            //
                            // -- trying to deserialize an object and this code does not have a matching class, clear cache and return empty
                            LogController.logWarn(core, ex);
                            cacheClient.Remove(serverKey);
                            result = null;
                        }
                        else
                        {
                            //
                            // -- some other error
                            LogController.logError(core, ex);
                            throw;
                        }
                    }
                }
                if ((result == null) && core.serverConfig.enableLocalMemoryCache)
                {
                    //
                    // -- local memory cache
                    typeMessage = "local-memory";
                    result      = (CacheDocumentClass)MemoryCache.Default[serverKey];
                }
                if ((result == null) && core.serverConfig.enableLocalFileCache)
                {
                    //
                    // -- local file cache
                    typeMessage = "local-file";
                    string serializedDataObject = null;
                    using (System.Threading.Mutex mutex = new System.Threading.Mutex(false, serverKey)) {
                        mutex.WaitOne();
                        serializedDataObject = core.privateFiles.readFileText("appCache\\" + FileController.encodeDosFilename(serverKey + ".txt"));
                        mutex.ReleaseMutex();
                    }
                    if (!string.IsNullOrEmpty(serializedDataObject))
                    {
                        result = DeserializeObject <CacheDocumentClass>(serializedDataObject);
                        storeCacheDocument_MemoryCache(serverKey, result);
                    }
                }
                string returnContentSegment = SerializeObject(result);
                returnContentSegment = (returnContentSegment.Length > 50) ? returnContentSegment.Substring(0, 50) : returnContentSegment;
                //
                // -- log result
                if (result == null)
                {
                    LogController.logTrace(core, "miss, cacheType [" + typeMessage + "], key [" + key + "]");
                }
                else
                {
                    if (result.content == null)
                    {
                        LogController.logTrace(core, "hit, cacheType [" + typeMessage + "], key [" + key + "], saveDate [" + result.saveDate + "], content [null]");
                    }
                    else
                    {
                        string content = result.content.ToString();
                        content = (content.Length > 50) ? (content.left(50) + "...") : content;
                        LogController.logTrace(core, "hit, cacheType [" + typeMessage + "], key [" + key + "], saveDate [" + result.saveDate + "], content [" + content + "]");
                    }
                }
                //
                // if dependentKeyList is null, return an empty list, not null
                if (result != null)
                {
                    //
                    // -- empty objects return nothing, empty lists return count=0
                    if (result.dependentKeyList == null)
                    {
                        result.dependentKeyList = new List <string>();
                    }
                }
            } catch (Exception ex) {
                LogController.logError(core, ex);
                throw;
            }
            return(result);
        }
 //
 //========================================================================
 /// <summary>
 /// get an object of type TData from cache. If the cache misses or is invalidated, null object is returned
 /// </summary>
 /// <typeparam name="TData"></typeparam>
 /// <param name="key"></param>
 /// <returns></returns>
 public TData getObject <TData>(string key)
 {
     try {
         key = Regex.Replace(key, "0x[a-fA-F\\d]{2}", "_").ToLowerInvariant().Replace(" ", "_");
         if (string.IsNullOrEmpty(key))
         {
             return(default(TData));
         }
         //
         // -- read cacheDocument (the object that holds the data object plus control fields)
         CacheDocumentClass cacheDocument = getCacheDocument(key);
         if (cacheDocument == null)
         {
             return(default(TData));
         }
         //
         // -- test for global invalidation
         int dateCompare = globalInvalidationDate.CompareTo(cacheDocument.saveDate);
         if (dateCompare >= 0)
         {
             //
             // -- global invalidation
             LogController.logTrace(core, "key [" + key + "], invalidated because cacheObject saveDate [" + cacheDocument.saveDate + "] is before the globalInvalidationDate [" + globalInvalidationDate + "]");
             return(default(TData));
         }
         //
         // -- test all dependent objects for invalidation (if they have changed since this object changed, it is invalid)
         bool cacheMiss = false;
         foreach (string dependentKey in cacheDocument.dependentKeyList)
         {
             CacheDocumentClass dependantCacheDocument = getCacheDocument(dependentKey);
             if (dependantCacheDocument == null)
             {
                 // create dummy cache to validate future cache requests, fake saveDate as last globalinvalidationdate
                 storeCacheDocument(dependentKey, new CacheDocumentClass(core.dateTimeNowMockable)
                 {
                     keyPtr   = null,
                     content  = "",
                     saveDate = globalInvalidationDate
                 });
             }
             else
             {
                 dateCompare = dependantCacheDocument.saveDate.CompareTo(cacheDocument.saveDate);
                 if (dateCompare >= 0)
                 {
                     //
                     // -- invalidate because a dependent document was changed after the cacheDocument was saved
                     cacheMiss = true;
                     LogController.logTrace(core, "[" + key + "], invalidated because the dependantKey [" + dependentKey + "] was modified [" + dependantCacheDocument.saveDate + "] after the cacheDocument's saveDate [" + cacheDocument.saveDate + "]");
                     break;
                 }
             }
         }
         TData result = default(TData);
         if (!cacheMiss)
         {
             if (!string.IsNullOrEmpty(cacheDocument.keyPtr))
             {
                 //
                 // -- this is a pointer key, load the primary
                 result = getObject <TData>(cacheDocument.keyPtr);
             }
             else if (cacheDocument.content is Newtonsoft.Json.Linq.JObject dataJObject)
             {
                 //
                 // -- newtonsoft types
                 result = dataJObject.ToObject <TData>();
             }
             else if (cacheDocument.content is Newtonsoft.Json.Linq.JArray dataJArray)
             {
                 //
                 // -- newtonsoft types
                 result = dataJArray.ToObject <TData>();
             }
             else if (cacheDocument.content == null)
             {
                 //
                 // -- if cache data was left as a string (might be empty), and return object is not string, there was an error
                 result = default(TData);
             }
             else
             {
                 //
                 // -- all worked, but if the class is unavailable let it return default like a miss
                 try {
                     result = (TData)cacheDocument.content;
                 } catch (Exception ex) {
                     //
                     // -- object value did not match. return as miss
                     LogController.logWarn(core, "cache getObject failed to cast value as type, key [" + key + "], type requested [" + typeof(TData).FullName + "], ex [" + ex + "]");
                     result = default(TData);
                 }
             }
         }
         return(result);
     } catch (Exception ex) {
         LogController.logError(core, ex);
         return(default(TData));
     }
 }