پیاده سازی پروژه API در دات نت ،  قسمت اول ، معماری پیاز (Onion Architecture)

سلام :) تصمیم دارم تو چند مقاله با هم به نوشتن یک پروژه کوچیک و البته با معماری پیاز (Onion Architecture) کنیم تا مقداری با هم در این مورد تبادل اطلاعات کنیم ، پیاده سازی پروژه API در دات نت ، قسمت اول ، معماری پیاز (Onion Architecture)پیاده سازی پروژه API در دات نت ، قسمت دوم ، ApplicationServices و DataLayerپیاده سازی پروژه API در دات نت ، قسمت سوم ، EndPoint توسط RestAPIپیاده سازی پروژه API در دات نت ، قسمت چهارم ، RestAPI Authentication Jwtپیاده سازی پروژه API در دات نت ، قسمت پنجم ، عملیات CRUD در RestAPIپیاده سازی پروژه API در دات نت ، قسمت ششم ( قسمت آخر ) ، عملیات CRUD در RestAPI پارت دوممواردی که گفته میشه ، نظر من هستش و ممکنه شما نظر متفاوتی داشته باشید که برای من محترم هست و میتونید در قسمت نظرات بنویسید :) مثال ما از نوع REST API هستش که امروزه خیلی استفاده میشه .تو این مثال ما کاربر هم تعریف میکنیم و برای عملیات ذکر شده نیاز به احراز هویت ( توسط JWT ) هستش که در مقالات بعدی میبینیم.خب ابتدا بریم تا یک سناریو ساده برای شروع کارمون داشته باشیم تا نقطه شروعمون همین باشه.سناریو ما اینه که قرار هست یکسری آزمون تعریف کنیم و عملیات CRUD ( ساختن ، خوندن ، ویرایش و حذف ) رو برای نمونه انجام بدیم ، قسمت کاربران هم داریم که به آزمون ها متصل هستند .ما از NET 6. برای ساخت پروژه هامون استفاده میکنیم.سورس کامل این مقالات رو میتونید از اینجا ببینید.برای شروع یک سلوشن خالی ایجاد میکنیم تا ساختار و پوشه های پروژه رو تعریف کنیم.شروع به اضافه کردن پوشه ها برای ساختارمون میکنیمابتدا سه پوشه به نام های Documents SrcTestsایجاد می‌کنیم ، که همونطور که از اسم هاشون مشخص هست برای فایل های مستندات ، سورس کد هامون و تست هایی که مینوسیم هستش.در ادامه در پوشه ی Src که سورس کد هامون هستش پوشه های زیر رو اضافه میکنیم01.Core02.Infrastructures03.EndPointاعدادی که ابتدای نام ها گذاشتیم برای اینه که ترتیب نمایش پوشه ها رو بتونیم مشخص کنیم.در این نمونه سعی میکنیم که از ساختار Onion Architecture استفاده کنیم ، در نظر داشته باشید که پیاده سازی ها ممکنه متفاوت باشه و جای دیگه ای ساختار به این شکل نباشه.مهمترین دلیل برای ایجاد چنین معماری ای، نیازمندی به ساختاری است تا قابلیت نگهداری برنامه ها در دراز مدت را فراهم نماید. این مورد با رعایت اصل Separation Of Concerns در سرتاسر سیستم به دست می آید.نکته حائز اهمیت در رابطه با Onion Architecture این است که این معماری برای پروژه های ساده و سبک اصلا مناسب نیست بلکه برای برنامه های بزرگ با رفتارهای پیچیده مناسب می باشد.معماری پیاز یکی از بهترین معماری های موجود برای پیاده سازی Testability ( قابل تست بودن )، Maintainability ( قابلیت نگهداری ) و Dependability ( قابلیت اطمینان ) در ساختار نرم افزار می باشد.پوشه ی اول یا همون Core ، هسته ی اپلیکیشن ما هست ، در این پوشه دو پروژه از نوع Class Librery به نام های ApplicationServices و DomainClass ایجاد میکنیم.برای پوشه ی 02.Infrastructures هم یک پروژه از نوع Class Librery ایجاد کرده و از اونجا که در این مثال از SqlServer میخوایم استفاده کنیم نام اون رو DataLayer.SqlServer میزاریم که فعلا با این قسمت کاری نداریم . در نظر داشته باشید که در این ساختار ما محدود به استفاده از ابزار دیتابیسی خاصی نیستیم ، مثلا میتونیم از Mongo و یا هر دیتابیس دیگه هم استفاده کنیم و اگر چنین قصدی داشته باشیم یک پروژه دیگه به نام DataLayer.Mongo تعریف میکنیم .در آخر برای در پوشه ی 03.EndPoint که لایه ی آخر ما هست و به اون لایه نمایش یا Persentation هم میگن یک پروژه از نوع RestApi ایجاد میکنیم و فعلا با این قسمت هم کاری نداریم ، در نظر داشته باشید که پروژه ی ما ممکنه فقط یک EndPoint نداشته باشه و مثلا بخوایم یک خروجی وب سایت از پروژمون هم داشته باشیم در این صورت می‌تونیم اینجا پروژه های بیشتری بر اساس خروجی ها مون تعریف کنیم .خب ساختار پوشه بندی و پروژه های ما تا اینجا باید مثل شکل زیر باشهدر معماری Onion Architecture ، لایه ی Domain، داخلی ترین لایه بوده و به هیچ لایه ی بیرونی وابستگی ندارد. این معماری با تکیه بر اصل Dependency Inversion تمام وابستگی های لایه ی Domain را در قالب Interface ها در اختیار آن قرار می دهد تا این لایه از جزئیات پیاده سازی و وابستگی به ابزارهای زیر ساختی در امان باشد.سراغ پروژه ی ( Class Librery ) DomainClass که داخل پوشه ی Src 01.Core هستش میریم و سه پوشه ی زیر رو بهش اضافه میکنیم.CommonUserUserExamپوشه ی Common برای تعریف کلاس های عمومی پایه ی ما هستش و پوشه های دیگه بر اساس موجودیت هایی که داریم کلاس های آنها را تعریف میکنیم در اینجا دو موجودیت کاربر و آزمون کاربر رو داریم .سراغ پوشه ی Common میریم و دو کلاس زیر رو بهش اضافه میکنیمBaseEntity.csIRepository.csکلاس BaseEntity برای تعریف کلاس پایه برای موجودیت هامون هستش ، یا این کار کد های تکراریمون حذف میشه و در آینده تغییراتمون راحتتر انجام میشه BaseEntity.cspublic class BaseEntity { [Key] public long Id { get; set; } public DateTime CreatedDate { get; set; } public DateTime ModifiedDate { get; private set; } public BaseEntity() { ModifiedDate = DateTime.Now; } }خب از اسم ها مشخص هستش که هرکدوم برای چه کاری هستند. Id از نوع Long به عنوان کلید اصلی CreatedDate تاریخ ایجاد موجودیت و ModifiedDate تاریخ آخرین ویرایش هستش که البته ویرایش اون private شده و توسط خود کلاس میتونه تغییر کنه ، همچنین در سازنده ی کلاس هم مقدار ModifiedDate رو تاریخ و زمان جاری میزاریم تا خودکار مقدار تاریخ جاری رو بگیره.این کلاس پایه برای موجودیت هامون هستش و توسط اونها ارث بری میشه.کلاس دوم به نام IRepository.cs که یک اینترفیس یا همون قرارداد هستش که مشخص میکنه مخزن داده ها یا همون ریپازیتوری هامون که قراره ازشون دیتا بخونیم قراره چه متد هایی رو پیاده سازی کنند .این کلاس هم توسط کلاس های ریپازیتوری هایی که در آینده می‌خوایم ایجاد کنیم ارث بری میشن .IRepository.cspublic interface IRepository<TEntity> where TEntity : BaseEntity, new() { void Insert(TEntity entity); void Remove(TEntity entity); void Update(TEntity entity); void SaveChanges(); TEntity Get(int id); IEnumerable<TEntity> GetAll(); IQueryable<TEntity> GetQueryable(); }در این کلاس از جنریک ها استفاده کردیم تا برای ارث بری و پیاده سازی برای کلاس های مختلف مشکلی نداشته باشیم همچنین در حالت ارث بری از این کلاس شرط گذاشتیم که حتما کلاس باید از BaseEntity که قبلتر ساختیم ارث بری کرده باشه.متد هایی که وجود دارند هم از اسمشون کاربرد اونها مشخص هستش ، مثل اضافه کردن ، ویرایش ،حذف ، ذخیره ی تغییرات و خوندن اونهاست.خب از اینجا به بعد شروع به پیاده سازی Domain های موجودیت های خودمون میکنیم ، یعنی User و UserExamUser.cspublic class User : BaseEntity { public string Mobile { get; set; } public string Name { get; set; } public string Password { get; set; } }خب کلاس User که برای موجودیت کاربران هستش و داخل پوشه ی User میسازیم ، همونطور که قبلتر گفتم از کلاس پایه مون یا همون BaseEntity ارث بری میکنیم تا پراپرتی های عمومی مثل ID و CreateDate و .. خودکار به کلاس اضافه بشن.علاوه بر اونها پراپرتی هایی مثل شماره موبایل و نام و پسورد هم برای این موجودیت تعریف میکنیم.میریم سراغ موجودیت بعدی که ازمون های کاربران هستش.UserExam public class UserExam : BaseEntity { public string Name { get; set; } public User User { get; set; } public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public bool IsDeleted { get; set; } }خب این کلاس رو در پوشه ی UserExam اضافه میکنیم و از کلاس پایمون ارث بری میکنیم و پراپرتی هایی برای نام ، کاربری که به این آزمون مرتبط است تاریخ شروع و تاریخ پایان و همچنین یک پراپرتی به نام IsDeleted برای حالت حذف آن ( Soft Delete ) اضافه میکنیم منظور از حذف منطقی اینه هستش که ما بشکل فیزیکی از روی حافظه ( دیتا بیس یا هر منبع ذخیره سازی ) موجودیت رو پاک نمیکنیم بلکه توسط یک نشونه یا flag اعلام میکنیم که این مورد از نظر ما حذف شده و استفاده نمیشه ولی بنا به دلایلی با یک نشونه این حالت رو در نظر میگیریم .خب میرسیم به تعریف قرارداد های ریپازیتوریمون یا منبع داده هامون ، اینکه از هر نوع دیتا بیسی استفاده میکنیم از این قرارداد ها ارث بری بشه تا پیاده سازی عملیات هایی که نیاز داریم مشخص باشه.برای این کار از اینترفیس پایه که برای ریپازیتوری هامون نوشتیم استفاده میکنیم تا از تکرار کد جلوگیری کنیم و تغییرات آیندمون راحتتر باشه ، به این شکل .برای اینترفیس ریپازیتوری کاربرانIUserRepository.cs public interface IUserRepository : IRepository<User> { }میبینید که هیچ پیاده سازی بیشتری در حال حاظر برای اینترفیسمون نداریم و همون متد های کلاس پایمون کارمون رو راه میندازه ، البته فعلا اینطوره.برای موجودیت آزمون کاربران هم همین کار رو انجام میدیم ، تعریف یک اینترفیس و ارث بری از کلاس پایه اینترفیس ریپازیتوریمونIUserExamRepository.cs public interface IUserExamRepository : IRepository<UserExam> { }در نهایت باید شکل سلوشنمون تا اینجا به این شکل باشهخب تا اینجا قسمت DomainClass هامون تکمیل شده .شاید اینجا سوال پیش بیاد که آیا واقعا نیاز به این همه تعریف کلاس های پایه ، ارث بری از اینترفیس های پایه و ... هستش!!در جواب بله ، شاید در نگاه اول نیاز به خیلی از این موارد نباشه ولی رعایت این موارد در توسعه ی پروژه بسیار کمک میکنه و مهم تر از اون کد تمیز و توسعه پذیری داریم که تمام قواعد اون مشخص هستش ، در مقاله بعدی میریم سراغ قسمت ApplicationServices و کارمون رو ادامه میدیم. البته هنوز کلی از کارمون مونده ولی سورس کامل این مقالات رو میتونید از اینجا ببینید.امیدوارم از این مطلب خوشتون اومده باشه :)