Подтвердить что ты не робот

Как отключить ссылку на объект на MemoryCache

В настоящее время я тестирую новый MemoryCache в .Net 4 для кэширования нескольких бит данных в одном из наших приложений. У меня проблема в том, что объекты обновляются, и в кеше сохраняются изменения, например.

public IEnumerable<SomeObject> GetFromDatabase(){
    const string _cacheKeyGetDisplayTree = "SomeKey"; 
    ObjectCache _cache = MemoryCache.Default;
    var objectInCache = _cache.Get(_cacheKeyGetDisplayTree) as IEnumerable<SomeObject>;
    if (objectInCache != null)
        return objectInCache.ToList();

    // Do something to get the items
    _cache.Add(_cacheKeyGetDisplayTree, categories, new DateTimeOffset(DateTime.UtcNow.AddHours(1)));

    return categories.ToList();
}

public IEnumerable<SomeObject> GetWithIndentation(){
    var categories = GetFromDatabase();

    foreach (var c in categories)
    {
        c.Name = "-" + c.Name;
    }

    return categories;
}

Если бы я сначала вызывал GetWithIndentation(), а затем вызывал GetFromDatabase(), я ожидал, что он вернет исходный список SomeObject, но вместо этого он вернет измененные элементы (с "-" с префиксом имени).

Я думал, что ToList() уничтожил ссылку, но, похоже, она продолжает сохраняться. Я уверен, что это очевидно, но может ли кто-нибудь обнаружить, где я ошибаюсь?

4b9b3361

Ответ 1

Я создал класс ReadonlyMemoryCache для решения этой проблемы. Он наследуется от .NET 4.0 MemoryCache, но объекты хранятся readonly (по значению) и не могут быть изменены. Я глубоко копирую объекты перед хранением, используя двоичную сериализацию.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Runtime.Caching;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;


namespace ReadOnlyCache
{
    class Program
    {

        static void Main()
        {
            Start();
            Console.ReadLine();
        }

        private static async void Start() {
            while (true)
            {
                TestMemoryCache();
                await Task.Delay(TimeSpan.FromSeconds(1));
            }
        }

        private static void TestMemoryCache() {
            List<Item> items = null;
            string cacheIdentifier = "items";

            var cache = ReadonlyMemoryCache.Default;

            //change to MemoryCache to understand the problem
            //var cache = MemoryCache.Default;

            if (cache.Contains(cacheIdentifier))
            {
                items = cache.Get(cacheIdentifier) as List<Item>;
                Console.WriteLine("Got {0} items from cache: {1}", items.Count, string.Join(", ", items));

                //modify after getting from cache, cached items will remain unchanged
                items[0].Value = DateTime.Now.Millisecond.ToString();

            }
            if (items == null)
            {
                items = new List<Item>() { new Item() { Value = "Steve" }, new Item() { Value = "Lisa" }, new Item() { Value = "Bob" } };
                Console.WriteLine("Reading {0} items from disk and caching", items.Count);

                //cache for x seconds
                var policy = new CacheItemPolicy() { AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddSeconds(5)) };
                cache.Add(cacheIdentifier, items, policy);

                //modify after writing to cache, cached items will remain unchanged
                items[1].Value = DateTime.Now.Millisecond.ToString();
            }
        }
    }

    //cached items must be serializable

    [Serializable]
    class Item {
        public string Value { get; set; }
        public override string ToString() { return Value; }
    }

    /// <summary>
    /// Readonly version of MemoryCache. Objects will always be returned in-value, via a deep copy.
    /// Objects requrements: [Serializable] and sometimes have a deserialization constructor (see http://stackoverflow.com/a/5017346/2440) 
    /// </summary>
    public class ReadonlyMemoryCache : MemoryCache
    {

        public ReadonlyMemoryCache(string name, NameValueCollection config = null) : base(name, config) {
        }

        private static ReadonlyMemoryCache def = new ReadonlyMemoryCache("readonlydefault");

        public new static ReadonlyMemoryCache Default {
            get
            {
                if (def == null)
                    def = new ReadonlyMemoryCache("readonlydefault");
                return def;
            }
        }

        //we must run deepcopy when adding, otherwise items can be changed after the add() but before the get()

        public new bool Add(CacheItem item, CacheItemPolicy policy)
        {
            return base.Add(item.DeepCopy(), policy);
        }

        public new object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        {
            return base.AddOrGetExisting(key, value.DeepCopy(), absoluteExpiration, regionName);
        }

        public new CacheItem AddOrGetExisting(CacheItem item, CacheItemPolicy policy)
        {
            return base.AddOrGetExisting(item.DeepCopy(), policy);
        }

        public new object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null)
        {
            return base.AddOrGetExisting(key, value.DeepCopy(), policy, regionName);
        }

        //methods from ObjectCache

        public new bool Add(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        {
            return base.Add(key, value.DeepCopy(), absoluteExpiration, regionName);
        }

        public new bool Add(string key, object value, CacheItemPolicy policy, string regionName = null)
        {
            return base.Add(key, value.DeepCopy(), policy, regionName);
        }

        //for unknown reasons, we also need deepcopy when GETTING values, even though we run deepcopy on all (??) set methods.

        public new object Get(string key, string regionName = null)
        {
            var item = base.Get(key, regionName);
            return item.DeepCopy();
        }

        public new CacheItem GetCacheItem(string key, string regionName = null)
        {
            var item = base.GetCacheItem(key, regionName);
            return item.DeepCopy();
        }

    }


    public static class DeepCopyExtentionMethods
    {
        /// <summary>
        /// Creates a deep copy of an object. Must be [Serializable] and sometimes have a deserialization constructor (see http://stackoverflow.com/a/5017346/2440) 
        /// </summary>
        public static T DeepCopy<T>(this T obj)
        {
            using (var ms = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj);
                ms.Position = 0;

                return (T)formatter.Deserialize(ms);
            }
        }
    }



}

Ответ 2

В памяти кешированные объекты хранятся в том же пространстве процесса, что и процесс клиентского кэша. Когда клиент кэша запрашивает кешированный объект, клиент получает ссылку на локально кэшированный объект, а не на копию.

Единственный способ получить чистую копию объекта - реализовать пользовательский механизм клонирования (ICloneable, Serialization, Automapping,...). С помощью этой копии вы сможете изменить новый объект без изменения родительского объекта.

В зависимости от вашего варианта использования обычно не рекомендуется обновлять объект в кеше.

Ответ 3

Почему бы просто не хранить как json или строку? Они не передаются по ссылке, и когда вы выходите из кеша, вы получите новую копию:) Я здесь, чтобы бросить вызов, потому что я делаю atm!

Ответ 4

Вы можете сделать это проще, если вы снова десериализуете и сериализуете и получите свой объект кеша "По значению".

Вы можете сделать это с помощью Newtonsoft lib (просто получите его от NuGet)

var cacheObj = HttpRuntime.Cache.Get(CACHEKEY);
var json = JsonConvert.SerializeObject(cacheObj);
var byValueObj = JsonConvert.DeserializeObject<List<string>>(json);
return byValueObj;