2012-12-18 16 views
12

Attualmente sto provando il nuovo MemoryCache in .Net 4 per memorizzare alcuni bit di dati in una delle nostre app. Il problema che ho è che gli oggetti vengono aggiornati e la cache sembra persistere le modifiche, ad es.Come staccare il riferimento all'oggetto su MemoryCache

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; 
} 

Se mi stavano chiamando GetWithIndentation() prima e poi chiamando GetFromDatabase() mi aspetterei che per tornare all'elenco originale dei SomeObject ma invece restituisce gli elementi modificati (con "-" prefisso del nome).

Ho pensato che ToList() ha distrutto il riferimento ma sembra continuare a persistere. Sono sicuro che sia ovvio ma qualcuno può sapere dove sto andando male?

+4

Si stanno facendo una copia della collezione, ma non gli oggetti al suo interno. –

+0

Ah buon punto, grazie. Ora mi chiedo il modo migliore per farlo, sarebbe di fare una copia profonda o c'è un modo per dire a MemoryCache di ignorare le modifiche successive? – Tim

+1

O modificarlo prima di caching o mai. Oltre a questo è un cambiamento così banale, dovrebbe probabilmente essere fatto nel tuo livello di presentazione. –

risposta

0

Nella memoria gli oggetti memorizzati nella cache sono memorizzati nello stesso spazio del processo del client della cache. Quando un client della cache richiede un oggetto memorizzato nella cache, il client riceve un riferimento all'oggetto memorizzato nella cache locale anziché una copia.

L'unico modo per ottenere una copia pulita dell'oggetto è implementare un meccanismo clone personalizzato (ICloneable, Serialization, Automapping, ...). Con quella copia sarai in grado di modificare il nuovo oggetto senza alterare l'oggetto genitore.

A seconda del caso d'uso, in genere non è consigliabile aggiornare un oggetto nella cache.

+0

"A seconda del caso d'uso, in genere non è raccomandato aggiornare un oggetto nella cache." Ti dispiacerebbe approfondire perché è così? Come esempio banale, dì che hai un elenco di utenti memorizzati nella cache. Gli utenti non cambierebbero di rado, ma a un certo punto cambierebbero (ad esempio, aggiornamento nome/cognome, indirizzo, ecc.) - questo sarebbe un buon candidato per archiviare una raccolta di utenti nella cache e aggiornarli quando necessario? –

8

Ho creato una classe ReadonlyMemoryCache per risolvere questo problema. Esso eredita da .NET 4.0 MemoryCache, ma gli oggetti vengono memorizzati in sola lettura (in base al valore) e non possono essere modificati. Copio in profondità gli oggetti prima di archiviare usando la serializzazione binaria.

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); 
      } 
     } 
    } 



} 
+0

Questo sembra un vincitore, la classe DeepCopyExtensionMethods è ciò che risolve il problema. – user917170

+0

Bello!Non sono sicuro del perché chiamare "DeepCopy" nei metodi "Get" sarebbe comunque necessario, o fare la differenza. Se l'oggetto memorizzato nella cache è stato aggiornato altrove, la clonazione nel metodo "Get" è troppo tardi ... i dati al suo interno sono già stati incasinati. –

+0

@GrantWinney Se rimuovi il deepcopy da Get() vedrai che non funziona. Sono d'accordo che non dovrebbe essere necessario. – Sire

0

Perché non archiviare semplicemente come json o una stringa? Questi non sono passati per riferimento e quando uscirai dalla cache ne riceverai una nuova copia :) Sono qui per essere sfidato come quello che sto facendo atm!

+0

Perché si pagherebbe il costo di de-serializzazione su ogni Get – TheLazyDogsBack

0

È possibile semplificare la deserializzazione e la serializzazione di nuovo e ottenere l'oggetto cache "Per valore".

si può fare in questo modo con Newtonsoft lib (appena ottenere da NuGet)

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