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:

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:

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:

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:

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

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:

Caching In Entity Framework Core Using NCache

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

External References