Caching In Entity Framework Core Using NCache

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;
    • 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 = "HANIH",
        ContactName = "Hanih Moos",
        ContactTitle = "Sales Representative",
        CompanyName = "Blauer See Delikatessen"
    };

    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

  • GetCache

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

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:

Caching In Entity Framework Core Using NCache

Congratulations! You have successfully made usage of NCache for caching your Entity Framework Core data.

External References