One of the most popular object-relational mapping (ORM) frameworks in the .NET environment is Entity Framework (EF Core) - the modern, cross-platform version of Microsoft's Entity Framework, which makes managing models and data easier. As applications grow and handle more concurrent users, maintaining scalability and performance becomes a top priority - and this is where distributed caching comes into play.
A distributed cache helps reduce database load by storing frequently accessed objects in memory across multiple servers. This speeds up data retrieval. However, when EF Core uses Plain Old CLR Objects (POCO) along with lazy loading, integrating with a distributed cache can become tricky due to how EF Core manages proxy objects internally.
Let's explore this challenge in general terms, and then see how NCache, a leading in-memory distributed caching solution for .NET, elegantly solves it.
In order to handle the delayed loading of related data, EF Core dynamically creates proxy classes at runtime when lazy loading is enabled. These proxy classes don't exist outside of the current process; they only exist in memory.
In contrast, a distributed cache operates out-of-process and frequently runs on different cache servers. This means objects placed in the cache must be serialized when stored and deserialized when retrieved across nodes.
Here's where the problem arises:
The proxy types used by EF Core are dynamically generated at runtime, making it impossible to serialize or identify them from outside the process. Consequently, caching proxy objects may result in deserialization errors or serialization exceptions on client machines.
Therefore, when you need to cache your entities for improved performance, EF's built-in lazy loading technique poses a problem, even if a distributed cache is necessary for scalability.
The solution lies in disabling EF Core's proxy generation and implementing custom lazy loading that keeps your entities as pure POCOs; making them serializable and cache-friendly. This method guarantees that the data is secure for caching in a distributed setting while maintaining the flexibility of lazy loading.
In EF Core, proxies are generated only when explicitly enabled, so by default, plain POCOs are used. You just need to ensure you're not using the proxy factory and that your entities remain serializable.
Here's how your DbContext looks:
using Microsoft.EntityFrameworkCore;
using EF_poco_lazy_loading.Models;
using System;
namespace EF_poco_lazy_loading.Data
{
public class NorthwindContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "Data Source=JOHN-SMITH\\SQLEXPRESS; Initial Catalog=Northwind; UserID=dbuser; Password=pass123; TrustServerCertificate=True;";
optionsBuilder.UseSqlServer(connectionString, options => options.EnableRetryOnFailure())
optionsBuilder.UseSqlServer(connectionString, options =>
{
options.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: null);
});
}
}
}
Since proxies are disabled now, EF Core won't automatically load related entities (like Orders for a Customer). To simulate lazy loading safely, create a helper class that uses the same EF context to load related data manually.
using Alachisoft.NCache.Client;
using Alachisoft.NCache.Runtime.Caching;
using EF_poco_lazy_loading.Data;
using EF_poco_lazy_loading.Models;
using System.Collections.Generic;
using System.Linq;
namespace EF_poco_lazy_loading.Helpers
{
public static class CustomerHelper
{
public delegate object LazyLoadDelegate(Customer parent, string navigationProperty);
public static NorthwindContext CurrentContext { get; internal set; }
public static object DoLazyLoading(Customer parent, string navigationProperty)
{
if (navigationProperty == nameof(Customer.Orders))
{
return CurrentContext.Orders
.Where(o => o.CustomerId == parent.CustomerId)
.ToList();
}
return null;
}
}
}
Your entities remain pure POCOs but are marked [Serializable] so they can safely be cached. You also maintain a reference for a LazyLoad delegate to be assigned at runtime.
using System.Collections.Generic;
using static EF_poco_lazy_loading.Helpers.CustomerHelper;
namespace EF_poco_lazy_loading.Models
{
[Serializable]
public class Customer
{
public static LazyLoadDelegate OrderLazyLoad { get; internal set; }
private List<Order> _orders;
public string CustomerId { get; set; }
public string CompanyName { get; set; }
// Navigation property
public List<Order> Orders
{
get
{
if (_orders == null && OrderLazyLoad != null)
{
// Call delegate to load related data
_orders = (List<Order>)OrderLazyLoad(this, nameof(Orders));
}
return _orders;
}
set => _orders = value;
}
[Serializable]
public class Order
{
public int OrderId { get; set; }
public string CustomerId { get; set; }
public DateTime OrderDate { get; set; }
}
}
Once your POCO entities are serialization-safe, you can integrate NCache to take your distributed caching to the next level. NCache is a.NET-native, high-performance, fully distributed caching platform that is specifically designed to function seamlessly with EF Core and enterprise-grade .NET applications, in contrast to generic distributed caching solutions.
Here's an example of how easily you can integrate NCache into your EF Core workflow:
using System;
using System.Linq;
using Alachisoft.NCache.Client;
using EF_poco_lazy_loading.Data;
using EF_poco_lazy_loading.Helpers;
using EF_poco_lazy_loading.Models;
namespace EF_poco_lazy_loading
{
class Program
{
static void Main(string[] args)
{
// Step 1: Initialize NCache
string cacheName = "demoCache"; // change if your cache name is different
ICache cache = CacheManager.GetCache(cacheName);
Console.WriteLine("NCache initialized successfully.\n");
// Step 2: Setup EF Core Context
NorthwindContext context = new NorthwindContext();
// Step 3: Connect Lazy Loading Helper
CustomerHelper.CurrentContext = context;
Customer.OrderLazyLoad = CustomerHelper.DoLazyLoading;
if (context.Database.CanConnect())
{
Console.WriteLine("Successfully connected to SQL Server!");
}
else
{
Console.WriteLine("Could not connect to SQL Server.");
}
// Step 4: Query & Cache Customers
var customers = from c in context.Customers
where c.CompanyName.StartsWith("W")
select c;
foreach (Customer c in customers)
{
cache.Insert(c.CustomerId, c);
Console.WriteLine($"Cached Customer: {c.CompanyName}");
foreach (Order order in c.Orders)
{
Console.WriteLine($"\t Order Date: {order.OrderDate}");
}
}
Console.ReadKey();
}
}
}
You can get the best of both worlds by combining EF Core and NCache: enterprise-level caching performance and modern ORM flexibility. Key advantages include:
While Entity Framework's dynamic proxies complicate lazy loading in distributed environments, modern solutions like NCache make it easy to overcome these challenges. Developers can safely cache POCO entities in NCache by turning off EF proxy creation and using custom lazy loading, which improves performance, scalability, and reliability without compromising the flexibility of EF Core.
With NCache, your .NET applications can handle large data volumes and concurrent requests effortlessly, all while maintaining a clean, modern architecture.
© Copyright Alachisoft 2002 - . All rights reserved. NCache is a registered trademark of Diyatech Corp.