تعلم برمجة الـ RESTful services أو الـ Web APIs بإستخدام ASP.NET Core بطريقة مفصلة وتدريجية
خاصية الـ caching تفيدنا في حفظ البيانات التي يكثر الطلب عليها في الذاكرة مما يقلل من الحاجة الى الذهاب الى مصدر المعلومة (قاعدة بيانات أو خدمة خارجية أو غيرها) ومعالجة هذه البيانات ثم إعادتها الى المستخدم مما يساعدنا في تحسين أداء الخدمة التي نقدمها.
في هذا الدرس سنتعلم طريقتين للـ caching. الأولى in-memory وتعتمد على ذاكرة الجهاز أو السيرفر الذي تعمل عليه الخدمة، والطريقة الثانية distributed وسنستفيد من Redis لتنفيذها.
كما ذكرنا سابقاً، سنستفيد من الذاكرة كمخزن مؤقت للبيانات التي يكثر إستدعائها وسنلاحظ تحسن في الأداء حيث لم يعد هنالك داعي لجلب البيانات من مصدرها في كل مرة. ولعمل ذلك نقوم بالتالي:
سنطور هذه الخاصية خارج الـ master branch، ولعمل ذلك ننفذ الأمر التالي:
git checkout -b inmemory-cache
![]() |
نضيف السطر التالي على الدالة ()ConfigureServices في الملف Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMemoryCache();
}
نضيف namespace جديد ونعدل على الـ constructor ليستقبل متغير من نوع IMemoryCache:
...
using Microsoft.Extensions.Caching.Memory;
public class EmployeesController : ControllerBase
{
private IMemoryCache _cache;
...
public EmployeesController(IMemoryCache cache, ...)
{
_cache = cache;
...
}
}
ولإستخدامها في الدالة ()GetEmployee نقوم بالتالي:
[HttpGet("{id}")]
public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> GetEmployee(int id)
{
_logger.LogInformation("GetEmployee requested");
if( !_cache.TryGetValue(id, out EmployeeDetailsDto employeeDetailsDto) )
{
var employeeEntity = await _repo.GetAsync(id);
if (employeeEntity != null)
employeeDetailsDto = _mapper.Map<EmployeeDetailsDto>(employeeEntity);
_cache.Set(
id,
employeeDetailsDto,
new MemoryCacheEntryOptions {
SlidingExpiration = TimeSpan.FromMinutes(1)
}
);
}
var response = new EmployeesResponse<EmployeeDetailsDto>()
{
IsSuccessful = true,
Status = (employeeDetailsDto == null) ? 404 : 200,
Message = (employeeDetailsDto == null) ? "Employee not found" : string.Empty,
Data = employeeDetailsDto
};
if (response.Status == 404)
return NotFound( response );
return Ok( response );
}
نتأكد أولاً من وجود البيانات في الـ cache بواسطة ()TryGet وإذا كانت القيمة مخزنة سيتم إحتوائها في response وإعادتها للمستخدم. وفي حالة لم تكن القيمة موجودة، سيتم إرجاعها من قاعدة البيانات وتخزينها في الـ cache بواسطة الأمر ()Set.
أستخدمنا هنا SlidingExpiration لتحديد متى يعتبر الـ cache منتهي وهي تستخدم لتحديد كم المدة التي يمكن للقيمة الا تستدعى قبل تحديثها من جديد وستظل القيمة موجودة في الـ cache طالما كان هنالك طلب عليها خلال المدة الزمنية المحددة. أي أنه طالما لم تمر الفترة الزمنية المحددة (دقيقة في مثالنا هذا) ستظل القيمة موجودة في الـ cache.
وبالإمكان إستخدام AbsoluteExpiration بدلاً من SlidingExpiration وذلك لتحديد مدة إنتهاء القيمة في الـ cache سواء تم الإستفادة منها أم لا.
والان، إذا جربنا عن طريق Postman نرى أن عملية التنفيذ لأول مرة أخذت 3 ثواني ونصف تقريباً:
![]() |
وعند تنفيذ نفس الأمر بنفس الـ argument في مدة لا تتجاوز الدقيقة نرى أن العملية أستغرقت وقت أقل بكثير، تحديداً 18 جزء من الثانية:
![]() |
نضيف الآن هذه التعديلات الى git:
git add .
git commit -m "adds in-memory cache support to GetEmployees()"
تعتبر الطريقة السابقة في الـ caching خيار جيد في حالة أن الخدمة مستضافة على خادم server واحد, ولكن في حالة إستضافة هذه الخدمة على أكثر من خادم أو في الـ cloud فإنه لا يمكن إستخدامها. هنا يأتي دور الـ distributed caching. وسنستفيد من redis كـ key value store للقيام بذلك.
![]() |
نعود الآن الى الـ master branch:
git checkout master
ثم ننشئ branch جديد ونتحول عليه:
git branch distributed-cache
git checkout distributed-cache
redis في الأساس تطبيق يعمل على Linux ومدعوم بشكل محدود على Windows ولذلك سنستخدم Docker للتعامل مع redis container.
![]() |
يمكن تحميل Docker من الرابط التالي ولكن يجب أن تكون نسخة وندوز Professional على الأقل:
في حالة لم يكن الوندوز Professional أو Enterprise، فبالإمكان تحميل Virtual Machine مثل Virtual Box أو VM Ware وتحميل أحد توزيعات Linux ثم تحميل Docker. والحل الثاني هو تحميل Docker هلى Windows Subsystem for Linux - WSL.
الآن في قائمة Turn Windows features on or off يجب علينا التأكد من إختيار Containers و Hyper-V:
![]() |
بعد الإنتهاء من تثبيت Docker فإنه يمكن التأكد من أن عملية التثبيت تمت بشكل صحيح بكتابة الأمر التالي في الـ command prompt:
docker run hello-world
وإذا ظهرت نتائج كالتالي فإنه يعمل بشكل صحيح:
![]() |
الآن نسحب الـ redis image من الـ docker registry:
docker pull redis
![]() |
ونقوم الآن بتشغيلها:
docker run --name redis-store -p 6379:6379 -d redis
![]() |
وللتأكد من أن الـ container يعمل نكتب الأمر التالي:
docker ps
![]() |
سنقوم الآن بالتأكد من مقدرتنا للوصول الى الـ Redis Command Line Interface أو redis-cli. ونقوم بذلك عن طريق فتح الـ bash command line في الـ container الذي أنشأناه ثم فتح الـ redis-cli:
docker exec -it redis-store /bin/bash
ثم:
redis-cli
ثم نكتب ping وإذا أعاد الينا PONG فإننا تمكنا من التعامل مع redis-cli بشكل صحيح:
![]() |
وفيما يلي بعض الأوامر التي يمكن تنفيذها:
![]() |
// لحفظ قيمة في الذاكرة بشكل دائم
set <key-name> <value>
// لجلب القيمة من الذاكرة
get <key-name>
// لحذف القيمة من الذاكرة
del <key-name>
// لحفظ قيمة في الذاكرة لعدد معين من الثواني
set <key-name> <value> EX <num-of-seconds>
// لطباعة جميع الـ keys
keys *
نضيف الآن الـ package المتعلق بالـ distributed caching:
dotnet add Microsoft.Extensions.Caching.StackExchangeRedis
الآن في Startup.cs نستدعي الـ namespace التالي:
using Microsoft.Extensions.Caching.StackExchangeRedis;
وفي ()ConfigureServices نقوم بالتعديل اللذي يلي:
...
using Microsoft.Extensions.Caching.StackExchangeRedis;
public void ConfigureServices(IServiceCollection services)
{
...
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
});
}
في هذا الملف، نقوم بالتعديلات التالية:
...
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
public class EmployeesController : ControllerBase
{
private IDistributedCache _cache;
...
public EmployeesController(IDistributedCache cache, ...)
{
_cache = cache;
...
}
}
ولإستخدامها في الدالة ()GetEmployee نقوم بالتالي:
[HttpGet("{id}")]
public async Task<ActionResult<EmployeesResponse<EmployeeDetailsDto>>> GetEmployee(int id)
{
_logger.LogInformation("GetEmployee requested");
var cachedResponse = await _cache.GetStringAsync(id.ToString());
EmployeeDetailsDto employeeDetailsDto = null;
if( string.IsNullOrEmpty(cachedResponse) )
{
var employeeEntity = await _repo.GetAsync(id);
if (employeeEntity != null)
employeeDetailsDto = _mapper.Map<EmployeeDetailsDto>(employeeEntity);
string serializedData = (employeeDetailsDto == null) ? string.Empty : JsonSerializer.Serialize(employeeDetailsDto);
await _cache.SetStringAsync(
id.ToString(),
serializedData,
new DistributedCacheEntryOptions {
SlidingExpiration = TimeSpan.FromMinutes(1)
}
);
}
else
{
employeeDetailsDto = JsonSerializer.Deserialize<EmployeeDetailsDto>(cachedResponse);
}
var response = new EmployeesResponse<EmployeeDetailsDto>()
{
IsSuccessful = true,
Status = (employeeDetailsDto == null) ? 404 : 200,
Message = (employeeDetailsDto == null) ? "Employee not found" : string.Empty,
Data = employeeDetailsDto
};
if (response.Status == 404)
return NotFound( response );
return Ok( response );
}
جرب ما قمنا بعملة بواسطة Postman ولاحظ كيف أنه عند تنفيذ العملية في المرة الثانية في مدة لا تتجاوز الدقيقة ستكون مدة التنفيذ أقل بشكل واضح.
وبالإمكان أن نرى كيف أن redis حفظ القيمة لديه. فبعد التنفيذ في Postman بمدة لا تتجاوز الدقيقة (وهي المدة التي حددناها في الـ SlidingExpiration) نستطيع أن نرى الـ keys التي تم حفظها بالأمر keys *
ثم بإمكاننا أن نستعرض القيمة المحفوظة بواسطة الأمر hgetall حيث أن redis يعتبر القيمة المخزنة من نوع hash:
![]() |
بإمكاننا الآن إيقاف الـ docker container وحذفها بعد تطبيق هذا المثال:
docker stop redis-store
docker rm redis-store
نضيف الآن هذه التعديلات الى git:
git add .
git commit -m "adds distributed cache support to GetEmployees()"
سنعتمد الآن ما قمنا به في الـ branch الذي أنشأناه سابقاً inmemory-cache وسنضيفة على الـ master.
نعود الآن الى الـ master branch:
git checkout master
ثم ندمج التعديلات التي تمت في الـ inmemory-cache branch:
git merge inmemory-cache