5

I want to cache a data collection in asp.net mvc and filter this collection without to changed the cached collection.

Is it possible to do this?

Because for now, when I get my cached collection I get a reference to it and filter data of this collection change the cached collection.

Does anyone know?

Dragouf
  • 4,676
  • 4
  • 48
  • 55
  • 1
    http://stackoverflow.com/questions/78536/cloning-objects-in-c-sharp/78551#78551 – VJAI Jun 04 '12 at 12:59
  • Great link, I finnally handle it by create copy of every sublevel objects and call clone method on main object juste after to retrieve it from cache. – Dragouf Jun 05 '12 at 13:15

9 Answers9

4

Simply copy the items from your cached list to a new list. The instances will be the same but the original (cached) list will not be touched. Something like this:

    List<[CLASS]> collectionCached; //assuming this this your cached list
    List<[CLASS]> collectionWorking = new List<[CLASS]>();
    collectionWorking.AddRange(collectionCached);

This will allow you to filter out any instances you want without touching the original (cached) list.

EDIT:

After further clarification from OP, it seems there is a need to make a copy of the entities themselves. The basic idea behind making a copy of a reference object is called "Cloning". This makes a copy of the original entity and allows you to change the copy without changing the original instance. SO has some good links, here is one that discusses both the concept of cloning and LINQ.

How to get a copy of data instead of a reference using linq/lambda in c#?

This should get you on the right track. Let me know if you have any other questions.

Community
  • 1
  • 1
Shai Cohen
  • 6,074
  • 4
  • 31
  • 54
  • I tried it but it didn't work. If I change an object from collectionWorking it change from collectionCached... – Dragouf Apr 17 '12 at 08:17
  • In your original question, you said you wanted to **filter** the collection. Changing the actual instance of a class is a completely different thing. Let's take a step back. Can you edit your original question and add more details/background of why you need these collections? – Shai Cohen Apr 17 '12 at 16:01
  • 1
    I want to filter the collection with linq without to modify the cached collection. But I need to modify some object inside of the collection during filtering process without to modify original cached objects. – Dragouf Apr 18 '12 at 08:09
  • Did you ever get a chance to implement the code from the link in the answer? Did it work for you? – Shai Cohen Jun 04 '12 at 15:52
  • I posted the complete answer for this. – Dragouf Jun 05 '12 at 13:31
  • This is not the solution, it doesn't copy anything because each object in the list is copied by reference. You need to deep clone as per https://stackoverflow.com/questions/12559219/read-httpruntime-cache-item-as-read-only – Max Favilli Nov 09 '17 at 10:00
3

Cache the full list, and then use Linq to filter the collection.

var list = GetFromCache();
var filtered = list.Where(x => x.Name.Contains("rob"));

EDIT:

When you get this filtered list, are you changing the objects contained WITHIN the list? Yes, that would be reflected for the same instance in the cached list as well. But at that point it sounds like this isn't great data to be caching. Usually cached data doesn't change, or you can live without seeing changes for some time. If data accuracy is more important, don't cache.

You might be able to get what you want simply though, if you are changing the state of items in your lists:

var filtered = list.Where(x => x.Name.Contains("rob")).Select(x => x.MemberwiseClone()).Cast<YourObjType>();

This uses MemberwiseClone which does a shallow copy; if that's not good enough, you'll need do some work on a method that does a keep copy. It returns Object, so I use the Cast extension method to get it back to the type of object originally in the list.

Andy
  • 8,432
  • 6
  • 38
  • 76
  • 2
    It shouldn't, `Where` (and all other LINQ utilities) do not change the object they're called on, but iterate through and return a new instance of `IEnumerable` with filtered results, leaving the base object intact. Anyway, as the other answer put it, you can always make a local copy of the collection and you're good to go. :) – Patryk Ćwiek Jun 01 '12 at 12:13
  • @Dragouf There must be more to your code that you're not showing us as all Linq extension methods return a new instance of IEnumerable. – Andy Jun 01 '12 at 12:21
  • @Dragouf I've updated my answer with more information that might help. – Andy Jun 01 '12 at 12:27
  • I think my problem is that each of my object contains other collection of objects on 3 or 4 different level. So nested objects are still references. – Dragouf Jun 01 '12 at 12:38
  • @Dragouf Well, that should only be a problem if you modify something; if you are, the cloning would have to be deep, but that should solve your issues. – Andy Jun 04 '12 at 20:01
3

What Malcolm Frexner is saying is a nice clean way to solve your problem but can be greatly simplified using AutoMapper.

Say you have an Adapter that you are pulling DTO objects from (or if you aren't using WCF if you are pulling it directly from the database, the same concept applies)

public class AccountServiceAdapter
{
     private List<Accounts> _accounts
     {
          get
          {
               if(HttpContext.Current.Cache["accounts"] == null)
                    HttpContext.Current.Cache["accounts"] = default(List<Accounts>());
               return (List<Accounts>)HttpContext.Current.Cache["accounts"];
          }
          set
          {
               HttpContext.Current.Cache["accounts"] = value;
          }
     }

     public List<AccountViewModel> FetchAccounts()
     {
          if(_accounts == default(List<Accounts>))
          {
               //Do Fetch From Service or Persistence and set _accounts
          }

          return _accounts.Select(x => x.ConvertToViewModel()).ToList();
     }
}

Well that's your caching piece which you likely already have written. The next bit is where you get to have a bit of fun. I won't explain all the mapping fun of AutoMapper because there's a billion links that do it but I'll give you some snippets to get you going.

https://github.com/AutoMapper/AutoMapper/wiki/Projection

You will need to make a ProjectionManager class after you have added a reference to AutoMapper using NuGet.

public static class ProjectionManager
{
     ProjectionManager()
     {
          //The mapping exercise is AS SIMPLE as this if all you are doing is
          //using the exact same property names, it'll autowire
          //If you use different projections you can create complex maps see the link
          //above.
          Mapper.CreateMap<Account,AccountViewModel>();
          Mapper.CreateMap<AccountViewModel,Account>();
     }

     //Make yourself some handy extension methods for syntactic sugar
     public static AccountViewModel ConvertToViewModel(this Account x)
     {
          return Mapper.Map<Account,AccountViewModel>(x);
     }

     //Make yourself some handy extension methods for syntactic sugar
     public static Account ConvertFromViewModel(this AccountViewModel x)
     {
          return Mapper.Map<AccountViewModel,Account>(x);
     }
}

After you have done all of this your caching is now crazy simple and to boot you can cast your data into view models that have all the neat features like Validation, Display Names, etc without having to clutter up your Entity objects or your DTO object out of your WCF service!

VulgarBinary
  • 3,520
  • 4
  • 20
  • 54
2

This should work:

IEnumerable<T> things = GetThings();

Cache["ThingCache"] = things;

T aThing = ((IEnumerable)Cache["ThingCache"]).First();

//Cache["ThingCache"] should still contain the original collection.
Paddy
  • 33,309
  • 15
  • 79
  • 114
2

I finally resolve my problem by creating a Clone() method in each of my objects and call Clone () method on my main object just after I retrieved it from cache.

My main object is of type account which contain himself list of domains, which contain list of product, etc...

Here is the Clone method in the Account object :

    public Account Clone()
    {
        // Copy all properties of the object in a new object account
        var account = (Account)this.MemberwiseClone();

        var domainsList = new List<Domain>();
        foreach (var d in this.Domains)
        {
            domainsList.Add(d.Clone());
        }

        account.Domains = domainsList;

        return account;
    }

Like you can see I use MemberwiseClone() to copy simple property and then I call Clone again on all complex objects. Clone which is a custom method I created in all of my complex objects.

And now it work fine...

Dragouf
  • 4,676
  • 4
  • 48
  • 55
2

I ran into this when I used my domain models directly in the View. As soon as you put all the displayed data in dedicated ViewModels, you easily work around the problem.

DomainModel

public class PersonInfo
{
     public int Age {get;set;}
     public string Name {get;set;}
}

ViewModel

public class PersonViewModel
{
     public PersonInfo PersonData {get;set;}
     public bool Visible {get;set;}
}

Now you can store PersonInfo in the cache without the risc of storing the property "Visible" for all users.

It gets a bit more difficult if you have lists of objects, but this pattern is applicable there to.

Mathias F
  • 15,906
  • 22
  • 89
  • 159
1

Far easiest is to serialize your objects to json before putting them into the cache and then deserialize when you get them out of there. That way you don't have to make any changes to your classes like implementing iclonable or similar.

Cache.Set(key, JsonConvert.SerializeObject(objectToCache), policy);

and then

var cachedObject = (string)Cache[ComposeKey(key, domain)];
return JsonConvert.DeserializeObject<T>(cachedObject);
Mr W
  • 597
  • 1
  • 7
  • 22
0

This is my go-to code when dealing with cache poisoning:

    var list_from_cache = HttpContext.Current.Cache["females"] as List<Females>;                
    var females = new List<Females>();
    females.AddRange(list_from_cache.Select(n => Cloner(n))); //clone all objects
   //now we can change the objects as much as we want without changing the cache
    females[0].Height=160;

code for cloner, put this somewhere: (remember to put [Serializable] on your class-to-clone)

public static T Cloner<T>(T source) where T : class
{
    if (!typeof(T).IsSerializable)
    {
        throw new ArgumentException("The type must be serializable.", "source");
    }

    // Don't serialize a null object, simply return the default for that object
    if (Object.ReferenceEquals(source, null))
    {
        return default(T);
    }

    IFormatter formatter = new BinaryFormatter();
    Stream stream = new MemoryStream();
    using (stream)
    {
        formatter.Serialize(stream, source);
        stream.Seek(0, SeekOrigin.Begin);
        //return    (T)formatter.Deserialize(stream);
        var clone = formatter.Deserialize(stream) as T;
        return clone;


    }
}
Contra
  • 2,754
  • 4
  • 20
  • 18
0

This can be solved using System.Collections.Immutable.

You are looking for ImmutableList.

Get The nuget-package here: https://www.nuget.org/packages/System.Collections.Immutable/ or: Install-Package System.Collections.Immutable