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;
Data type: Datetime
CreateDbDependency, creates or not a database dependency from the result set;
Data type: boolean
ExpirationType, sets the expiration type:
Absolute,
Sliding,
None.
IsSyncEnabled, sets if the expired items must be re-synced with the database.
Data type: boolean
Priority, sets the relative priority of items stored in the cache.
Normal,
Low,
BelowNormal,
AboveNormal,
High,
NotRemovable,
Default
QueryIdentifier, result set identifier.
Data type: string.
ReadThruProvider, sets the read thru provider for cache sync
Data type: string
SlidingExpirationTime, sets the sliding expiration time
Data type: TimeSpan
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
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 (object Entity)
Remove a single object from the cache.
var cust = new Customers
{
CustomerId = "PETER",
ContactName = "Peter Scott",
ContactTitle = "Sales Representative",
CompanyName = "Hostforlife"
};
cache.Remove(cust);
Remove (string cacheKey)
Remove an object by passing its cache key
cache.Remove("cacheKey");
RemoveByQueryIdentifier
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();
}
FromCache
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);
LoadIntoCache
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);
FromCacheOnly
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
Have NCache running on your machine. Access your NCache through http://localhost:8251/
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:
Product
[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; }
}
Store
[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; }
}
Consumer
[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; }
}
Transaction
[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; }
}
DBContext
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: