如何在MemoryCache上分离对象引用

我目前正在试用.Net 4中的新MemoryCache来缓存我们的一个应用程序中的一些数据。 我遇到的麻烦是对象被更新,缓存似乎是持久的变化,例如

 public IEnumerable GetFromDatabase(){ const string _cacheKeyGetDisplayTree = "SomeKey"; ObjectCache _cache = MemoryCache.Default; var objectInCache = _cache.Get(_cacheKeyGetDisplayTree) as IEnumerable; 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 GetWithIndentation(){ var categories = GetFromDatabase(); foreach (var c in categories) { c.Name = "-" + c.Name; } return categories; } 

如果我先调用GetWithIndentation()然后再调用GetFromDatabase()我希望它返回SomeObject的原始列表,而是返回修改后的项目(名称前面加上“ – ”)。

我以为ToList()破坏了引用,但似乎仍然存在更改。 我敢肯定这很明显,但是有人能发现我哪里出错吗?

我创建了一个ReadonlyMemoryCache类来解决这个问题。 它inheritance自.NET 4.0 MemoryCache,但对象以只读(按值)存储,无法修改。 我在使用二进制序列化存储之前深度复制对象。

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

在内存中,缓存对象存储在与缓存客户端进程相同的进程空间中。 当缓存客户端请求缓存对象时,客户端将接收对本地缓存对象的引用,而不是副本。

获取对象的干净副本的唯一方法是实现自定义克隆机制(ICloneable,Serialization,Automapping,…)。 使用该副本,您将能够在不更改父对象的情况下更改新对象。

根据您的使用情况,通常不建议更新缓存中的对象。

为什么不直接存储为json或字符串? 这些不是通过引用传递的,当你离开缓存时,你将获得一个新副本:)我在这里受到挑战,因为那就是我在做什么!

如果再次反序列化和序列化并使缓存对象“按值”,则可以更轻松地完成。

你可以用Newtonsoft lib这样做(只需从NuGet获取)

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

序列化/反序列化将解决问题,但同时它会破坏在内存中具有对象的作用。 缓存的作用是提供对存储对象的快速访问,我们在这里添加反序列化开销。 由于需要反序列化,我建议将缓存作为服务,例如redis缓存,它将被集中,因此您不必拥有每个工作进程的内存对象的副本,反正无论如何都要进行反序列化。

在这种情况下,您选择了快速序列化/反序列化选项的关键。