پیاده سازی ساختار مناسب برای وب اپلیکیشن های Asp.Net Core

اپلیکیشن هایی که به صورت Monolithic در Asp.Net Core پیاده سازی می‌شوند، اغلب یک نقطه ورود و یا entry point تک خواهند داشت. این نوع از اپلیکیشن ها در واقع یک پروژه از نوعی Asp.Net Core را به عنوان entry point تک خود لحاظ می‌کند. البته این بدان معنا نیست که یک Solution فقط می‌تواند شامل یک پروژه باشد. در بسیاری از Solution ها اغلب یک برنامه به چندین لایه مختلف شکسته می شود و از این طریق separation of concerns به دست آورده می‌شود. زمانی که Solution به لایه‌های مختلف شکسته شود، می‌توان به سادگی پروژه ها را در فولدرهای جداگانه قرار داد و encapsulation بهتری را به دست آورد. در رابطه با encapsulation به شما توصیه می‌کنیم از بسته ی آموزش ویدئویی شی گرایی در سی شارپ دیدن کنید. بهترین روش برای بدست آوردن هدف هایی از قبیل separation of concerns در برنامه های نوشته شده با تکنولوژی Asp.Net Core پیاده سازی نسخه ای از معماری تمیز و یا Clean Architecture است که در فصل قبل از این آموزش نیز خدمتتان معرفی شد. در این رابطه توصیه می‌کنیم از آموزش ویدئویی معماری تمیز (Clean Architecture) در ASP.NET Core 3 کنید. با استفاده از معماری تمیز و یا Clean Architecture در اپلیکیشن‌های ایجاد شده با Asp.Net Core شما می توانید قسمت های مختلف برنامه از قبیل UI و Infrastructure و ApplicationCore را با استفاده از کتابخانه های مجزا ایجاد کنید.

علاوه بر این گونه از پروژه ها می توان پروژه های تست جداگانه را نیز به منظور تست کردن نرم افزار نوشته شده ایجاد کرد. در رابطه با تست نرم افزار های ایجاد شده با Asp.Net Core توصیه می‌کنیم از بسته ی آموزش ویدئویی تست نرم افزار در ASP.NET Core MVC و بسته ی آموزش ویدئویی معماری نرم افزارهای ASP.NET Core MVC برای تست پذیری دیدن کنید. ضمنا در رابطه با تست اپلیکیشن های ایجاد شده با Asp.Net Core در فصل‌های بعدی مطالب بیشتری را بررسی خواهیم کرد.

در پروژه ApplicationCore مواردی از قبیل object model و اینترفیس های مربوط به برنامه قرار می‌گیرد. این پروژه حداقل dependency ها را در خود خواهد داشت و دیگر پروژه های موجود در Solution شما به این پروژه Reference ایجاد خواهند کرد. علاوه بر این موضوع Business entity های مورد نظر که نیاز است در بانک اطلاعاتی شما Persist شوند در پروژه ApplicationCore تعریف گردیده و همچنین سرویس هایی که به طور مستقیم به infrastructure و یا زیرساخت وابستگی ندارند در این لایه قرار می گیرند.

جزئیات پیاده سازی و یا Implementation detail ها برای مثال روند Persist شدن اطلاعات در بانک اطلاعاتی و یا مکانیسم ارسال Notification به کاربر تماما بخشی از پروژه infrastructure خواهند بود. این پروژه حاوی پیاده‌سازی های دقیق مربوط به مکانیسم هایی است که قرار است در برنامه پیاده سازی بشوند. برای مثال استفاده کردن از تکنولوژی هایی از قبیل Entity Framework Core برای انجام Persistence سازی به همراه جزئیات مربوط به آن در این لایه قرار می‌گیرد. جزئیات مربوط به این گونه از مباحث نباید به بیرون از پروژه infrastructure رخنه کنند. سرویس‌ها و Repository های مربوط به infrastructure نیز می بایست Interface هایی که در پروژه ApplicationCore تعریف شده اند را پیاده سازی کنند. علاوه بر این کدهای نوشته شده برای Persistence سازی Entity ها و علاوه بر این بازیابی آنها از بانک اطلاعاتی می بایست بر روی Entity های کار کند که در ApplicationCore تعریف گردیده اند.

پروژه UI در این Solution که در حال بررسی آن هستیم مسئول مدیریت کردن جنبه‌های مربوط به واسط کاربری و یا UI برنامه می باشد و نباید حاوی جزئیات مربوط به business logic و یا پروژه infrastructure باشد. به بیان دیگر در یک حالت ایده‌آل پروژه UI نباید هیچ dependency خاصی بر روی پروژه infrastructure داشته باشد و اگر چنین روالی را پیاده کنیم باعث می‌شویم که هیچ گونه وابستگی تصادفی نیز بین دو پروژه اتفاق نیفتد. برای انجام چنین کاری می توانیم از DI container های third-party از قبیل Autofac صحبت کنیم که در بسته ی آموزش ویدئویی ساخت یک Enterprise Application با WPF و MVVM و Entity Framework از آن استفاده نموده‌ایم. ابزار Autofac تفکر اجازه می دهد که شما قوانین Dependency injection و یا DI rule های خاص خود را در کلاسهای Module در هر کدام از پروژه‌ها تعریف نمایید.

یکی دیگر از از روش‌های decouple کردن اپلیکیشن ها از Implementation detail های هر کدام از آنها، پیاده‌سازی کردن مایکروسرویس ها در Docker container های مجزا و منحصر به فرد می باشند. این موضوع separation of concerns بالاتری را برای ما فراهم کرده و به راحتی قسمتهای مختلف برنامه را بیش از آنچه که DI در اختیار ما قرار می‌دهد از یکدیگر Decouple خواهد نمود. البته پیاده‌سازی کردن مایکروسرویس ها نیز پیچیدگی‌های خاص خود را خواهد داشت.

روند صحیح سازماندهی کردن Feature ها

به طور پیش فرض پروژه های نوشته شده با فریم ورک های Asp.Net Core بر اساس ساختار فولدرهای خود فایل ها را سازماندهی می کنند. به عبارت دیگر در این نوع از پروژه‌ها اغلب فولدرهایی با نام های Controllers و Views و ViewModel قرار می گیرند. علاوه بر این کدهای مربوط به Client side اغلب در فولدری با نام wwwroot قرار داده می‌شوند. این سازماندهی برای پروژه‌های کوچک می‌تواند مناسب باشد اما پروژه های بزرگ با لحاظ نمودن چنین سازماندهی اغلب دچار مشکل خواهند شد. دلیل این موضوع نیز این است که برای کار کردن بر روی هر قابلیت و یا feature اغلب نیاز است بین فولدرهای مختلف جابجا بشویم. این موضوع با افزایش تعداد فولدرها و فایل های پروژه شرایط بدتری نیز پیدا می‌کنند و برنامه نویس نیاز است که مرتباً در Solution Explorer بین فایل ها و فولدرهای مختلف جابجا بشود. یک روش دیگر برای سازماندهی کردن بهتر یک اپلیکیشن این است که فایلها و فولدرها را بر اساس قابلیت‌ها و یا Feature ها سازماندهی کنیم نه بر اساس file type ها. این سبک سازماندهی را اصطلاحاً feature slices یا feature folders می نامند.

همانطور که احتمالا می دانید در ASP.NET Core MVC از ماهیت Area ها پشتیبانی می شود. با استفاده از Area شما می توانید مجموعه ای از Controller ها و View های مرتبط با یکدیگر را به همراه کلاسهای مدل آنها در هر کدام از فولدرهای Area قرار بدهید. تصویری که در قسمت زیر مشاهده می کنید مثالی از استفاده کردن از Area ها در یک پروژه Asp.Net Core می باشد.

زمانی که از Area ها استفاده می کنید می بایست از Attribute ها برای decorate کردن و یا تزیین نمودن کنترل ها با نام Area هایی که هر کدام از آن کنترل ها به آنها تعلق دارند استفاده نمایید. این موضوع در کد زیر نشان داده شده است.

علاوه بر این می‌توانیم از Area ها در تعریف کردن Route ها نیز استفاده کنیم. این موضوع در کد زیر نشان داده شده است.

علاوه بر استفاده کردن از قابلیت درونی Area ها می توانیم از ساختار فولدر بندی کردن منحصر به فرد و خاص خود نیز استفاده کنیم و به جای استفاده کردن از Attribute Route ها و یا Custom Route ها از Convention Route ها استفاده نمایید. این موضوع باعث می‌شود که بتوانیم feature folder هایی را داشته باشیم که شامل فولدرهای جداگانه‌ای برای Views و Controllers و غیره نباشد و علاوه بر این سازماندهی فولدرهای های درون برنامه ساده تر و قابل درک تر باشد. با استفاده از این روش نیز می توانید تمامی فایل ها و فولدر های مرتبط با یکدیگر را بر اساس Feature های مربوط به برنامه در کنار هم ببینیم.

در Asp.Net Core از Convention type های از قبل ساخته شده برای کنترل کردن رفتار Asp.Net Core استفاده می شود. شما به راحتی می توانید این Convention ها را تغییر و یا جایگزین کنید. برای مثال می‌توان یک Convention را تعریف کرد تا به صورت خودکار نام Feature مربوط به یک Controller را بر اساس namespace آن Controller به دست بیاورد. همانطور که می‌دانید namespace مربوط به یک Controller اغلب با فولدری که آن Controller در آن تعریف شده است تطابق دارند. کد زیر مثالی از این کار را نشان می‌دهد.

برای لحاظ نمودن این Convention می توانید به سادگی در ConfigureServices و در زمان اضافه کردن MVC و با استفاده از متد AddMvc آن را لحاظ کنیم. این موضوع در کد زیر نشان داده شده است:

علاوه بر این موضوع در ASP.NET Core MVC از convention هایی برای پیدا کردن مکان صحیح ویوها استفاده می شود. به سادگی می توانیم یک custom convention را در چنین شرایطی استفاده کنیم و بر اساس کد هایی که در آن قرار می دهیم ویوها را در feature folder های خاص خود لحاظ نماییم. برای یادگیری هرچه بهتر این موضوع می توانید در گوگل عبارت Feature Slices for ASP.NET Core MVC را جستجو کنید.

بررسی Cross-cutting concern ها

با رشد یک اپلیکیشن در Asp.Net Core اهمیت ریفکتور کردن cross-cutting concernها برای جلوگیری کردن از duplicationو لحاظ نمودن consistencyو یا یک شکل بودن قسمتهای مختلف برنامه بسیار اهمیت پیدا می‌کند. در رابطه با Refactoring می توانید از بسته ی آموزش ویدئویی ریفکتورینگ در سی شارپ استفاده کنید. برخی از cross-cutting concern ها در یک اپلیکیشن Asp.Net Core شامل مباحثی از قبیل authentication و model validation rule ها و output caching و error handling و غیره می باشد. با استفاده از فیلترها درAsp.Net Core شما می توانید کدهایی را قبل و یا بعد از مراحل خاصی در request processing pipeline اجرا کنید. برای مثال یک فیلتر می توانند دقیقاً قبل و یا بعد از انجام عملیات model binding و یا اجرا شدن یک action و یا حتی برگردانده شدن یک Action result اجرا بشود. علاوه بر این موضوع می توانیم از یک authorization filter برای کنترل کردن دسترسی به دیگر قسمت‌های Request pipeline استفاده کنیم. تصویری که در قسمت زیر مشاهده می کنید نحوه لحاظ شدن فیلترها در روند اجرا شدن یک request و تولید شدن Response آن را نشان می‌دهد.

در Asp.Net Core MVC اغلب از Attribute ها برای پیاده‌سازی کردن فیلتر ها استفاده می کنیم و سپس آنها را به Controller ها و یا Actionها و یا حتی به صورت سراسری لحاظ می نماییم. برای یادگیری Attribute ها می توانید از بسته ی آموزش ویدئویی Attribute ها در سی شارپ دیدن کنید. زمانی که فیلترها را با استفاده از Attribute ها لحاظ می‌کنیم سلسله مراتبی را در برنامه خواهیم داشت. به عبارت دیگر فیلترهایی که در سطح یک Action method تعریف می شوند فیلترهای تعریف شده در سطح Controller را رونویسی کرده و به همین ترتیب فیلترهایی که در سطح یک Controller تعریف می شوند فیلترهای سراسری و یا Global را رونویسی می کنند. برای مثال اگر از [Route] برای درست کردن Route ها بین Controller ‌ها و Action method ها استفاده کنیم، Route های تعریف شده به صورت گلوبال و یا سراسری در برنامه را رونویسی خواهیم کرد. به همین ترتیب می‌توانیم از authorization برای پیکربندی کردن مباحث مربوط به امنیت در سطح Controller استفاده کرده و سپس در سطح هر کدام از Action ها آن را رونویسی کنیم. مثالی که در قسمت زیر مشاهده می کنید روند انجام این کار را نشان می‌دهد.

اولین متد در این کد Login نام دارد که از یک فیلتر به نام AllowAnonymous استفاده می‌کند تا بتواند یک فیلتر دیگر به نام Authorize را که در سطح Controller تنظیم شده است رونویسی کند. علاوه بر این متد ForgotPassword و یا هر متد دیگر در این Controllerکه از AllowAnonymous به عنوان یک Attribute استفاده نمی‌کند نیاز دارند که در زمان درخواست یک کاربر آن کاربر را authenticate کند.

یکی دیگر از کاربردهای فیلترها حذف کردن duplication و یا کدهای تکراری مربوط به error handling در استفاده کردن از API ها می باشد. برای مثال فرض کنید که در یک API یک Policy را تعریف کرده‌ایم که یک NotFound response را برای request هایی که درخواست کلیدهای ناموجود را می‌دهند برگرداند. علاوه بر این یک BadRequest response را زمانی برگرداند که model validation شکست می خورد. مثال زیر نحوه انجام دادن این کار را نشان می‌دهد:

البته نباید بگذاریم که action method های درون Controller ‌ها با چنین جملات‌شرطی شلوغ شود در عوض می بایست این Policy ها را در قالب فیلترهایی لحاظ کنیم و بعد در صورت نیاز از آنها استفاده کنیم. در این مثال model validation می‌تواند در قالب یک Attribute لحاظ بشود و در زمان مورد نیاز در API از آن استفاده بگردد. کد زیر این موضوع را نشان می‌دهد:

نکته دیگر اینکه می‌توان از یک پکیج به نام Ardalis.ValidateModel استفاده کرد و از یک کلاس به نام ValidateModelAttribute که در آن تعریف شده است استفاده کرد. برای API ها می توانیم از یک Attribute به نام ApiController استفاده کنیم تا با استفاده از آن بدون لحاظ کردن فیلتر جداگانه‌ای از نوع ValidateModel رفتارهای مورد نظر خود را پیاده سازی نماییم.

به طور مشابه می‌تواند فیلترها برای برگرداندن NotFound response ها نیز استفاده کرد. با استفاده از این کار دیگر Action Method نیازی به کنترل کردن وجود و یا عدم وجود یک Resource را نخواهد داشت. زمانی که چنین کدهایی را در قالب business logic و یا infrastructure code های جداگانه در قالب فیلترها تعریف می کنیم Action Method های مربوط به برنامه به مراتب کوچکتر خواهند شد. این موضوع در کد زیر نشان داده شده است.

برای یادگیری هرچه بهتر فیلترها می توانید عبارت Real-World ASP.NET Core MVC Filters را در گوگل جستجو کنید. در درس بعدی در رابطه با بحث امنیت صحبت خواهیم کرد.