Reputation: 4274
I'm learning my way around EF, and I know caching is faster than a round trip to the DB for things like state, country, etc. But I'm not sure how to implement it. I was looking at this post (entities from local cache) that mentioned an extension, but is there something built in I should leverage?
I'd like to have a function like this that wouldn't have to go to the db every time:
public static int GetCountryId(string countryCode = "US")
{
if (countryCode == string.Empty)
countryCode = "US"; // Assume US
return db.Country.Where
(
p => p.CountryCode == countryCode
).Select(p => p.CountryId).First();
}
Updated:
I'm now thinking about a GetCached function that would use generics to hold some lookup lists in memory. Somthing like this:
public class Lookups
{
private static MkpContext db = new MkpContext();
public static IEnumerable GetCached(CachedLists list)
{
ObjectCache cache = MemoryCache.Default;
var listOut = cache[list.ToString()] as IEnumerable;
if (listOut != null) return listOut;
switch (list)
{
case CachedLists.Countries:
cache.Set
(
list.ToString(),
db.Country.ToList(),
new DateTimeOffset(DateTime.Now,new TimeSpan(1,0,0,0))
);
break;
default:
return null;
}
listOut = cache[list.ToString()] as IEnumerable;
return listOut;
}
}
public enum CachedLists
{
Countries,
}
But the above solution would leave me with an un-typed Enumerable. I'd love to be able to specify the types somehow, or better yet, do some sort of extension.
Upvotes: 1
Views: 2229
Reputation: 382
For something basic like mapping a country code to a countryId I'd suggest keeping it simple and using a dictionary. If this is happening inside a web application you'll want to use a ConcurrentDictionary to handle multiple threads hitting the code, otherwise a normal dictionary will be fine.
You could also have some code that populates the dictionary when the app starts up to make the experience even snappier for users.
static ConcurrentDictionary<string, int> countryLookup = new ConcurrentDictionary<string, int>();
public static int GetCountryId(string countryCode = "US")
{
if (countryCode == string.Empty)
countryCode = "US"; // Assume US
if (countryLookup.TryGetValue(countryCode, out int countryId))
return countryId;
var countryId = db.Country
.First(p => p.CountryCode == countryCode)
.CountryId;
countryLookup.TryAdd(countryCode, countryId);
return countryId;
}
Upvotes: 0
Reputation: 15772
You can use EntityFramework Plus.
public static int GetCountryId(string countryCode = "US")
{
return db.Country.Where
(
p => p.CountryCode == countryCode
).Select(p => p.CountryId).First();
}
Would probably become...
public static int GetCountryId(string countryCode = "US")
{
using var context = new Context();
var allCountries = context.Country
.FromCache({cachePolicy})
.ToDictionary(x => x.CountryCode);
return allCountries[countryCode];
}
Upvotes: 1
Reputation: 2851
The method you have proposed is usually what I do to return cached data.
However data is cached as an object and therefor you have to return it as the object you expect.
Lets assume that your country class is represented by:
public class Country
{
#region Constructors
public Country(string code, string name)
{
this.Code = code;
this.Name = name;
}
#endregion
#region Properties
public string Name { get; set; }
public string Code { get; set; }
#endregion
}
What we need to do is look up our cache by a given key, if exists return the cache, otherwise get from database - same logic as you did.
Difference is here var listOut = cache[list.ToString()] as IEnumerable;
You want to check whether the value for that key is
of type Country
If we define a GetCache
method as following:
static object GetCache(string cacheKey)
{
if (Cache[cacheKey] is object cachedResult)
{
return cachedResult;
}
return null;
}
What we need to do in order to return a List<Country>
is
if (GetCache(cacheKey) is List<Country> cachedData)
{
return cachedData;
}
Now instead of a list of object we have a List<Country>
I have made a simple console app to show the result - hope it helps:
namespace ConsoleApp3
{
#region Usings
using System;
using System.Collections.Generic;
using System.Runtime.Caching;
#endregion
class Program
{
#region Fields
private static readonly ObjectCache Cache = MemoryCache.Default;
#endregion
static void Main(string[] args)
{
//simulates app life span
for (int i = 0; i < 5; i++)
{
var countries = GetData();
foreach (var country in countries)
{
Console.WriteLine(country.Name);
}
Console.ReadLine();
}
}
static List<Country> GetData()
{
string cacheKey = "Country-Lookup";
if (GetCache(cacheKey) is List<Country> cachedData)
{
return cachedData;
}
// otherwise do some logic stuff and get from DB
//db data simulation
List<Country> coutries = new List<Country>
{
new Country("IT", "Italy"),
new Country("UK", "United Kindom"),
new Country("US", "United States")
};
//add to cache
AddToCache(cacheKey, coutries);
return coutries;
}
static object GetCache(string cacheKey)
{
if (Cache[cacheKey] is object cachedResult)
{
return cachedResult;
}
return null;
}
static void AddToCache(string cacheKey, object dataToCache)
{
if (dataToCache == null)
{
return;
}
Cache.Set(cacheKey, dataToCache, DateTimeOffset.Now.AddMinutes(1));
}
}
}
Upvotes: 1
Reputation: 156554
There are a lot of options, but here's one approach that will work well if users are mostly querying the same few country codes:
Create a MemoryCache instance to use as a static, private, readonly field on your class. In your method, try to get a cached item from this cache if there is one with the given countryCode
as its key. If it's not there, do your database query and Add the result into the cache before returning it.
By the way, the approach in the article you linked probably isn't a very good approach: it will only help if you've already got data in the specific database context instance that you're dealing with, and usually it's best for your contexts to be short-lived. It's also really CPU-intensive to compile an expression into a function, and then run that function against every entity that the context has cached, just to find out whether the item is there or not. If you then find out that it's not there, and you have to go back to the database anyway, you just wasted time and resources.
Upvotes: 1