Caching In Entity Framework (EF) Core Using NCache

Caching is a proven technique used at various levels in web applications to improve performance and responsiveness. A cache is a high-speed data storage layer that saves a subset of data, often temporary in nature so that subsequent requests for that data are served up faster than accessing the data's primary storage location.

Entity Framework Core is a cross-platform version of the popular Entity Framework data access technology that is lightweight, extendable, and open source.

EF Core can be used as an object-relational mapper (O/RM), which can do the following:

  • Allows .NET developers to use .NET objects to interact with a database.
  • Removes the requirement for most of the data-access code that is generally required.

However, during peak loads, high-transaction .NET Core apps using EF Core have performance and scalability problems in the database tier. This is because, although you can scale the application layer by adding more application servers, you can't scale the database tier by adding more database servers.

You can quickly remove these performance and scalability limitations and manage severe transaction loads by using a distributed cache like NCache in your .NET Core apps.

Why should you use NCache as your EF Core Cache?

Entity Framework Core lacks the caching framework (like NHibernate does). NCache, on the other hand, has created a flexible, powerful, and yet easy caching framework for you. It is recommended for the following reasons:

  • Ideal multi-server environments
    NCache is a multi-server distributed cache that performs really well in terms of scalability and availability. If you have a high-transaction .NET Core application, running in a multi-server scenario, NCache is an ideal choice.
     
  • Millisecond-Scale latency
    NCache is an efficient in-memory distributed caching solution that provides sub-millisecond response times.
     
  • Linear scalability
    NCache allows you to add more servers to the cache cluster and handles high transaction loads without any performance degradation. Unlike a traditional database, NCache never becomes the scalability bottleneck.
     
  • Expandable cache size
    You can easily increase the cache storage by simply adding more servers to the cluster. NCache provisions cache partitioning and pools the memory of all cache servers together for it. In this way, the cache storage increases without any additional configuration.
     
  • Data replication
    NCache implements an intelligent replication strategy without compromising the speed. So even if a cache server goes down due to failure or any other reason, NCache ensures high availability through data replication.
     
  • 100% uptime
    NCache has a dynamic cache cluster that self-heals with no single point of failure. Consequently, you can add or remove cache servers at runtime without stopping your application or the cache.

Implementing Caching in Entity Framework Core With NCache

Before connecting to our NCache server, we need to first install the Nuget package. Let's Install the package by executing the following command in the Package Manager Console.

Install-Package EntityFrameworkCore.NCache

This package is an addition to the usual Entity Framework Core packages.

Configure Entity Framework Core with NCache

We need to configure the NCache provider accordingly while registering our DatabaseContext in the Startup.

public void ConfigureServices(IServiceCollection services) {
    services.AddDbContext < ApplicationDbContext > (optionsBuilder => {
        string cacheId = "myClusteredCache";
        NCacheConfiguration.Configure(cacheId, DependencyType.SqlServer);
        NCacheConfiguration.ConfigureLogger();
        optionsBuilder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    });
}

The Cache information (the Server IP, Port, and other information) is usually maintained in a configuration file named client.nconf. If NCache cannot find such a file in the execution directory, it searches the default installation path.

Check out my post on setting up the NCache environment on your PC if you haven't already.

Implementing EF Core Caching through Extension Methods

C# extension methods allow you to expand existing classes and types without having to create derived classes. An extension method is a special kind of static method but it is called as if it is an instance method on the extended class.

Through Extension Methods, NCache has integrated its caching features into EF Core. Using NCache from an EF Core application is now very simple and straightforward. NCache provides the following EF Core extension methods.

FromCache()

The FromCache() method checks the cache first for the data before going to the database to fetch the data. If found, it returns it from the cache. Otherwise, it fetches it from the database, caches it to ensure that it is available the next time, and then returns the results to the caller.

You may either cache the entire collection as a single item in the cache or break out all its entities and cache them separately. If you want to fetch or update these entities individually then breaking out is useful.

By caching the result set of your LINQ queries, the FromCache() extension method allows you to store data. Consider the following example.

public async Task < IEnumerable < City >> GetAllAsync() {
    CachingOptions cachingOptions = new CachingOptions {
        StoreAs = StoreAs.SeperateEntities
    };
    var items = (from c in _dbContext.Cities select c).FromCache(cachingOptions).ToList();
    return await Task.FromResult(items);
}

There are two potential values for the property StoreAs.– SeperateEntities and Collection. StoreAs.SeperateEntities guarantees that each entity in the output dataset is saved as a separate entity, whereas Collection stores the whole dataset as a single Collection.

LoadIntoCache()

The LoadIntoCache method is useful in situations where new data from the database is cached each time the method is called. This retrieves and caches a result set from the source before returning it. When you run the same query again, the result is received from the source and cached again, which means that any previous cache data is overwritten every time. This helps to update existing data in the cache when there is a subsequent FromCache call that returns new data.

The LoadIntoCache function is especially useful for frequently updated data, such as client orders, or for sensitive data, such as payment details.

The example below fetches cities from the database and loads the result into a cache collection. It also returns the cache key generated internally which can be saved for future use.

public async Task < IEnumerable < City >> LoadIntoCache() {
    CachingOptions cachingOptions = new CachingOptions {
        StoreAs = StoreAs.Collection
    };
    var items = (from c in _dbContext.Cities select c).LoadIntoCache(out string cacheKey, cachingOptions);
    return await Task.FromResult(items);
}

FromCacheOnly()

FromCacheOnly just queries the entities in cache and never attempts to access the data source. If the entity is already in the cache, the result set will be returned for any subsequent FromCacheOnly() calls directly from the cache. It is important to note that, in order to use FromCacheOnly to query data from the cache the query indexes must be configured.

If the entity is not in the cache, it will still not be fetched from the data source, and the LoadIntoCache or FromCache method can be used to load the result set into the cache.

After you’ve loaded the cache with all the data, you can run LINQ queries on it instead of the database. This will improve your application performance and scalability while reducing pressure on your database.

This API only works when entities are stored separately, i.e. the caching option StoreAs is set to SeperateEntities.

The example below fetches a list of cities from the cache if the city entities exist, if not, the result set returned will be empty.

public async Task < City > GetAsync(int id) {
    var item = (from c in _dbContext.Cities where c.Id == id select c).FromCacheOnly();
    return await Task.FromResult(item.FirstOrDefault());
}

Limitations of FromcacheOnly

FromCacheOnly supports aggregate functions only. Group By, Order By Ascending, Order By Descending, Select with Group By only, Sum, Min, Max, Average, Count functions are supported by FromCacheOnly. However, these are immediate functions whose result is cached, not the entities themselves. NCache provides Query Deferred API, which defers the query to resolve so that the entities in the cache can be accessed to obtain the result.

  • Except for Count, all aggregate functions need integer values. Count requires an entity to count.
  • More than one aggregate function cannot be used in a single LINQ expression for FromCacheOnly.
  • OrderBy (both ascending and descending) can only be used in a LINQ expression for FromCacheOnly if GroupBy is used.
  • More than one GroupBy and OrderBy operators cannot be used in a single LINQ expression for FromCacheOnly.
  • Joins are not supported so the Include() method will not work.

Updating the Cache: EF Core Cache Class

You can update the EF Core Cache whenever you make changes to your Entities in EF Core, by obtaining the "Cache" handle and making the appropriate update method call.

public async Task AddAsync(City entity) {
    // Add record to database
    _dbContext.Cities.Add(entity);
    await _dbContext.SaveChangesAsync();
    // Get the cache string cacheKey;
    var cache = _dbContext.GetCache();
    CachingOptions options = new CachingOptions {
        StoreAs = StoreAs.SeperateEntities
    };
    // Add to cache (without querying the database)
    cache.Insert(entity, out cacheKey, options);
}

Similarly, we implement DeleteAsync() method where the record is deleted simultaneously from the database as well as the cache.

public async Task DeleteAsync(int id) {
    var entity = _dbContext.Cities.Find(id);
    _dbContext.Cities.Remove(entity);
    await _dbContext.SaveChangesAsync();
    var cache = _dbContext.GetCache();
    cache.Remove(entity);
}

You can directly add, edit, or remove entities from EF Core by obtaining a Cache class handle. And then updated data shows up in your LINQ queries. This gives you a lot more control over updating entities when you modify data yourself. Below is the interface of the Cache class.

Summary

NCache is a simple, interesting, and reliable caching solution for EntityFrameworkCore. This improves application speed and database costs by reducing total turnaround time for client queries. It is ideal to ensure that a larger chunk of generic data is cached, but user-specific data such as thumbnails, profile information, and so on could be persisted via cookies or tokens rather than caches.