مرتضی دلیل
مرتضی دلیل
خواندن ۱۰ دقیقه·۲ سال پیش

معرفی Best Practice ها در Asp.Net Core : مدل های Api

این مجموعه با نگاهی به ویدیوی آموزشی Best Practice در Pluralsight که توسط Steve Smith تهیه شده، گردآوری شده است. این مجموعه مقالات به شکل ویدیویی هم در کانال تلگرامم قرار میگیره. خوشحال میشم برای با خبر شدن از مقالات و نوشته ها و ویدیوهای آموزشی در این کانال حضور داشته باشید و نظراتتون رو بنویسید. https://t.me/mediapub_channel

فهرست مقالات مجموعه معرفی Best Practice های دات نت :



مدل های موجود در Web Api معمولا DTO هستند. DTO ها آبجکت های بدون Behavior هستند که فقط state را مشخص میکنند، یعنی فقط Property دارند. برخی DTO را با POCO اشتباه میگیرند.(Plain Old CLR C# Object )
همه DTO ها POCO هستند(DTO ها از System.Object به طور پیش فرض ارث بری میکنند) ولی همه POCO ها DTO نیستند چون میتوانند Behavior داشته باشند یعنی حاوی متد باشند.

در این مقاله منظور از مدل همه مدلهایی که استفاده میکنیم نیست بلکه مدلهاییست که در ورودی یا خروجی api استفاده میشوند و کلاینت به معنی برنامه نویس Front-End با آنها تعامل دارند.


چند ویژگی مهم و پایه مدل ها :

- تمرکز مدل ها روی انتقال اطلاعات است.
- مدل ها یا کلاس هستند یا record.
- مدل ها با اینکه حاوی متد نیستند اما میتوانند ولیدیشن را به روش Data Annotation هندل کنند. این یک خاصیت درونی در دات نت است و به کمک اتریبیوت ApiController امکان کنترل ولیدیشن را به ما میدهد.
- نامگذاری DTO ها بر پایه Entity ها نیست. مثلا CustomerDTO یا AuthorDTO نامگذاری مناسب نیستند. نامگداری مناسب متاثر از کارکرد DTO است، مثلا CreateAuthorRequest یک نامگذاری مناسب است برای یک کارکرد خاص و در جای دیگر DTO دیگر با کارکرد دیگر لازم است.

به نام مناسب این DTO نگاه کنید. شامل سه پراپرتی است. چون DTO هست هیچ Encapsulationی ندارد یعنی Protected یا Private یا Internal نیست.
به نام مناسب این DTO نگاه کنید. شامل سه پراپرتی است. چون DTO هست هیچ Encapsulationی ندارد یعنی Protected یا Private یا Internal نیست.

به مثال فوق نگاه کنید:

اولا نامگذاری مناسب استفاده شده و صرفا به AuthorDTO بسنده نشده چون کاملا کارکرد مبهمی دارد.
ثانیا همه پراپرتی ها به شکل پابلیک هستند.
ثالثا چون هدف ایجاد یک ریسورس است و کلمه Command داریم از شناسه یا Id در پراپرتی ها استفاده نشده است چون در سمت سرور تولید میشود.
رابعا با اینکه این کلاس یک DTO است و قرار نیست Behavior داشته باشد اما استفاده از Annotation ایرادی ندارد. ضروری بودن یک پراپرتی یا محدودیت در تعداد کاراکتر ها از این روش امکانپذیر است.
خامسا از ثابت ها برای ارقام مربوط به MaxLength استفاده کردیم که قابلیت تغییر در یکجا و تاثیر در همه جا را ایجاد کرده باشیم.

میتوانیم از ویژگی جدید سی شارپ یعنی record برای ایجاد DTO استفاده کنیم.

ظاهر باید تعداد خطوط کد در روش record کمتر شود ولی چون Data Annotation داریم چندان فرقی با حالت قبل ندارد. طبیعتا برای DTO هایی که در خروجی یا بین لایه ها استفاده میشوند این روش ساده تر است.

قانون Postel

در آنچه ارسال میکنی سخت گیر و در آنچه دریافت میکنی آسانگیر باش.

واضح است وقتی برنامه شما دیتایی به برنامه دیگری ارسال میکند باید دقیقا بر اساس آنچه آن برنامه میخواهد و به شکل اینترفیس در اختیار ما قرار داده دیتا را ارسال کرد. اما وقتی برنامه های دیگر، برنامه شما را صدا میزنند باید بتوانید دیتای ریکوئست هایی که کاملا دقیق و بر مبنای مدل ورودی نیستند نیز دریافت کنید.

بهتر است api های شما آسانگیر باشند و خطاهای کمتری نمایش دهند.
بهتر است api های شما آسانگیر باشند و خطاهای کمتری نمایش دهند.

قانون Hyrum

وقتی Api شما کاربران زیادی دارد مهم نیست که برای api چه قراردادی در دریافت یا پاسخ قرار داده اید، آنچه مهم است رفتار عینی کاربران و نحوه تعامل آنها با api است. سیستم شما بستگی به نحوه استفاده دیگران دارد.

مثال : فرض کنید api برای توئیتر را نوشته اید و میخواهید alias برای کاربر دریافت کنید. همچنین api شما برای مدتی نیز استفاده شده است و فرمت های مختلفی برای نمایش alias وجود دارد.

آیا راه حل استفاده از یک مدل ولیدیشن با محدودیت زیاد است؟
آیا راه حل استفاده از یک مدل ولیدیشن با محدودیت زیاد است؟
آیا اینکه همه باید با علامت @ نام کاربری خود را به عنوان alias بفرستند یک آسانگیری است؟
آیا اینکه همه باید با علامت @ نام کاربری خود را به عنوان alias بفرستند یک آسانگیری است؟

طبیعتا این روش کلاینت هایی را که انتظار ندارند همیشه از @ استفاده کنند با خطا مواجه خواهد کرد. پس استفاده از Regular expression به این شکل کار صحیحی نیست. به علاوه اینکه معمولا خطای صادر شده توسط Regular expression مبهم است.

پس دو مشکل داریم یکی عدم انعطاف پذیری در قبول alias و دیگری پیام خطای نامناسب برای کاربر.

راه حل برای رعایت قانون Postel این است که تمامی فرمت های alias را قبول کنیم ولی در کد به یک مدل واحد تبدیل کنیم. در این حالت api همیشه درست کار میکند و کلاینت ها با خطا روبرو نمیشوند.

روش فوق پیاده سازی همین ایده است اما بدون در نظر گرفتن پرفورمنس و تمیزی کد.
روش فوق پیاده سازی همین ایده است اما بدون در نظر گرفتن پرفورمنس و تمیزی کد.

چه دیتایی توسط API باید برگردانده شود؟

دیتایی که در اختیار کلاینت قرار میگیرد نباید خیلی بزرگ و نباید خیلی کوچک باشد. بزرگ بودن خروجی باعث مصرف کردن دیتای اضافی در شبکه و کند شدن میشود. دیتای کم نیز ممکن است کلاینت را مجبور به فراخوانی چندین api کند تا بتواند مجموعه دیتای مورد نیاز خود را بدست آورد.

اما چطور بفهمیم کلاینت برای فراهم کردن دیتای مورد نیاز یک صفحه به چندین api نیاز دارد؟

با در نظر گرفتن caching و data storage میتوان گفت آنچه کلاینت نیاز دارد شاید از پیش در اختیار داشته باشد. در مرحله بعد اگر هیچ دیتایی نداشته باشد پاسخ این سوال 1 است.

اگر فقط یک کلاینت داشته باشیم

اگر تنها یک Client داشته باشید بهتر است api خود را بر اساس نیاز این تک کلاینت طراحی کنید. یعنی از الگوری BFF استفاده کنید (Backend For Frontend) . در این روش مبنای طراحی بر اساس نیازهای کلاینت است. هر صفحه در بخش کلاینت مجموعه ای از دیتا نیاز دارد که ممکن است توسط بخش های مختلف از Backend قابل تامین باشد.

متدهایی که کلاینت از آنها استفاده نمیکند را expose نکنید.

تا جایی که ممکن است عملیات Data Aggregation را در سمت سرور انجام دهید که کلاینت دقیقا دیتایی را دریافت کند که به آن نیاز دارد.

اگر کلاینت های زیادی داشته باشیم(یا public api باشد)

برای هر ریسورس خود حالت summary , detailed داشته باشید. حالت summary را در api هایی که list ارائه میدهند استفاده کنید. حالت detailed را به شکل api های مستقل طراحی کنید.
ریسورس هایی که به شکل کالکشن قرار است در لیست به کلاینت ارائه شوند قابلیت sort و filter داشته باشند و یک فرمت استاندارد داشته باشند.
برای حالت های کالکشن هایی که sub-collection دارد تصمیم بگیرید که به چه شکل نمایش دهید. آیا به شکل لیست کامل نمایش داده شوند یا برای آنها نیز URI جایگزین کنیم؟

این یک api برای گیتهاب و مربوط به اطلاعات کاربر است. دقت کنید که به جای نمایش لیست بلند از فالوررهای یک کاربر از آدرس ریسورس آن استفاده شده است.
این یک api برای گیتهاب و مربوط به اطلاعات کاربر است. دقت کنید که به جای نمایش لیست بلند از فالوررهای یک کاربر از آدرس ریسورس آن استفاده شده است.


این نتیجه api مربوط به فالورهای یک user است , بار دیگر دیتیل ها به جای خود دیتا ها از URI تشکیل شده اند و ما دیتا را در اینجا به شکل Summary میبینیم.
این نتیجه api مربوط به فالورهای یک user است , بار دیگر دیتیل ها به جای خود دیتا ها از URI تشکیل شده اند و ما دیتا را در اینجا به شکل Summary میبینیم.


بصورت پیش فرض تنها 30 رکورد هر بار در لیست وجود دارد.
همانطور که میبینید اطلاعات مربوط به pagination در خروجی وجود ندارد. این اطلاعات در هدر خرجی است.

اطلاعات صفحه قبل و بعد در بخش هدر خروجی وجود دارد. که نویگیشن لینک ها هستند.
اطلاعات صفحه قبل و بعد در بخش هدر خروجی وجود دارد. که نویگیشن لینک ها هستند.


در برخی api ها اطلاعات مربوط به pagination در بدنه خروجی وجود دارد.

ملاحظاتی برای طراحی api زمانی که pagination دارند باید لحاظ شود:

  • آیا اصلا نیازی به pagination در سمت BackEnd هست؟ اگر تعداد رکوردهای خروجی زیاد باشد نیاز است. بهتر است از ابتدا پیش بینی pagination را برای api در نظر گرفت.
  • اطلاعات مربوط به pagination را در بدنه خروجی قرار میدهید یا در هدر آن؟ طبق توصیه RFC 5988 بهتر است در Response Header باشد.
  • صفحه اول بطور پیش فرض 1 است یا صفر؟ تعداد رکوردهای یک صفحه بطور پیش فرض چند تاست؟ صفحه اول را شماره 1 مینامیم و پیشنهاد میشود هر صفحه بین 30 تا 100 رکورد داشته باشد.
  • نامگذاری پیش فرض برای پراپرتی های مربوط به pagination چیست؟ page/per_page یا offset/limit
    هر دو نامگذاری رایج هستند. اگر روش offset/limit را استفاده میکنید مقدار offset برای صفحه اول از صفر باید شروع شود.


ویژگی های مدل های API

  • نامگذاری خوب (خوش-نام) : متاثر از خود مدل ها و URI باید باشد.
    - از نام قابل درک ریسورس برای کلاینت استفاده شود و نه نامگذاریهای داخلی.
    - از نامگذاریهایی که مرتبط با دیتابیس یا معماری داخلی است جلوگیری شود.
    - از پسوند DTO استفاده نکنید. استفاده از DTO در C# برای ارتباطات داخلی مانعی ندارد اما این مدل را به سمت کلاینت نفرستید!
    - مدل هایی که مستقیما معادل ریسورس ها نیستند میتوانند از پیشوند یا پسوند مناسب مثل Request,Command,Response,Result استفاده کنند.
    - به نحوه استفاده از نام ها دقت کنید چون هم در کد و هم در سوئگر استفاده میشوند. مثلا مدل مربوط به ایجاد یک Author میتواند CreateAuthorCommand باشد اما در سوئگر یک بدنه جیسون وجود دارد حاوی اطلاعات این مدل است و در حقیقت کلاینت از این نام اطلاعی ندارد.


  • با معیار درست (خوش-معیار) : منسجم و مستقل(loosly coupled)
    - چندین کانسپت را با هم در یک مدل قرار ندهید. مفاهیم بزرگ را به شکل ریسورس های جدا از هم طراحی کنید.
    - آبجکت ها را بر اساس مقادیر (primitive value) های مرتبط بسازید. مثلا آبجکت line که بر اساس چهار نقطه به شکل چهار پراپرتی StartX و StartY و EndX و EndY تعریف میشود یا میتوان نقطه شروع را یک پراپرتی و نقطه پایان را یک پراپرتی دیگر قرارداد که هر کدام یک آبجکت از تایپ Point هستند.
    - وقتی ریسورسهای بزرگ را برای مدلسازی میشکنید از ارتباط یک به یک پرهیز کنید. راه های زیاد و بهتری برای شکستن ریسورس ها وجود دارد. یکی از استثنائات برای این نکته Summary و Detail است که به شکل Uri resource مانعی برای استفاده ندارد.


  • با اندازه درست (خوش-اندازه) : میزان اطلاعات درون آنها اندازه و درست باشد.
    - از مدل های بسیار کوچک پرهیز کنید.
    - از مدل های خیلی بزرگ با sub-record های زیاد یا sub-sub-record های زیاد پرهیز کنید.
    - سایز مدل شما باید مناسب برای استفاده اکثر کلاینت های هدف باشد. مثلا اگر یک کلاینت دارید فقط برای آن کلاینت مدل را بسازید.
    - از روش summary/details برای نمایش دیتیل های رکوردها استفاده کنید (استفاده از URI)
    - از ابزاری برای paging و filtering استفاده کنید.
  • پایداری : باید پایدار باشد و تغییرات روی آن به ندرت اتفاق بیافتد.
    - اگر چیزی واقعا بعدها نیاز شد به آن اضافه شود و حذف و تغییر روی آن اتفاق نیفتد.
    - از ورژنینگ برای تغییرات مدل استفاده کنید.


آنتی پترن ها در Api Model

استفاده از نام نا مناسب و غیر استاندارد و غیر قابل درک برای Client.

مشکل بعدی شباهت مدل ها به جداول دیتابیس است که در تیم هایی که حول محور داده برنامه نویسی میکنند بیشتر اتفاق می افتد. حواستان باشد که کلاینت قرار نیست نسبت به نحوه ذخیره سازی ریسورسها مطلع باشد!

مشکل بعدی تغییر مدلها حین توسعه یا در آینده بدون استفاده از سازوکار ورژنینگ است.

مشکل بعدی عدم قابلیت برای کنترل سایز خروجی مدل های api است. مثلا جوری که بنویسید کلاینت نتواند روی تعداد رکوردهای خروجی یک لیست اختیار داشته باشد!

مشکل بعدی عدم تهیه document برای client است. مثلا عدم استفاده از swagger میتواند کلاینت را دچار مشکل کند. داکیومنت باید واضح، کامل و به روز باشد.

میخواهیم یک مدل بد برای خروجی را که وردپرس استفاده میکند بررسی کنیم.

مدل بد
مدل بد

نام ID به شکل حروف بزرگ است. جیسون معمولا به شکل camel case است. یعنی با حروف کوچک شروع میشود. پراپرتی های دیگر هم با اینکه حروف کوچک هستند ولی camel case نیستند. همه اسم ها انگار پیشوند دارند به جز Id که هماهنگی وجود ندارد.
هیچ نوع ریلیشنی برای post_author تعریف نشده و صرفا به عدد بسنده شده. همچنین برای comment هم ریلیشنی تعریف نشده.

مدل بهتر
مدل بهتر

مدل فوق اصلاح شده ی مدل قبلی است. نامگذاری camel case شده. آدرس برای author و comment به منظور نمایش ریلیشن تعریف شده است.(استاندارد لینک به ریسورس)

مدل api به منظور درک بهتر کلاینت برای کار با api تهیه میشود و نباید دیدگاه دیتابیسی یا دیدگاه معماری نرم افزاری در آن دخیل باشد.


جمع بندی :

در این مقاله مبانی استفاده از مدل های api بررسی شد. به قانون Postel پرداختیم. در مورد اینکه web api چه مدل دیتایی باید برگرداند پرداختیم. مشخصه های api خوب بررسی شد و آنتی پترن های یک api model را دیدیم.

برنامه نویسیآموزش asp coreآموزش برنامه نویسیbest practiceapi
برنامه نویس و علاقمند به برنامه نویسی، سینما، فلسفه و هر چیزی که هیجان انگیز باشد. در ویرگول از روزمرگیهای مرتبط با علاقمندیهام خواهم نوشت. در توئیتر و جاهای دیگر @mortezadalil هستم.
شاید از این پست‌ها خوشتان بیاید