I recently found a problem that at first did not seem so dubious. Let me start by describing a big picture of my environment:
I have a domain model that matches a table module architecture that consumes a data access layer written using Entity Framework 6.x. My application is an application for Windows forms, and the domain model and the data access level are running on the client and I am using .NET 4.0 (fortunatelly, EF 6 is still compatible with .NET 4.0)
My goal: Create a commmon nomenclite cache that is commonly used in combo boxes / lookups. This cache will be updated at the request of our users (there is a button to the right of each control that represents items that can update the cache).
So far so good. I started writing this cache. In a few words, my cache consists of a set of TableCaches <T>, and each of them can get a list from memory or from a database (if something has been changed).
Then imagine that you have a business like this:
public class PersonsModule { public List<Person> GetAllByCityId(int cityId) { using (var ctx = new Container()) { return (from p in ctx.Persons join addr in ctx.Addresses on p.AddressId equals addr.Id where addr.CityId == cityId select p ).ToList(); } } }
In my mind the idea began to grow: what if I can do a trick so that my βContainerβ sometimes gives fake collections, collections found in my cache ?. But here I found the biggest problem: the .NET compiler does something complicated at compile time: it checks to see if your collections are IQueriable <OfSomething>. If true, it burns inside IL code calls for extension methods that deal with the expression tree, such as calls, otherwise it just calls LINQ on the object extension methods. I also tried (for research purposes only):
public class Determinator<TCollectionTypePersons, TCollectionTypeAddresses> where TCollectionTypePersons : IEnumerable<Person> where TCollectionTypeAddresses : IEnumerable<Address> { public List<Person> GetInternal(TCollectionTypePersons persons, TCollectionTypeAddresses addresses, int cityId) { return (from p in persons join addr in addresses on p.AddressId equals addr.Id where addr.CityId == cityId select p ).ToList(); } }
and wrote in my module:
public class PersonsModule { private ICache _cache; public PersonsModule(ICache cache) { _cache = cache; } public PersonsModule() { } public List<Person> GetAllByCityId(int cityId) { if (_cache == null) { using (var ctx = new Container()) { var determinator = new Determinator<IQueryable<Person>, IQueryable<Address>>(); return determinator.GetInternal(ctx.Persons, ctx.Addresses, cityId); } } else { var determinator = new Determinator<IEnumerable<Person>, IEnumerable<Address>>(); return determinator.GetInternal(_cache.Persons, _cache.Addresses, cityId); } } }
Why have I tried this? I just hoped that at run time they would emit the correct extension methods for MSIL extensions whenever he sees that type type parameters are actually IQueryable <T>. But, unfortunately, this naive attempt proved to me that I forgot some deep things related to how the CLR and .NET compiler works. I remember that in the .NET world you should expect compilation in two stages: step 1 is a normal compilation that also contains syntax sugar resolution (type inference is allowed, anonymous types are generated, anonymous functions are converted to real methods for some anonymous types or perhaps on our types, etc.). Unfortunately for me, all LINQ expressions are found in this category.
The second stage was found at run time, when the CLR performs some additional modifications to the MSIL code for various reasons: a new generic type is generated, expression trees are compiled, user code creates new types / methods at runtime, etc.
The last thing I tried is ... I said OK. I will consider all collections as IQueryable. It's nice that no matter what you do (database calls or memory calls), the compiler will call the LINQ extension methods. It works, but it is slow because at the end the expression is compiled every time (even for memory collections). Code below:
public class PersonsModuleHelper { private IQueryable<Person> _persons; private IQueryable<Address> _addresses; public PersonsModuleHelper(IEnumerable<Person> persons, IEnumerable<Address> addresses)## Heading ## { _persons = persons.AsQueryable (); _addresses = addresses.AsQueryable (); } private List<Person> GetPersonsByCityId(int cityId) { return (from p in _persons join addr in _addresses on p.AddressId equals addr.Id where addr.CityId == cityId select p ).ToList(); } }
In the end, I wrote the code below that works, but .. hell, I duplicate my code !!!
public class PersonsModuleHelper { private bool _usecache; private IEnumerable<Person> _persons; private IEnumerable<Address> _addresses; public PersonsModuleHelper(bool useCache, IEnumerable<Person> persons, IEnumerable<Address> addresses) { _usecache = useCache; _persons = persons; _addresses = addresses; } private List<Person> GetPersonsByCityId(int cityId) { if (_usecache) { return GetPersonsByCityIdUsingEnumerable(cityId); } else { return GetPersonsByCityIdUsingQueriable(cityId, _persons.AsQueryable(), _addresses.AsQueryable()); } } private List<Person> GetPersonsByCityIdUsingEnumerable(int cityId) { return (from p in _persons join addr in _addresses on p.AddressId equals addr.Id where addr.CityId == cityId select p ).ToList(); } private List<Person> GetPersonsByCityIdUsingQueriable(int cityId, IQueryable <Person> persons, IQueryable <Address> addresses) { return (from p in persons join addr in addresses on p.AddressId equals addr.Id where addr.CityId == cityId select p ).ToList(); } }
What should I do? . I also know that EF does cache, but the lifespan is short (only for the lifetime of your context instance), and this is not at the query level, but only at the row level. Correct me if I am wrong!
Thanks in advance.