Introduction
It will be explained in this article how to integrate Entity Framework Core with a caching engine using NCache. The article will give a practical example of how we could set up our Entity Framework Core in a Console application and how to make use of NCache to make faster requests to the database with its native in-memory distributed cache.
What is Entity Framework Core?
Entity Framework Core is Microsoft's most recent ORM - Object Relational Mapper, that helps software applications map, connect, and manage entities to a wide range of databases. Entity Framework Core is open source and cross-platform, being the top 1 ORM used by software using Microsoft technologies.
At the moment of writing this article, Entity Framework Core offers two ways to connect your entities to the database:
- Code First, writing your project's entities first and then reflecting those objects in the database;
- Database First, have your database created first and then generate your project's entities.
What is NCache?
NCache is also open-source and cross-platform software. Its cache server offers a scalable in-memory distributed cache for .NET, Java, Scala, Python, and Node.js. As this article will be focusing on .NET technologies, we can use NCache to take advantage of the following usages:
- ASP.NET session state storage;
- ASP.NET view state caching;
- ASP.NET output cache;
- Entity Framework cache;
- NHibernate second-level cache.
NCache with Entity Framework Core
We can add a layer of cache between the Entity Framework Core and our application with NCache, this would improve our queries response time and reduce the necessity of round trips to the database as far as we would get data from NCache cached entities.
Caching Options
NCache gives the possibility to have a different set of options to be sent from each request, meaning that we can use the cache differently based on the result set that we are working with in order to be more efficient.
As we are going to see in the practical samples, we must provide the cache options on each request to NCache and those options are the following:
- AbsoluteExpirationTime, sets the absolute time when the cached item will expire;
- CreateDbDependency, creates or not a database dependency from the result set;
- ExpirationType, sets the expiration type:
- IsSyncEnabled, sets if the expired items must be re-synced with the database.
- Priority, sets the relative priority of items stored in the cache.
- Normal,
- Low,
- BelowNormal,
- AboveNormal,
- High,
- NotRemovable,
- Default
- QueryIdentifier, result set identifier.
- ReadThruProvider, sets the read thru provider for cache sync
- SlidingExpirationTime, sets the sliding expiration time
- StoreAs, sets how the items are to be stored.
- Collection
- SeperateEntities
Deferred Calls
NCache has its own extension methods for us to work with Entity Framework Core deferred calls and they are in 3 different groups:
- Aggregate Operators, making operations against collections. Can be used with both FromCache and FromCacheOnly methods.
- DeferredAverage.
- Products.Select(o => o.UnitPrice).DeferredAverage()
- DeferredCount
- Customers.Select(c => c.Country).GroupBy(c => c).DeferredCount()
- DeferredMin
- Orders.Where(o => o.CustomerId == "VINET").Select(o => o.RequiredDate).DeferredMin()
- DeferredMax
- Orders.Select(o => o.RequiredDate).DeferredMax()
- DeferredSum
- OrderDetails.Select(o => o.UnitPrice).DeferredSum()
- Element Operators, making operations for single elements. Can be used only with the FromCache method.
- DeferredElementAtOrDefault
- Customers.DeferredElementAtOrDefault(c => c.City == "London")
- DeferredFirst
- Customers.DeferredFirst(c => c.ContactTitle == "Sales Representative")
- DeferredFirstOrDefault
- Customers.DeferredFirstOrDefault(c => c.ContactTitle == "Sales Representative")
- DeferredLast
- Customers.DeferredLast(c => c.City == "London")
- DeferredLastOrDefault
- Customers.DeferredLastOrDefault(c => c.City == "London")
- DeferredSingle
- Customers.DeferredSingle(c => c.CustomerId == "ALFKI")
- DeferredSingleOrDefault
- Customers.DeferredSingleOrDefault(c => c.CustomerId == "ANATR")
- Others. Can be used only with the FromCache method.
- DeferredAll
- Products.DeferredAll(expression)
- DeferredLongCount
- Products.DeferredLongCount()
- DeferredContains
- Products.DeferredContains(new Products { ProductId = 1 })
Caching Methods
NCache's methods to manipulate cached objects:
Insert a single object in the cache with its own options. Returns the cache key
var customerEntity = new Customers
{
CustomerId = "HANIH",
ContactName = "Hanih Moos",
ContactTitle = "Sales Representative ",
CompanyName = "Blauer See Delikatessen"
};
//Add customer entity to database
database.Customers.Add(customerEntity);
database.SaveChanges();
//Caching options for cache
var options = new CachingOptions
{
QueryIdentifier = "CustomerEntity",
Priority = Runtime.CacheItemPriority.Default,
};
//Add customer entity to cache
Cache cache = database.GetCache();
cache.Insert(customerEntity, out string cacheKey, options);
Remove a single object from the cache.
var cust = new Customers
{
CustomerId = "HANIH",
ContactName = "Hanih Moos",
ContactTitle = "Sales Representative",
CompanyName = "Blauer See Delikatessen"
};
cache.Remove(cust);
Remove an object by passing its cache key
cache.Remove("cacheKey");
Remove all entities from the cache which match the query identifier
Tag tag = new Tag(queryIdentifier);
cache.RemoveByQueryIdentifier(tag);
Caching using NCache extension methods
NCache's Extension methods for Entity Framework Core
Gets the cache instance.
using (var context = new NorthwindContext())
{
Cache cache = context.GetCache();
}
If there is cached data, then it will be returned without going through the data source. If there is no data cached, then data will be returned from the data source and cached.
var options = new CachingOptions
{
StoreAs = StoreAs.SeperateEntities
};
var resultSet = (from cust in context.Customers
where cust.CustomerId == 10
select cust).FromCache(options);
Returning the cacheKey from the result set
var options = new CachingOptions
{
StoreAs = StoreAs.Collection
};
var resultSet = (from cust in context.Customers
where cust.CustomerId == 10
select cust).FromCache(out string cacheKey, options);
Every request goes first to the data source, caches its result set, and returns it.
var options = new CachingOptions
{
StoreAs = StoreAs.SeperateEntities
};
var resultSet = (from custOrder in context.Orders
where custOrder.Customer.CustomerId == 10
select custOrder)).LoadIntoCache(options);
Returning the cache key from the result set
var options = new CachingOptions
{
StoreAs = StoreAs.Collection
};
var resultSet = (from custOrder in context.Orders
where custOrder.Customer.CustomerId == 10
select custOrder)).LoadIntoCache(out string cacheKey, options);
Never goes to the data source. The request is only going to the cache, if no matching result is cached, then it will be returned as an empty result set.
Includes and joins are not supported by FromCacheOnly().
var resultSet = (from cust in context.Customers
where cust.CustomerId == someCustomerId
select cust).FromCacheOnly();
NCache Implementation Step by Step
0. Pre-Requisites
1. The application
- Create a C# console application targeting .NET 6.0 and install the following nugets packages:
- EntityFrameworkCore.NCache
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.SqlServer.Design
- Microsoft.EntityFrameworkCore.Tools
- System.Data.SqlClient
- System.Collections
- The following NCache files will be inserted into your project after installing the Nuget Packages.
- client.ncconf
- config.ncconf
- tls.ncconf
2. Models
For this sample, it was created a very simple model relationship, as follows:
[Serializable]
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public List<Transaction> Transactions { get; set; }
public Store Store { get; set; }
public int? StoreId { get; set; }
}
[Serializable]
public class Store
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public ICollection<Product> AvailableProducts { get; set; }
public ICollection<Consumer> RegularConsumers { get; set; }
}
[Serializable]
public class Consumer
{
public int Id { get; set; }
public string Name { get; set; }
public Store FavouriteStore { get; set; }
public int? FavouriteStoreId { get; set; }
public List<Transaction> Transactions { get; set; }
}
[Serializable]
public class Transaction
{
public int Id { get; set; }
public Consumer Consumer { get; set; }
public int ConsumerId { get; set; }
public Product Product { get; set; }
public int ProductId { get; set; }
}
The DBContext class has NCache initialization settings and the model's relationship.
public class SampleDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// configure cache with SQLServer DependencyType and CacheInitParams
CacheConnectionOptions initParams = new CacheConnectionOptions();
initParams.RetryInterval = new TimeSpan(0, 0, 5);
initParams.ConnectionRetries = 2;
initParams.ConnectionTimeout = new TimeSpan(0, 0, 5);
initParams.AppName = "appName";
initParams.CommandRetries = 2;
initParams.CommandRetryInterval = new TimeSpan(0, 0, 5);
initParams.Mode = IsolationLevel.Default;
NCacheConfiguration.Configure("democache", DependencyType.SqlServer, initParams);
optionsBuilder.UseSqlServer(@"Data Source=DESKTOP-AT3H2E;Initial Catalog=sampleDatabase;Integrated Security=True");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Store>()
.HasMany(x => x.AvailableProducts)
.WithOne(x => x.Store)
.HasForeignKey(x => x.StoreId)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<Store>()
.HasMany(x => x.RegularConsumers)
.WithOne(x => x.FavouriteStore)
.HasForeignKey(x => x.FavouriteStoreId)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
modelBuilder.Entity<Transaction>()
.HasOne(x => x.Consumer)
.WithMany(x => x.Transactions)
.HasForeignKey(x => x.ConsumerId)
.IsRequired(false);
modelBuilder.Entity<Transaction>()
.HasOne(x => x.Product)
.WithMany(x => x.Transactions)
.HasForeignKey(x => x.ProductId)
.IsRequired(false);
}
public DbSet<Store> Stores { get; set; }
public DbSet<Consumer> Consumers { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Transaction> Transactions { get; set; }
}
3. NCache Methods
Here is the class with NCache methods needed to manipulate objects from and to the cache.
public class NCacheExtensions
{
private SampleDbContext Database { get; set; }
private CachingOptions CachingOptions { get; set; }
private Cache Cache { get; set; }
public NCacheExtensions(SampleDbContext database)
{
this.Database = database;
this.CachingOptions = new CachingOptions
{
QueryIdentifier = "Sample QueryIdentifier",
Priority = Alachisoft.NCache.Runtime.CacheItemPriority.Default,
CreateDbDependency = false,
StoreAs = StoreAs.Collection
};
Cache = database.GetCache();
}
public string AddSingleEntity<T>(T entity)
{
Cache.Insert(entity, out string cacheKey, this.CachingOptions);
return cacheKey;
}
public void RemoveSingleEntity<T>(T entity)
{
Cache.Remove(entity);
}
public void RemoveSingleEntity(string cacheKey)
{
Cache.Remove(cacheKey);
}
public void RemoveByQueryIdentifier(string queryIdentifier)
{
var tag = new Tag(queryIdentifier);
Cache.RemoveByQueryIdentifier(tag);
}
public IEnumerable<Consumer> GetAllConsumersFromCache(CachingOptions cachingOptions)
{
return Database.Consumers.Include(x => x.Transactions).ThenInclude(x => x.Product).FromCache(cachingOptions);
}
public async Task<IEnumerable<Consumer>> GetAllConsumersFromCacheAsync(CachingOptions cachingOptions)
{
return await Database.Consumers.Include(x => x.Transactions).ThenInclude(x => x.Product).FromCacheAsync(cachingOptions);
}
public IEnumerable<Consumer> LoadAllConsumersIntoCache(CachingOptions cachingOptions)
{
return Database.Consumers.Include(x => x.Transactions).ThenInclude(x => x.Product).LoadIntoCache(cachingOptions);
}
public async Task<IEnumerable<Consumer>> LoadAllConsumersIntoCacheAsync(CachingOptions cachingOptions)
{
return await Database.Consumers.Include(x => x.Transactions).ThenInclude(x => x.Product).LoadIntoCacheAsync(cachingOptions);
}
public IEnumerable<Consumer> GetAllConsumersFromCacheOnly(CachingOptions cachingOptions)
{
return Database.Consumers.FromCacheOnly();
}
}
4.The program.cs class
Here we have the start point of our console application. With an example on how to connect to NCache and use its extension methods that were provided above.
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
using (var context = new SampleDbContext())
{
var cachedContext = new NCacheExtensions(context);
Console.WriteLine("start LoadAllConsumersIntoCache " + DateTime.Now.ToString("HH:mm:ss.f"));
var loadInCache = cachedContext.LoadAllConsumersIntoCache(new CachingOptions { StoreAs = StoreAs.Collection, QueryIdentifier = "Sample QueryIdentifier" });
Console.WriteLine("finish LoadAllConsumersIntoCache" + DateTime.Now.ToString("HH:mm:ss.f"));
Console.WriteLine("start GetAllConsumersFromCache " + DateTime.Now.ToString("HH:mm:ss.f"));
var getFromCache = cachedContext.GetAllConsumersFromCache(new CachingOptions { Priority = Alachisoft.NCache.Runtime.CacheItemPriority.Default });
Console.WriteLine("finish GetAllConsumersFromCache " + DateTime.Now.ToString("HH:mm:ss.f"));
Console.WriteLine("start load from DBContext " + DateTime.Now.ToString("HH:mm:ss.f"));
var getFromDb = context.Consumers.Include(x => x.Transactions).ThenInclude(x => x.Product);
Console.WriteLine("finishg load from DBContext " + DateTime.Now.ToString("HH:mm:ss.f"));
var cachedEntity = cachedContext.AddSingleEntity<Consumer>(getFromDb.FirstOrDefault());
Console.WriteLine("cache key: " + cachedEntity);
cachedContext.RemoveSingleEntity(cachedEntity);
cachedContext.RemoveByQueryIdentifier("Sample QueryIdentifier");
}
}
}
Application working:
Congratulations! You have successfully made usage of NCache for caching your Entity Framework Core data.
External References