استفاده از JSON Patch با ASP.Net Core

JSONPatch شیوه آپدیت اسناد روی یک API با روشی بسیار صریح و روشن است. قراردادی است برای بیان شیوه‌ای که می‌خواهید یک سند را تغییر دهید (مثلا مقداری در یک فیلد را با مقدار دیگری جایگزین کنید) بدون اینکه نیاز داشته باشید تا مقادیر تغییر نیافته را همراه با آن ارسال کنید.

استفاده از JSON Patch با ASP.Net Core

درخواست‌های JSON Patch چگونه عمل می‌کنند؟

مستندات رسمی JSON Patch در این لینک قرار دارند: http://jsonpatch.com/، اما ما کمی مسائل را باز می‌کنیم تا ببینیم در #ASP/C چگونه کار می‌کند. در این مقاله ما عملکرد JSON Patch  در ASP.net Core را به طور سریع بیان خواهیم کرد.

در تمام مثال‌ها، درخواست‌های JSON Patch را برای یک شیء نوشته‌ایم، بنابراین در C# داریم:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<string> Friends { get; set; }
}

همه درخواست‌های Patch ساختار مشابهی را دنبال می‌کنند. این لیستی از عملیات در یک آرایه است. این عملیات دارای سه ویژگی هستند:

op”: نوع عملیاتی که می‌خواهید انجام دهید را تعریف می‌کند. مثلا افزودن، جایگزینی، تست و غیره

path”: مسیر ویژگی‌های شی‌ء‌ای که می‌خواهید ویرایش کنید را نشان می‌دهد. در مثال بالا، اگر بخواهیم "FirstName" را ویرایش کنیم، ویژگی path مانند "firstname/" خواهد شد.

value”: در بیشتر بخش‌ها، مقداری است که می‌خواهید درون عملیات استفاده کنید.
اکنون اجازه دهید به هر یک از عملیات‌ها نگاهی بیندازیم.

Add

عملیات Add به طور معمول بدان معناست که یک ویژگی را به یک شیء یا یک آیتم را به یک آرایه اضافه کند. قبلا این کار در #C انجام نمی‌شد. زیرا سی‌شارپ یک زبان strongly-typed است و شما نمی‌توانید یک ویژگی را به شیء‌ای که قبلا در زمان کامپایل تعریف نشده است اضافه کنید.

برای افزودن یک آیتم در آرایه، درخواست به صورت زیر می‌باشد:

 	
{ "op": "add", "path": "/Friends/1", "value": "Mike" }

این عملیات می‌تواند مقدار "Mike" را در ایندکس 1 آرایه Friends قرار دهد. همچنین می‌توانید از کاراکتر "-" برای قرار دادن یک رکورد در انتهای آرایه استفاده کنید.

{ "op": "add", "path": "/Friends/-", "value": "Mike" }

Remove

همانند عملیات "Add" که در بالا ذکر شد، عملیات Remove معمولا یک ویژگی از یک شیء یا آیتمی از یک آرایه را حذف می‌کند. زیرا در حقیقت شما در سی‌شارپ نمی‌توانید یک ویژگی را از یک شیء حذف کنید، و عملی که اتفاق می‌افتد این است که مقدار را روی پیش‌فرض (T) تنظیم می‌کند. در بعضی مواقع اگر شیء (یا نوع مرجع) بتواند خالی باشد (nullable)، روی NULL تنظیم خواهد شد. اما مراقب باشید زمانی که نوع مقدار را مشخص می‌کنید، مثلا int، در واقع مقدار روی 0 ریست می‌شود.

برای اجرای Remove روی ویژگی شیء جهت ریست کردن "reset" آن، می‌توانید دستورات زیر را اجرا کنید:

{ "op": "remove", "path": "/FirstName"}

همچنین می‌توانید عملیات Remove را برای حذف یک آیتم خاص در آرایه اجرا کنید:

{ "op": "remove", "path": "/Friends/1" }

این دستور آیتم را از ایندکس 1 آرایه حذف می‌کند. در اینجا شرط " where" برای حذف یا پاک کردن وجود ندارد، بنابراین این عمل در آیتم‌ها می‌تواند بسیار خطرناک باشد. اگر آرایه بعد از آن که ما آن را از سرور واکشی کردیم تغییر کند چه می‌شود؟ در واقع یک عملیات JSON Patch وجود دارد که به این کار کمک خواهد کرد، که بعدا درباره آن بیشتر توضیح خواهیم داد.

Replace

این عملیات می‌تواند هر مقداری را به جای مقدار دیگری جایگزین کند و می‌تواند بر روی ویژگی‌های ساده اشیاء کار کند:

{ "op": "replace", "path": "/FirstName", "value": "Jim" }

همچنین می‌تواند مقدار خاصی را داخل یک عنصر آرایه جایگزین کند:

{ "op": "replace", "path": "/Friends/1", "value": "Bob" }

همچنین می‌تواند تمام مقادیر اشیاء/آرایه‌ها را جایگزین کند:

{ "op": "replace", "path": "/Friends", "value": ["Bob", "Bill"] }

Copy

کپی یک مقدار را از مسیری به مسیری دیگر انتقال می‌دهد، که می‌تواند ویژگی، شیء، آرایه و غیره باشد. در مثال زیر، مقدار Firstname را به LastName منتقل می‌کنیم. شما به جای کپی یک جایگزینی ساده روی ویژگی‌ها را می‌بینید، اما این عمل امکان‌پذیر است! اگر واقعا کد سورس اجرای ASP.net Core توسط JSON Patch  را بررسی کنید، می‌بینید که عملیات کپی، عملیات افزودن را در پس‌زمینه خود روی هر مسیری انجام می‌دهد.

{ "op": "copy", "from": "/FirstName", "path" : "/LastName" }

Move

عملیات Move خیلی شبیه به Copy است، اما مقدار، دیگر در فیلد "from" نخواهد ماند. این مورد دیگری است که اگر پشت پرده کار ASP.net Core را بررسی کنید، می‌بینید که مقدار واقعا از فیلد from حذف می‌شود و به فیلد Path اضافه می‌شود.

{ "op": "move", "from": "/FirstName", "path" : "/LastName" }

Test

عملیات Test در حال حاضر در انتشار عمومی ASP.net Core وجود ندارد، اما اگر کد سورس را روی Github بررسی کنید، می‌توانید ببینید که در حال حاضر توسط مایکروسافت کار می‌کند و باید آن را در انتشار بعدی ایجاد کنید. Test بررسی می‌کند که اگر شیء روی سرور تغییر کرد بتوانیم اطلاعات را بازیابی کنیم.

Patch کامل زیر را ببینید:

[
	{ "op": "test", "path": "/FirstName", "value": "Bob" }
	{ "op": "replace", "path": "/FirstName", "value": "Jim" }
]

آنچه که دستور بالا می‌گوید این است که ابتدا چک کن که در مسیر " /FirstName" مقدار Bob هست، و اگر وجود داشت آن را به Jim تغییر دهد. اگر هم مقدار Bob موجود نیست، پس هیچ اتفاقی نخواهد افتاد. نکته مهمی که وجود دارد این است که شما می‌توانید بیشتر از یک عملیات Test را در patch payload داشته باشید، اما اگر هر کدام از Test‌ها با شکست مواجه شوند، کل Patch اعمال نخواهد شد.

اما چرا استفاده از JSON Patch؟

بدیهی است که یکی از مزیت‌های بزرگ JSON Patch این است که در payload بسیار سبک است و فقط همان چیزی را که روی شیء تغییر کرده است را ارسال می‌کند. اما مزیت خوب دیگری که در Asp.net Core دارد، این است که واقعا برای سی شارپ که زبان typed است بسیار سودمند می‌باشد. سخت است تا بدون مثال این مسأله را خوب توضیح دهیم. پس فرض کنید از یک API یک شیء "Person" را درخواست کرده‌ایم. در #C مدل می‌تواند شبیه دستور زیر باشد:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

و وقتی از یک API شیء JSON برمی‌گردد، مانند زیر عمل می‌کند:

{
	"firstName" : "James", 
	"lastName" : "Smith"
}

حالا از front end، بدون استفاده از JSON Patch، من تصمیم می‌گیرم که فقط firstname را آپدیت کنم، بنابراین payload زیر را ارسال می‌کنم:

{
	"firstName" : "Jim"
}

حالا یک سوال از شما دارم. بدون اینکه به پایین نگاه کنید بگویید مقادیر مدل ما چه مقداری می‌شود؟

public class Person
{
    public string FirstName { get; set; } //Jim
    public string LastName { get; set; } //<Null>
}

چون ما LastName را ارسال نمی‌کنیم، مقدار آن Null می‌شود. درست است، ما فقط می‌توانیم مقادیری را نادیده بگیریم که null هستند و در لایه داده‌ها کاری کنیم که فقط فیلدهایی که تغییر می‌دهیم آپدیت شوند. اما اگر فیلدی واقعا بتواند خالی باشد، این مسأله لزوما صحیح نیست. اما اگر به صورت زیر عمل کنیم چطور:

{
	"firstName" : "Jim", 
	"lastName" : null
}

حالا واقعا مشخص کرده‌ایم که می‌خواهیم این فیلد null باشد. اما چون #C زبان strongly typed است، هیچ راهی وجود ندارد که سمت سرور مشخص کنیم که مقدار در مقایسه با زمانی که توسط model binding استاندارد با null تنظیم می‌شود، حالا هنگام payload فاقد مقدار است.

این مثال ممکن است سناریوی عجیبی به نظر برسد، front end فقط می‌تواند همیشه مدل کامل را ارسال کند و هرگز فیلدها را حذف نمی‌کند. و در بیشتر موارد مدل کتابخانه وب front end با API مطابقت دارد. اما موردی وجود دارد که در آنجا همیشه اینگونه نیست و آن برنامه‌های موبایل است. وقتی برنامه‌های موبایل برای App Store اپل ارائه می‌شوند،‌ اغلب ممکن است هفته‌ها طول بکشد تا تأیید شوند. همچنین ممکن است برنامه‌های وب یا اندروید داشته باشید که نیاز دارند برای استفاده از برنامه‌های جدید مورد استفاده قرار گیرند. هماهنگ‌سازی بین پلتفرم‌های مختلف بسیار سخت و اغلب غیرممکن است. در حالی که نسخه API برای نگهداری این امر زمان زیادی را صرف می‌کند، ما احساس می‌کنیم که JSON Patch مزایای بسیار خوبی برای حل این مشکلات دارد.

در نهایت، payload مربوط به JSON Patch زیر را برای شیء Person ببینید:

[
    {
      "op": "replace",
      "path": "/firstName",
      "value": "Jim"
    }
]

این دستور به صراحت می‌گوید که می‌خواهیم فقط first name را تغییر دهیم نه چیز دیگری را. هیچ ابهامی وجود ندارد و دقیقا به ما می‌گوید چه اتفاقی باید بیفتد.

افزودن JSON Patch به پروژه ASP.net Core

در ویژوال استودیو، دستور زیر را از کنسول Package Manager اجرا کنید تا کتابخانه JSON Patch نصب شود.

Install-Package Microsoft.AspNetCore.JsonPatch

در این مثال از کنترلر زیر استفاده می‌کنیم. نکته‌ای که باید به آن توجه کنید این است که HTTP Verb‌ای که ما از آن استفاده می‌کنیم "Patch" است. نوع "<JsonPatchDocument<T" را می‌گذاریم و در Patch از "ApplyTo" استفاده می‌کنیم و چیزی که می‌خواهیم آپدیت کنیم را به شیء پاس می‌دهیم.

[Route("api/[controller]")]
public class PersonController : Controller
{
    private readonly Person _defaultPerson = new Person
    {
        FirstName = "Jim",
        LastName = "Smith"
    };
 
    [HttpPatch("update")]
    public Person Patch([FromBody]JsonPatchDocument<Person> personPatch)
    {
        personPatch.ApplyTo(_defaultPerson);
        return _defaultPerson;
    }
}
 
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

در این مثال ما فقط از یک شیء ساده و آپدیت آن در کنترلر استفاده کردیم. اما در یک API واقعی داده‌ها را، با استفاده از patch، از منبع داده واکشی کرده و سپس ذخیره می‌کنیم.

وقتی با payload زیر مرحله پایانی را صدا می‌زنیم:

[
	{"op" : "replace", "path" : "FirstName", "value" : "Bob"}
]

پاسخی که دریافت می‌کنیم:

{
    "firstName": "Bob",
    "lastName": "Smith"
}

عالی! first name ما به Bob تغییر یافت. این کار واقعا با JSON Patch ساده است.

تولید Patch‌ها

اولین سوالی که بیشتر مردم می‌پرسند این است که چگونه payload‌های JSON Patch خود را بسازیم. لازم نیست این کار را به صورت دستی انجام دهید. کتابخانه‌هایی بسیاری وجود دارد که می‌توانند دو شیء را با هم مقایسه کرده و patch را تولید کنند. حتی ساده‌تر از این، تعداد زیادی کتابخانه وجود دارند که می‌توانند یک شیء را مشاهده کرده و patch را براساس تقاضا ایجاد کنند. وب سایت JSON Patch لیست خوبی از کتابخانه‌ها برای شروع کار دارد: http://jsonpatch.com/

استفاده از Automapper با JSON Patch

سوال مهمی که در زمینه JSON Patch وجود دارد، این است که View Models/DTO‌های خود را از API می‌گیرید و patche‌ها را از آنجا می‌سازید. اما چگونه می‌توان این patche‌ها را به شیء پایگاه داده اعمال کرد؟ مردم تمایل دارند تا بیشتر درمورد این مسأله تفکر کنند و تغییراتی برای تبدیل یک patche‌ از یک شیء به دیگری ایجاد کنند. اما راه ساده‌تری با استفاده از Automapper وجود دارد. این دستور به این صورت عمل می‌کند:

[HttpPatch("update/{id}")]
public Person Patch(int id, [FromBody]JsonPatchDocument<PersonDTO> personPatch)
{
	PersonDatabase personDatabase = _personRepository.GetById(id); // Get our original person object from the database. 
	PersonDTO personDTO = _mapper.Map<PersonDTO>(personDatabase); //Use Automapper to map that to our DTO object. 
	
	personPatch.ApplyTo(personDTO); //Apply the patch to that DTO. 
	
	_mapper.Map(personDTO, personDatabase); //Use automapper to map the DTO back ontop of the database object. 
	
	_personRepository.Update(personDatabase); //Update our person in the database. 
 
	return personDTO;
}