Recentelly I found an issue that at the beginning didn't appeared so dubious. Let me start by describing the big picture of my environment:
I have a domain model respecting the Table Module architecture that consumes a data access layer written using Entity Framework 6.x. My application is a windows forms application, both domain model and data access layer runs on client and I am using .NET 4.0 (fortunatelly, EF 6 is still compatible with .NET 4.0)
My goal: Create a cache of commmon nomenclators that are commonly used in combo boxes / lookups. This cache will be refreshed on demand by our users (there is a button on the right of every control that presents a nomenclator that can refresh the cache).
So far, so good. I've started to write this cache. In just a few words, my cache consists of a collection of TableCaches< T> instances and each of them is able to get a List from memory or from database (if something has been changed).
Next, 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 an ideea started 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 issue: The .NET compiler does something tricky at compile time: It checks whether your collections are IQueriable < OfSomething >. If true, it burns inside IL code calls to extention methods that deals with expression tree like calls, otherwise it will simple call LINQ to Objects extention methods. I also tried (just for research purposes) this:
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 I've tried this? I just hoped that the runtime will emit correct MSIL extention methods calls whenever it sees that the generic type parameters are actually IQueryable< T>. But unfortunatelly this naive try proved me that I forgot some of the deep things related to how CLR and .NET compiler works. I remembered that in .NET world, you should expect compilation in two steps: step 1 is the normal compilation that contains also syntactic sugar resolution (type inference is resolved, anonymous types are generated, anonymous functions are transformed into real methods on some anonymous types or maybe on our types, etc. ). Unfortunatelly for me, in this category are found all LINQ expressions.
The second step is found at runtime, when CLR does some additional MSIL code emition from various reasons : A new generic type gets emitted, Expression trees are compiled, user code creates new types / methods at runtime, etc.
The last thing I've tried is ... I said OK I will treat all collections as IQueryable. The nice thing is that no matter what you will do (database calls or in memory calls) the compiler will emit calls to Expression trees LINQ extention methods. It works BUT it is veeeeery slow because in the end the expression gets compiled every time (even for in memory collections). The code is 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.. damn it, 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 make a cache but the lifetime is short (only for the lifetime of your context instance) and it's not at query level but only at row level. Correct me if I am wrong!
Thanks in advance.
Why not using an existing library with caching instead to make your own?
EF+ Query Cache
Support
The library is open source, so you might find some good information if you still want to implement your own cache.
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
).FromCache().ToList();
}
Disclaimer: I'm the owner of the project EF+ on GitHub
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With