جميعنا يعلم أهمية التزامن في البرمجيات فمعظم الـ API حالتها ضعيفة بعد جلبها من المصدر، فإذا كنت تريد ان تحكم سيطرتك على البيانات فبالتالي انت مطالب بعمل إضافي في برمجياتك.
نفترض ان لدينا مجموعة بيانات قام المستخدم A بجلبها من قاعدة البيانات، وبعدها بلحظات قام المستخدم B بجلب نفس البيانات لعمل تعديل عليها ماذا تتوقع أن يحدث في تلك الحالة إذا قام المستخدم B بتغيير البيانات. ماهي حالة المستخدم A هنا؟
من المؤكد أن المستخدم A في هذه الحالة سيقوم بالتعامل مع بيانات قديمة.
هذه المقالة تعبتر متقدمة نوعا ما Advanced وموجهة للمبرمجين المحترفين بصورة عامة، فإن كنت تواجه صعوبة كبيرة في المتابعة فبإمكانك تركها وعدم قراءتها فقط يكفيك أن تقوم بإنشاء حقل في كل جداول قاعدة البيانات خاصتك يأخد النوع rowversion وبالتالي سيقوم الـ SQL Server بالتحكم في جميع البيانات سواء التي تم جلبها والتي يريد المستخدم حفظها
حسنا إذا كنت مواصل معي في القراءة فمرحبا بك.
ماهو الـ ETag
هو عبارة عن معرف (في الغالب GUID) يتم تخزينه بصورة مؤقتة مع جميع الـ Requests والـ Responses فيساعد في سرعة التقليل من الـ BandWidth بحيث أنه في حال قمت بطلب جلب بيانات لم تتغير منذ الـ Request الأول بالتالي سيتم عرض نفس البيانات بدون توليدها مرة أخرى، أيضا يساعد في عدم حدوث overwrite للبيانات كما شرحنا سابقا في حالتي المستخدمين A و B.
قم بإنشاء مشروع ASP.NET Core جديد وقم بإنشاء Class بإسم داخل المشروع بإسم HashFactory وقم بإنشاء Method داخل نفس الـ Class بإسم GetHashFromProprites الشفرة كاملة لهذا الـ Class ستكون كتالي:
public static class HashFactory
{
public static string GetHashFormProperties(Type type)
{
if (type == null)
{
return string.Empty;
}
string propertyToHash = string.Empty;
var getProperites = from m in type.GetProperties() select m.Name.ToString(); // Use Reflection
foreach (var data in getProperites)
{
propertyToHash = data.ToList().ToString();
}
using var md5 = MD5.Create();
byte[] dataRow = md5.ComputeHash(Encoding.UTF8.GetBytes(propertyToHash));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < dataRow.Length; i++)
{
sb.Append(dataRow[i].ToString("x2"));
}
return sb.ToString();
}
مهمة هذا الـ Class هي ان يقوم بتوليد رقم من النوع Guid وهي الرقم الذي يتم إسناده للـ ETag header وقمت بإستخدام الخوازمية MD5 وبها Method تسمى CreateHash، ومحتوى الـ Guid الذي يتم توليده عبارة عن جميع الـ Property للـ Class الذي سيتم عليه عمليتا الـ Request والـ Response وقد قمت بإستخدام الـ Reflection لجلب جميع الـ Properties للـ Object الذي نريد التعامل معه وقمت بإستخدام تقنية Linq لإختيار اسم الـ Property فقط.
قم بإنشاء Controller وبها اسلوبين الأول عبارة عن GET والآخر عبارة عن PUT الشفرة ستكون كالتالي، سأقوم بالشرح بعد تنفيذ المشروع:
const string ETAG_HEADER = "ETag";
const string MATCH_HEADER = "If-Match";
[HttpGet("{id}",Name ="GetPostById")]
public IActionResult GetById(int id)
{
var post = _context.Posts.Where(x => x.Id == id).FirstOrDefault();
var eTag = HashFactory.GetHashFormProperties(typeof(Post));
HttpContext.Response.Headers.Add(ETAG_HEADER, eTag);
if(HttpContext.Request.Headers.ContainsKey(MATCH_HEADER) &&
HttpContext.Request.Headers[MATCH_HEADER].Contains(eTag))
{
return new StatusCodeResult(StatusCodes.Status304NotModified);
}
return new ObjectResult(post);
}
[HttpPut("{id}",Name ="UpdatePostById")]
public async Task<IActionResult> Update(int id, [FromBody] Post post)
{
if(post == null || post.Id != id)
{
return BadRequest();
}
var postToUpdate = _context.Posts.FirstOrDefault(x => x.Id == id);
if(postToUpdate != null)
{
return NotFound();
}
var contextTag = HashFactory.GetHashFormProperties(typeof(Post));
if(!HttpContext.Request.Headers.ContainsKey(MATCH_HEADER) ||
!HttpContext.Request.Headers[MATCH_HEADER].Contains(contextTag))
{
return new StatusCodeResult(StatusCodes.Status412PreconditionFailed);
}
postToUpdate.Name = post.Name;
_context.Posts.Update(post);
await _context.SaveChangesAsync();
return new NoContentResult();
}
حسنا هنا في الحدث GET قمت بإضافة الـ ETag header مع الـ Request وقبل جلب البيانات .
بعدها قمت بالتأكد في حال كان الـ Header قام بإرسال الـ Etag من خلال If-Match header والذي سيقوم بالتأكد من أن المعرف GUID الذي تم إرساله مع الـ Request الأول هو نفسه، هنا لدينا حالتين
الأولى: إذا كان تم إرساله من قبل فيسقوم الـ API بإرجاع 304NotModified Status فبالتالي لن يقوم السيرفر بمخاطبة قاعدة البيانات او XML او أي Provider لجلب البيانات بل سيقوم بجلبها من البيانات المخزنة لديه من Chache
الحالة الثانية: إذا أراد مستخدم آخر أن يعدل في البيانات لابد أن يقوم بإرسال الـ If-Match header مع الـ Request وذلك حتى لايحدث تضارب في جلب البيانات بعد تعديلها بالنسبة للمستخدم الأول الذي قام بعمل Request.
بإمكانك التجربة من خلال الـ Postman ورؤية ما قمنا بشرحه بصورة عملية.
في الحدث PUT قمت بالتأكد من أنه لابد أن يقوم المستخدم بإرسال If-Match header وذلك حتى يتسنى لي مقارنة الـ Guid الذي تم تخزينه مع الـ Request مع نفس الـ Guid الذي سيتم إرساله في هذا الحدث PUT فإذا كانا مختلفين فحينها ستم إجهاض عملية التعديل وسيتم إرجاع 412PreConditionFailed status
تحياتي