ekrem özer

her yerde olan şeyler.

.Net Core IMemoryCache Kullanımı

Merhaba arkadaşlar, bu yazımda .net core da cache yönetimini anlatacağım. Cache yapısı sürekli kullandığımız verileri her defasında veritabanından okumak yerine belli bir süre rame atıp son kullanıcı veriyi istediğinde veritabanına gitmeden veriyi ramden okumamızı sağlar. Şimdi konuyu örnek bir projede inceleyelim. Best Practices açısından projede cache'i yöneteceğimiz bir class yapısı oluşturacağım. Önce CoreMemoryCache adında bir proje oluşturup içince CoreMemoryCache.Web adında bir .net core mvc projesi ekliyorum ve projeme Core ve altına da Caching diye bir klasör ekliyorum. Cache yönetimini başka bir katmanda yapmak daha doğru olur ancak makalenin konusu olmadığı için ben aynı projede yapıyorum.

Sonrasında ilk olarak Startup.cs MemoryCache servisimizi ekliyoruz.

public void ConfigureServices(IServiceCollection services)
{
        services.AddMemoryCache();
}

Cache'i yönetmek için kullanacağım sınıfımda parametreleri kullanmak için CacheKey adında bir class oluşturuyorum.

namespace CoreMemoryCache.Web.Core.Caching
{
    public class CacheKey
    {
        #region Ctor
        public CacheKey(string key, int cacheTime)
        {
            Key = key;
            CacheTime = cacheTime;
        }
        public CacheKey(string key, int cacheTime, int cacheSlidingTime)
        {
            Key = key;
            CacheTime = cacheTime;
            CacheSlidingTime = cacheSlidingTime;
        }
        public CacheKey(string key, int cacheTime, int cacheSlidingTime, CacheItemPriority cacheItemPriority)
        {
            Key = key;
            CacheTime = cacheTime;
            CacheSlidingTime = cacheSlidingTime;
            CacheItemPriority = cacheItemPriority;
        }
        #endregion

        public string Key { get; protected set; }
        public int CacheTime { get; set; }
        public int? CacheSlidingTime { get; set; }
        public CacheItemPriority? CacheItemPriority { get; set; }
    }
}

Bu classımda 3 tane farklı kullanım şekli sunan Consructor metodum var. Aldığı parametleri kısaca açıklamak gerekirse;
Key: Cache tutacağımız nesneler için kullanacağımız unique key.
CacheTime: Objenin cache'de duracağı zamanı dakika cinsinden alan property.
CacheSlidingTime: SlidingTime bir objenin kaç dakikada(diğer birimlerde de süre belirleyebilirsiniz, bizim projemizde dk cinsinden olacak) bir çağırılsa cache süresinin uzatılacağını belirler. Örneğin CacheSlidingTime'ı 15 dk verirsek obje 15dk içerisinde her çağırıldığında cache süresi 15 dk daha uzayacaktır. AbsoluteExpiration ile de kullanılabilir ancak AbsoluteExpirationTime dolduğunda her halükarda obje cache'den silinecektir.
CacheItemPriority: Cache'e attığının objeye öncecil verir, Enum tipindedir ve aşağda ki gibi 4 adet değeri vardır, Ram dolduğunda cache'deki nesneler bu önceliğe göre silinir.
 

    public enum CacheItemPriority
    {
        Low,
        Normal,
        High,
        NeverRemove,
    }

CacheKey sınıfımız bu kadardı, ihtiyacınıza göre diğer parametleri çoğaltabilirsiniz, örneğin Size propertysini ben bu projede kullanmadım. Sonrasında da bağımlılığı azaltman için kullanıcağım classı türeteceğim Interface'e metodlarımı ekliyorum.

    public interface ICacheManager
    {
        T Get<T>(string key);
        Task<T> GetAsync<T>(string key);
        void Set(CacheKey cacheKey, object model);
        Task SetAsync(CacheKey cacheKey, object model);
        T GetOrCreate<T>(CacheKey cacheKey, Func<T> acquire);
        Task<T> GetOrCreateAsync<T>(CacheKey cacheKey, Func<T> acquire);
        void Remove(string key);
        Task RemoveAsync(string key);
    }

Şimdide MemoryCacheManager classımızı ekleyelim ve fonksiyonları inceleyelim.

namespace CoreMemoryCache.Web.Core.Caching
{
    public class MemoryCacheManager : ICacheManager
    {
        private readonly IMemoryCache _memoryCache;
        public MemoryCacheManager(IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;
        }

        public T Get<T>(string key)
        {
            _memoryCache.TryGetValue(key, out T model);
            return model;
        }

        public async Task<T> GetAsync<T>(string key)
        {
            return await Task.Run(() =>
            {
                _memoryCache.TryGetValue(key, out T model);
                return model;
            });
        }
        public void Set(CacheKey cacheKey, object model)
        {
            var memoryCacheEntryOptions = PrepareMemoryCacheEntryOptions(cacheKey);
            _memoryCache.Set(cacheKey.Key, model, memoryCacheEntryOptions);
        }

        public Task SetAsync(CacheKey cacheKey, object model)
        {
            var memoryCacheEntryOptions = PrepareMemoryCacheEntryOptions(cacheKey);
            _memoryCache.Set(cacheKey.Key, model, memoryCacheEntryOptions);
            return Task.CompletedTask;
        }

        public T GetOrCreate<T>(CacheKey cacheKey, Func<T> acquire)
        {
            if (cacheKey.CacheTime <= 0 && (cacheKey.CacheSlidingTime == null || cacheKey.CacheSlidingTime <= 0))
            {
                return acquire();
            }

            var result = _memoryCache.GetOrCreate(cacheKey.Key, entry =>
            {
                entry.SetOptions(PrepareMemoryCacheEntryOptions(cacheKey));

                return acquire();
            });

            if (result == null)
            {
                Remove(cacheKey.Key);
            }

            return result;
        }

        public async Task<T> GetOrCreateAsync<T>(CacheKey cacheKey, Func<T> acquire)
        {
            if (cacheKey.CacheTime <= 0 && (cacheKey.CacheSlidingTime == null || cacheKey.CacheSlidingTime <= 0))
            {
                return acquire();
            }

            var result = _memoryCache.GetOrCreate(cacheKey.Key, entry =>
            {
                entry.SetOptions(PrepareMemoryCacheEntryOptions(cacheKey));

                return acquire();
            });

            if (result == null)
            {
                await RemoveAsync(cacheKey.Key);
            }

            return result;
        }

        public void Remove(string key)
        {
            _memoryCache.Remove(key);
        }

        public Task RemoveAsync(string key)
        {
            _memoryCache.Remove(key);
            return Task.CompletedTask;
        }

        private static MemoryCacheEntryOptions PrepareMemoryCacheEntryOptions(CacheKey cacheKey)
        {
            var memoryCacheEntryOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(cacheKey.CacheTime)
            };

            if (cacheKey.CacheSlidingTime > 0)
            {
                memoryCacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes((int)cacheKey.CacheSlidingTime);
            }
            if (cacheKey.CacheItemPriority != null)
            {
                memoryCacheEntryOptions.Priority = (CacheItemPriority)cacheKey.CacheItemPriority;
            }

            return memoryCacheEntryOptions;
        }
    }
}

Sonrada oluşturduğumuz interface ile classın bağımlılığını Startup.cs de tanımlayalım.
 

public void ConfigureServices(IServiceCollection services)
{
     services.AddControllersWithViews();
     services.AddMemoryCache();
     services.AddSingleton<ICacheManager, MemoryCacheManager>();
}

Gördüğünüz gibi ConfigureServices metodunda AddSingleton metoduyla ICacheManager çağırıldığında MemoryCacheManager sınıfımızın dönmesiniz sağladık. AddSingleton metodu uygulama ayağa kalktığında nesnenin bir örneğini alıp her çağırdığımızda bize o nesneyi dönecektir.

Şimdi bu classımızdaki metodları tek tek inceleyelim;

public T Get<T>(string key)
{
	_memoryCache.TryGetValue(key, out T model);
	return model;
}

string tipinde key parametresi alır ve geriye generic tipte cacheden istediğimiz nesneyi döner, cache'de bulamazsa tipin default değerini döner.

public async Task<T> GetAsync<T>(string key)
{
	return await Task.Run(() =>
	{
		_memoryCache.TryGetValue(key, out T model);
		return model;
	});
}


Get metodun asenkron hali.

public void Set(CacheKey cacheKey, object model)
{
	var memoryCacheEntryOptions = PrepareMemoryCacheEntryOptions(cacheKey);
	_memoryCache.Set(cacheKey.Key, model, memoryCacheEntryOptions);
}

CacheKey tipinde yukarıdaki anlattığım cache de kullanacağımız nesneyi PrepareMemoryCacheEntryOptions metoduyla MemoryCacheEntryOptions türüne prepare eder ve object tipinde cache'e atacağımız nesneyi alır, gelirye değer dönmez.

public Task SetAsync(CacheKey cacheKey, object model)
{
	var memoryCacheEntryOptions = PrepareMemoryCacheEntryOptions(cacheKey);
	_memoryCache.Set(cacheKey.Key, model, memoryCacheEntryOptions);
	return Task.CompletedTask;
}

Set metodunun asenkron hali.

public T GetOrCreate<T>(CacheKey cacheKey, Func<T> acquire)
{
	if (cacheKey.CacheTime <= 0 && (cacheKey.CacheSlidingTime == null || cacheKey.CacheSlidingTime <= 0))
	{
		return acquire();
	}

	var result = _memoryCache.GetOrCreate(cacheKey.Key, entry =>
	{
		entry.SetOptions(PrepareMemoryCacheEntryOptions(cacheKey));

		return acquire();
	});

	if (result == null)
	{
		Remove(cacheKey.Key);
	}

	return result;
}

Bu metotda CacheKey bir nesne ve generic tipte bir fonksiyon alır, obje cache'de varsa cache'den verir eğer yoksa parametre olarak gönderdiğimiz fonkstion ile nesneyi oluşturur, cache atar ve size döner, üstteki if kontrolü, eğer CacheKey nesmenizde cache süresi belirlenmemişse objeyi cache atmadan parametre olarak gönderdiğimiz fonksiyonla üretip dönmesini sağlar, en alttaki if null kontrolü ile de cacheden veya fonksiyondan gelen obje null ise null objeyi cache'den siler. kullanımına aşağıda değineceğiz.

public async Task<T> GetOrCreateAsync<T>(CacheKey cacheKey, Func<T> acquire)
{
	if (cacheKey.CacheTime <= 0 && (cacheKey.CacheSlidingTime == null || cacheKey.CacheSlidingTime <= 0))
	{
		return acquire();
	}

	var result = _memoryCache.GetOrCreate(cacheKey.Key, entry =>
	{
		entry.SetOptions(PrepareMemoryCacheEntryOptions(cacheKey));

		return acquire();
	});

	if (result == null)
	{
		await RemoveAsync(cacheKey.Key);
	}

	return result;
}

GetOrCreate metodunun askenkron hali.

public void Remove(string key)
{
	_memoryCache.Remove(key);
}

Bu metod string tipinde key parametresi alır ve keye karşılık gelen nesneyi cache'den siler.

public Task RemoveAsync(string key)
{
	_memoryCache.Remove(key);
	return Task.CompletedTask;
}

Remove metodunun asenkron hali.

private static MemoryCacheEntryOptions PrepareMemoryCacheEntryOptions(CacheKey cacheKey)
{
	var memoryCacheEntryOptions = new MemoryCacheEntryOptions
	{
		AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(cacheKey.CacheTime)
	};

	if (cacheKey.CacheSlidingTime > 0)
	{
		memoryCacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes((int)cacheKey.CacheSlidingTime);
	}
	if (cacheKey.CacheItemPriority != null)
	{
		memoryCacheEntryOptions.Priority = (CacheItemPriority)cacheKey.CacheItemPriority;
	}

	return memoryCacheEntryOptions;
}

Bu metod CacheKey tipindeki nesnemizi IMemoryCache sınıfımızın kullanacağı MemoryCacheEntryOptions tipine çevirir. CacheTime propertysi not null olduğu için null kontrollü yapmadan MemoryCacheEntryOptions nesnemizin AbsoluteExpirationRelativeToNow propertysine set ediyoruz, CacheSlidingTime ve CacheItemPriority propertiyleri null gelmemişse eğer nesnemize ekliyoruz.

Şimdi kısaca örnekte bir personel datamız olacak, bu dataları listeleyip detayına gideceğiz. Veritabanı bağlantısı EF katmanı vs. ile uğraşıp konuyu uzatmamak için mockaroo.com üzerinden 1000 adet random oluşturulmuş personel bilgisini json formatında indirip oluşturduğum Personal classına parse eden bir metod yazıyorum.

Personal classımız;

namespace CoreMemoryCache.Web.Models
{
    public class Personal
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Gender { get; set; }
        public string IpAddress { get; set; }
    }
}

Buda fake data oluşturan classımız, wwwroot klasörüne attığım json dosyasını okuyup List<Personal> tipinde bize dönüyor.

namespace CoreMemoryCache.Web.Core.FakeData
{
    public static class FakeDataGenerator
    {
        public static List<Personal> GetPersonals()
        {
            var jsonFilePath = $"{Directory.GetCurrentDirectory()}\\wwwroot\\fake-data.json";
            var jsonString = File.ReadAllText(jsonFilePath);
            var personalList = JsonSerializer.Deserialize<List<Personal>>(jsonString).Take(100).ToList();
            return personalList;
        }
    }
}

Şimdi PersonalController adında bir controller oluşturuyorum;

public class PersonalController : Controller
{
	private readonly ICacheManager _cacheManager;

	public PersonalController(ICacheManager cacheManager)
	{
		_cacheManager = cacheManager;
	}

	public async Task<IActionResult> Index()
	{
		var cacheKey = new CacheKey("Personals", 15);

		var personals = await _cacheManager.GetOrCreateAsync<List<Personal>>(cacheKey, () => FakeDataGenerator.GetPersonals());

		return View(personals);
	}

	public async Task<IActionResult> Detail(int id)
	{
		var cacheKey = new CacheKey($"Personal:{id}", 15, 3);

		var personal = await _cacheManager.GetOrCreateAsync<Personal>(cacheKey, () => FakeDataGenerator.GetPersonals().FirstOrDefault(x => x.Id == id));

		return View(personal);
	}
}

Bu controllere 2 tane action ekledim birincisi Index, bunda oluşturduğumuz cacheKeyde key olarak Personals ve CacheTime olarakta 15 dk veriyoruz. Sonra GetOrCreateAsync metoduna generic tip olarak List<Personal> tipini dönüş tipi olarak belirliyoruz, cacheKey'imizi ve obje eğer cachde yoksa, çağıracağı metodu parametre olarak veriyoruz.

Detail actionumuzda ise parametre olarak id alıyoruz ve bu id'ye ait personeli getiriyoruz, indexden faklı olarak tek bir nesne beklediğimiz için dönüş tipini Personal yapıyoruz ve CacheSlidingTime parametresini de ekliyoruz, burada da cache'de obje yoksa çağıracağı metodu parametre olarak ekliyoruz.

MemoryCacheManager classımızda ki diğer metodlar için ayrı ayrı örnek yapmayacağım ancak kullanım şekillerine Example actionunda aşağıda kısaca değinelim;

public async Task<IActionResult> Example()
{
	//CacheKey'in 3 farklı  kullanım şekli;
	var cacheKey1 = new CacheKey(key: "Personal", cacheTime: 20);
	var cacheKey2 = new CacheKey(key: "Personal", cacheTime: 20, cacheSlidingTime: 4);
	var cacheKey3 = new CacheKey(key: "Personal", cacheTime: 20, cacheSlidingTime: 4, cacheItemPriority: CacheItemPriority.Low);

	var personal = FakeDataGenerator.GetPersonals().FirstOrDefault(x => x.Id == 1);

	//Set metodu
	_cacheManager.Set(cacheKey1, new Personal());

	//Asenkron set metodu
	await _cacheManager.SetAsync(cacheKey1, personal);

	//Get Metodu
	var get = _cacheManager.Get<Personal>(cacheKey1.Key);
	//Asenkron get metodu
	var getAsync = _cacheManager.GetAsync<Personal>(cacheKey1.Key);

	//GetOrCreate metodu
	var getOrCreate = _cacheManager.GetOrCreate<Personal>(cacheKey1, () => FakeDataGenerator.GetPersonals().FirstOrDefault(x => x.Id == 1));

	//Asenkron GetOrCreate metodu
	var getOrCreateAsync = await _cacheManager.GetOrCreateAsync<Personal>(cacheKey1, () => FakeDataGenerator.GetPersonals().FirstOrDefault(x => x.Id == 1));

	//Remove metodu
	_cacheManager.Remove(cacheKey1.Key);

	//Asenkron Remove metodu
	await _cacheManager.RemoveAsync(cacheKey1.Key);

	return View("Index", FakeDataGenerator.GetPersonals());
}

.net core da kısaca IMemoryCache ile cache yönetimine değindim, umarım faydalı olmuştur. Kodlara alttaki github linkinden ulaşabilirsiniz, soru ve görüşleriniz için iletişim kısmındaki mailimden bana ulaşabilirsiniz.

https://github.com/ekremozer/CoreMemoryCache