این مجموعه با نگاهی به ویدیوی آموزشی Best Practice در Pluralsight که توسط Steve Smith تهیه شده، گردآوری شده است. این مجموعه مقالات به شکل ویدیویی هم در کانال تلگرامم قرار میگیره. خوشحال میشم برای با خبر شدن از مقالات و نوشته ها و ویدیوهای آموزشی در این کانال حضور داشته باشید و نظراتتون رو بنویسید. https://t.me/mediapub_channel
فهرست مقالات مجموعه معرفی Best Practice های دات نت :
علیرغم ظهور Minimal Api ها و پرکاربرد شدنشان در آینده نزدیک، اما استفاده از MVC هنوز یکی از راه های متداول برای تولید api است.
فریم Asp Core MVC قابلیت های زیادی برای تولید Api های بهینه دارد. بخشی از این قابلیت ها توسط ابزارهای زیر قابل پیاده سازی است.
با کاهش کدهای داخل اکشن ها به این هدف میرسیم. باید "منطق و محاسبات" مربوط به اپلیکیشن را به بخش های مربوط به خودشان منتقل کرد. معمولا داخل اکشن ها پیاده سازی شرط و تصمیم ندارد. اگر مجبور به بررسی مواردی در اکشن ها باشید باید از Filter استفاده کنید. باید از نوشتن Business logic یا Data Access logic در داخل اکشن ها بپرهیزید. اگر اینها را رعایت کنید میبینید که چیز قابل ذکری برای پیاده سازی در اکشن ها باقی نمیماند!
در اکشن های خودتان از نوشتن try/catch بپرهیزید. کنترل خطا هم نوعی مرتبط با شرط و تصمیم است که پیش از این گفتیم نباید در اکشن ها وجود داشته باشد. پیاده سازی try/catch برای تولید خطا در خروجی یک نوع anti pattern است. روش درست این است که سرویس های ما خطا را به شکل خاص تولید کنند و ریترن کنند و ما نیازی به تولید ریسپانس بر اساس exception نداشته باشیم.
ایجاد یک کنترلر بزرگ باعث موارد زیر میشود:
اگر به api های خود دقت کنید و تک تک endpoint ها و اکشن های خود را ببینید و کدهای مشترک فراوانی پیدا کنید، این شانس را دارید که تمام این کدها را refactor کرده و در فیلترها قرار دهید.
اگر عملیات شما مرتبط با model binding یا چیزهای سطح پایین تر از فیچرهای MVC باشد میتوانید از middleware ها استفاده کنید.
استفاده از filter و middleware باعث پایداری بیشتر api های شما میشوند.
هرگز به حافظه توسعه دهنده ها برای رعایت توصیه ها در کد تکیه نکنید، به جای آن مجموعه ای از ضوابط را به کمک فیلترها و middleware ها به پروژه متصل کنید.
مثلا ApiController که به شکل اتریبیوت روی کنترلر مینشیند یک فیلتر است که عملیات Model Validation را انجام میدهد و در ورودی اکشن ها ما را از استفاده از FromRoute یا FromBody و ... را بی نیاز میکند. یا ایجاد میدلویر برای Logging یا Exception ها از جمله مواردیست که میتواند به پایداری و تمیزی کد شما کمک کند.
هر api به طور بالقوه میتواند پیشوند async داشته باشد، میتواند به دیتابیس وصل شود یا به api های دیگر پیام ارسال کند یا با فایل ها کار کند. به ندرت پیش می آید که یک api داشته باشید که نیاز به پیشوند async نداشته باشد.
از این اتریبیوت برای کش کردن ریسورس ها میتوان استفاده کرد. افزودن این اتریبیوت باعث اضافه شدن چیزی به هدر ریکوئست میشود که در سمت کلاینت یا intermediate proxy قابل استفاده است و تعیین میکند که چگونه response باید کش شود.
دقت کنید که ریسپانس ها به طور پیشفرض روی وب سرور ذخیره نمیشوند تنها کار اولیه ای که این اتریبیوت با افزودن هدر کش میتواند انجام دهد بر اساس HTTP Caching با پروتکل RFC 7234 است. با این وجود اگر میخواهید ریسپانس ها را در حافظه سرور ذخیره کنید تا از مزیت عدم استفاده از دیتابیس و کوئری نگرفتن در مواقع مورد نیاز استفاده کنید.
ما برای response cache از میدل ویر هم میتوانیم استفاده کنیم که با همین اتریبیوت ترکیب میشود. در این حالت باید از VaryByParam استفاده کرد تا ریسپانس متفاوت از یک ریکوئست بر اساس کوئری استرینگ را هندل کرد. مثلا اگر از کوئری استرینگ برای paging استفاده میکنید و صفحه 1 را کش کرده اید و کسی از شما صفحه 2 را میخواهد (کوئری استرینگ عوض شده) نباید این اشتباه رخ دهد که همان صفحه 1 را به فرد جدید نمایش دهید. چون اطلاعات در سمت سرور ذخیره میشود زمانی که توسعه به شکل چند سروری انجام میشود روش مناسبی نیست.
در تصویر بالا میبیند که مدت زمان کش را 10 ثانیه و محل کش را روی Any تنظیم کردیم. آنچه در هدر ریسپانس دیده میشود به شکل کادر بنفش رنگ است. با این قرارداد client و intermediate proxy میفهمند که کش را در کجا و در چه مدت زمانی انجام دهند.
بصورت پیش فرض بر اساس فیچرهای وب سرور فشرده سازی انجام میشود که از پیاده سازی فشرده سازی در middleware سریعتر است. مثلا IIS به طور پیش فرض از فشرده سازی پشتیبانی میکند.
اگر قرار هست خودتان فشرده سازی را به عهده بگیرید از فشرده سازی ریسپانس های کوچک بپرهیزید چون دستاوردی ندارد و حتا ممکن است در بعضی الگوریتم ها علاوه بر صرف زمان نتیجه فشرده سازی حجم بیشتری از دیتای اولیه داشته باشد.
بهتر است زیر 1000 بایت را بیخیال شوید.
به کمک هدر Accept-Encoding کلاینت باید به سرور اطلاع دهد که از چه Enodingی پشتیبانی میکند. سرور نیز باید بر اساس این Encoding و آنچه استفاده کرده است هدر Content-Encoding را در ریسپانس تولید کند.
برای HTTPS موضوعات امنیتی برای فشرده سازی باید لحاظ شود.
در هنگام بروز خطا، معمولا در حالت پروداکشن اطلاعات کمتری به سمت کلاینت ارسال میشود چون اطلاعات خطا برای توسعه ضروریست و ممکن است در معرض دید قرار گرفتن آن برای همه، اطلاعات ناخواسته ای از سیستم را منتشر کند.
ما در این تصویر یک هندلر با دو آدرس متفاوت برای هندل کردن خطاها ایجاد کرده ایم که نحوه نمایش هر کدام متفاوت خواهد بود.
این اکشن از IExceptionHandlerFeature که به طور Built-in در دات نت وجود دارد استفاده میکند تا StackTrace و Message مربوط به exception را نمایش دهد.(دقت کنید که ریترن ما Problem باید باشد)
یکی از کتابخانه هایی که به ما کمک میکند تا کنترلر سبک تری داشته باشیم MediatR است.
الگوی Madiator یکی از 23 الگوی طراحی در شی گرایی است که در کتاب Design Pattern منتشر شده در 1994 توضیح داده شده است.
اصطلاح Gang of Four برچسب گروهی بود که شامل اریک گاما (Erich Gamma)، ریچارد هلم (Richard Helm)، رالف جانسون (Ralph Johnson) وجان ولیسیدس (John Vlissides) میشد. این افراد نویسندگان کتاب مهندسی نرمافزار Elements of Reusable Object-Oriented Software بودند.
ما یک آبجکت MediatR میسازیم که بین آبجکت صدا زده شده(اکشن) و آبجکت تولید کننده ریسپانس(هندلر) مسیج رد و بدل میکند.
این کتابخانه به این شکل عمل میکند که به ازای هر ریکوئست یک اینترفیس جنریک از تایپ دیتای ورودی (یا تغییر یافته آن) نیاز دارد. همچنین به یک هندلر جنریک با دو تایپ TReqeust و TResponse نیاز دارد. کلاس TRequest باید بر اساس IRequest پیاده سازی شود.
پس ما یک کلاس به عنوان Request و یک کلاس به عنوان Request Handler داریم.
وقتی application راه اندازی میشود MediatR به کمک reflection از روی assembly تمامی این کلاس ها را پیدا میکند یعنی تمامی کلاس هایی که IRequest را پیاده سازی کرده اند. همچنین به کمک dependenct injection در جاهایی که IMediatR اینجکت شده است سرویس مورد نظر را در Program.cs تعریف میکند.
به کمک متد send این سرویس میتواند چیزی از جنس IRequest دریافت کند که طبیعتا تمامی endpoint های ما باید کلاسی از روی این اینترفیس به عنوان ورودی پیاده سازی کنند.
کتابخانه به کمک همان رفلکشن، هندلر مرتبط را می یابد و آبجکت ورودی را به آن پاس میدهد. (با invoke کردن handler)
در نهایت ریترن ما چیزی از جنس TResponse است که به اکشن برگردانده میشود.
اگر به سختی متوجه مطلب شدید یا نشدید! نگران نباشید؛ چند خط پایین تر با مثال این موضوع را خواهید دید.
اگر علاقمند به این موضوع هستید مجموعه مقالات Clean Architecture را مطالعه کنید. در این مجموعه کار با MediatR توضیح داده شده است.
با این روش هیچ وابستگی به شکل Compile time بین اکشن و هندلری که صدا زده میشود وجود ندارد و همه چیز runtime اتفاق می افتد.
این کتابخانه برای notification نیز روش مشابه ای دارد با این تفاوت که ریترنی بر نمیگرداند.(حتا چندین هندلر میتوانند نوتیفیکیشن را هندل کنند)
در اکثر endpoint هایی که با data سروکار دارند و به عنوان web api شناخته میشوند رفتار مشترک تغییر این اطلاعات است و معمولا پیاده سازی مراحل زیر را دارد :
اول : یک مدل ورودی داریم که باید validate شود. این بررسی به شکل رایگان و Built-in در web api وجود دارد، کافیست اتریبیوت ApiController را در بالای کنترلر بنویسیم.
دوم : مدل ورودی باید به یک دیتامدل جدید و قابل استفاده برای هندلر تبدیل شود. ما نباید آنچه مربوط به Business است در معرض عموم قرار دهیم یعنی Business Model ما باید از روی مدل ورودی ساخته شود و مدل ورودی بهینه ترین حالت برای گرفتن اطلاعات است (نه اطلاعات زیاد و نه کمتر از حد معمول)
سوم: کار واقعی مدل اینجا شروع میشود، آیا تغییری در سیستم توسط این مدل باید اعمال شود؟ آیا متدی باید صدا زده شود؟ چیزی باید ذخیره شود؟ وضعیت ها باید تغییر کنند؟ همه این چیزها در همین مرحله اتفاق می افتد.
چهارم : مدل مورد نیاز به عنوان Response باید تولید شود. اگر عملیات موفق بود و نیازی به دیتا نبود نیاز به ارسال Business model نیست.
پنجم : یک پاسخ مناسب در بستر HTTP بر اساس مدل ایجاد شده در مرحله قبل به بیرون ارسال میشود.
تعداد کمی از این مراحل 5 گانه در خود اکشن اتفاق می افتد. مخصوصا اگر از MediatR استفاده کنید.
حتا اینجکت سرویس های مختلف در کنترلرهای مختلف در این روش اتفاق نمی افتد.
یک اکشن داریم که آبجکت newAuthor را به شکل HttpPost دریافت میکند و سپس در خطوط 44 تا 48 آن را به یک آبجکت Author (این آبجکت از تایپ های Domain است ) مپ و در خط 50، مدل را ذخیره میکند. در نهایت در خط 52 آنچه به عنوان response مورد نیاز است ایجاد کرده و در خط 53 همان را ریترن میکند.
به کمک کتابخانه MediatR کد اکشن را به شکل زیر اصلاح میکنیم. برای استفاده از MediatR باید آن را به کنترلر اینجکت کنیم، میتوانیم یک BaseController به شکلی که میبینید بسازیم و پراپرتی آن را در آنجا تعریف کنیم که نیاز به اینجکت کردن در هر کنترلر به شکل جدا نباشد.
در اکشن ابتدا به کمک مدل ورودی، آبجکت Command را که مورد استفادهی هندلر MediatR است میسازیم.
سپس به کمک متد send این آبجکت را به عنوان ورودی پاس میدهیم. چون جنس این آبجکت مشخص است و معلوم است از چه اینترفیسی ارث بری کرده است، MediatR میداند هر اینترفیس مرتبط با کدام هندلر است (این دانایی از رفلکشن موجود در program.cs آمده است!)
خروجی هندلر مورد نظر نیز به عنوان خروجی api به بیرون فرستاده میشود.
میتوان به جای استفاده از مفهوم کنترلر و اکشن مجموعه ای از endpoint ها معرفی کرد و ارتباط مدل بیرونی با بیزنس داخلی را نزدیک تر کرد.
یک پکیج به نام Ardalis.ApiEndpoint داریم که کلاس های پایه ای را برای اینکار فراهم میکند.
حالت جنریک به شما این امکان را میدهد که هر نوع ریکوئستی را به هر نوع تایپی منتهی کنید.
اساس این پکیج، الگوی PEPR است.
همانطور که میدانید MVC براساس سه کلمه Model-View-Controller است و برای ایجاد یک پروژه به همراه طراحی رابط کاربری مناسب است. در زمانی که شما Api میسازید هیچ ویویی در کار نیست. یعنی الگوی شما MC میشود یعنی Model-Controller یا مثلا Model-Action-Controller که میشود الگوی MAC!!
نکته اینجاست که شما باز هم از MVC استفاده نمیکنید پس چرا به الگوی بهتری به جای این الگوی نصفه فکر نکنیم؟
Api Endpoints یک ویژگی مناسب برای اینکار است که این قابلیت در دات نت وجود دارد. الگوی REPR سه قسمت دارد :
بخش Request : شکلی از دیتا که endpoint نیاز دارد.
بخش EndPoint : منطقی که endpoint روی request پیاده میکند.
بخش Resonse : پاسخی که endpoint به سمت صدا زننده بر میگرداند.
این مقاله را ببینید
با این روش هر endpoint در api های شما متناظر با یک فایل یا کلاس endpoint میتواند باشد، که هندلر در آن قرار دارد.
به کمک ویژگی های Visual Studio در اهمیت دادن به نامگذاری ها میتوانید جوری api های خود را نامگذاری کنید که گروه بندی شوند. با کمک prefix این اتفاق می افتد مثلا api مربوط به Create میتواند به شکلی که میبینید دسته بندی شود. دسترسی به Dto ها هم راحت تر است و در کنار endpoint مرتبط با خودشان هستند.
در تصویر بالا فایل endpoint مربوط به عملیات create را میبینیم. کل endpoint را در یک صفحه میبینید. (بر خلاف MVC که اکشن و هندلر دو بخش جدا از هم بودند)
هر Endpoint باید از کلاس EndpointBaseAsync ارث بری کند.
همچنین برای ریکوئست و ریزالت دو تایپ WithRequest و WithActionResult تعریف شده که حتا از NoResult و NoRequest هم پشتیبانی میکند.
داخل HandleAsync رفتاری شبیه به هندلر MediatR داریم.
مثال دیگری ببینیم.
آپدیت تفاوت چندانی با Create ندارد فقط در HttpPut ما پارامتر id را دریافت کردیم. این id در آبجکت request هم وجود دارد که توسط همان پارامتر پر میشود. این ویژگی در MVC و Web api وجود نداشت.
همانطور که میبیند Id وجود دارد و از طریق Routing پر میشود و Required است.
از اتریبیوت JsonIgnore هم استفاده کردیم که در سوئگر دیده نشود.
آنچه در swagger دیده میشود به شکل زیر است
نکته کلیدی در استفاده از پکیج هایی مثل fast endpoint یا minimalApi.Endpoint این است که میتوانید endpoint های خود را دسته بندی کنید.
با تصویر زیر به سادگی این روش مشخص است:
در خروجی minimal api باید تایپ IResult برگرداند که پیاده سازی های متنوعی دارد.
مشکل بزرگ minimal api این است که مایکروسافت یک داکیومنت خوب برای سازماندهی آنها تهیه نکرده است. طبیعتا قراردادن ده ها یا صدها مینیمال ای پی آی درون program.cs کار درستی نیست. فایل های بزرگ برای سازماندهی و پیدا کردن بخش های مورد نظر آزاردهنده هستند. همچنین کار تیمی روی پروژه ای که فایل های بزرگ دارد مشکل است.(به دلیل تداخل ها هنگام merge و commit)
باید بتوانیم از فولدرها و فایل ها برای سازماندهی endpoint ها استفاده کنیم.
فولدرها متناظر با endpoint ها باید باشند.
به طور ایده ال بهتر است هر endpoint فایل خودش را داشته باشد.
به دو روش میتوان این سازماندهی را انجام داد:
1. استفاده از پکیج MinimalApi.Endpoint
2. استفاده از Extension Methods
مینیمال api ها از داخل program.cs ستاپ میشوند.
در تصویر فوق دو minimal api میبینیم کی در خط 53 و دیگری در خط 58 که تنظیمات اضافی متادیتاهای سوئگر را دارد.
api های واقعی ظاهری شبیه تر به همان خط 58 یعنی delete دارند. یعنی طولانی تر هستند. بدیهیست تعداد خطوط فایل program.cs بیش از 1000 خط میشود اگر با یک اپلیکیشن واقعی سروکار داشته باشیم. چطور میتوان این خطوط را به فایل های مستقل endpoint تقسیم بندی کرد؟
یکی از راه ها استفاده از کتابخانه MinimalApi.endpoint است. این کتابخانه در خط 43 تصویر زیر قابل مشاهده است.
با این روش تمامی endpoint هایی که اینترفیس IEndpoint را پیاده سازی کرده باشند داخل بازی محسوب میشوند.
به اینترفیس ها نگاه کنید ورودی های جنریک آن نشان میدهد که مدل CreateAuthorRequest را میگیرد و IResult را برمیگرداند.
دو متد را با توجه به این اینترفیس ها باید پیاده سازی کنیم. AddRoute و HandleAsync
در اولین متد عملیات مپ کردن به متد درست Http به همراه آدرس انجام میشود. همچنین آبجکت ریکوئست و اعمال دیپندنسی در این بخش انجام میشود.
در متد دوم عملیات اصلی Endpoint پیاده سازی میشود.
راه دیگر برای جداسازی endpoint ها و قرار دادن آنها در فایل جدا استفاده از extension method هاست.
با این روش برای هر endpoint متد و کلاس مربوط به خودش در فایل جدا خواهیم داشت.
میتوانید به جای اینکه برای تک تک endpoint ها اکستنشن جدا معرفی کنید، آنها را دسته بندی کنید و مثلا آنهایی که مربوط به Author هستند در یک اکستنشن متد قرار دهید.
بطور کلی شکستن endpoint ها به فایل های جدا برای مدیریت آنها روش درستی است. با نگاه کردن به فایل ها به راحتی متوجه میشوید کدام endpoint ها پیاده سازی شده و چه چیزهایی باقیماندهاست.
زمانی که از یک message bus اکسترنال یا یک صف برای برقراری ارتباط با سرویس های دیگر استفاده میکنید دات نت کور به شکل Built-in میتوند از روش Background Service در کنار پروژه Api شما استفاده کند.
از AddHostedService استفاده خواهیم کرد و نیازی به اضافه کردن پکیج جدید از Nuget نیست.
روش کار به این شکل است که یک سرویس به شکل تایپ تعریف میکنید که از تایپ BackgroundService ارث بری کند. حالا متد ExecuteAsync را پیاده سازی میکنید. در این متد میتوانید Delay خود را معرفی کنید که فواصل زمانی را کنترل کند. یعنی در چه بازه های زمانی عملیات درون این متد تکرار شوند.
در مثال بالا ما یک worker به نام DataConsistencyWorker تعریف کرده ایم که بطور پریودیک دیتابیس را برای داده های ناپایدار چک میکند و اگر چیزی نیاز به اصلاح داشت اصلاح میکند.
چون قرار است با دیتابس کار کنیم باید از DbContext و Repository استفاده کنیم.
در خط 41 سرویسی به شکل Scope تنظیم شده که این بک گراند سرویس با آن کار کند.
کانستراکتور IServiceProvider را میگیرد و به سرویس ها میتواند دسترسی داشته باشد. بک گراند سرویس ها به طور Singleton در اپلیکیشن وجود دارند یعنی فقط یک نمونه از آنها در زمان شروع اپلیکیشن ساخته میشود و در دفعات بعدی که سرویس بک گراند اجرا میشود یک نمونه جدید از سرویس نخواهیم داشت.
در داخل متد DoWork میبینیم که از آبجکت Services که از IServiceProvider ساخته شده استفاده شده است.
سرویسی که از آن استفاده شده به شکل زیر است:
کاری که این متد میکند این است که از دیتابیس تمامی نام کاربری هایی که ولید نیستند بیرون کشیده و اصلاح میکند. و اینکار را هر 30 ثانیه تکرار میکند.
در این مقاله مطالب زیر را مرور کردیم :
با قابلیت های MVC آشنا شدیم.
با MediatR آشنا شدیم که باعث شد کنترلر های ما کوچک تر شوند و منطق برنامه در جایی دیگر و در هندلر خودش کپسوله شود.
با الگوی REPR آشنا شدیم و فهمیدیم که برای api ها الگوی MVC بخش View را ندارد و این الگو جایگزین مناسبی برای MVC است.
با Minimal Api ها ونحوه سازماندهی آنها به کمک اکستنشن متد و MinimalApi.Endpoint آشنا شدیم.
در نهایت هم با Background Service ها مقاله را پایان دادیم.