مرتب سازی در ASP.NET Core Web API
شناسه پست: 2513
بازدید: 1029

در این مقاله، ما میخواهیم در مورد مرتب سازی در ASP.NET Core Web API صحبت کنیم. مرتب سازی، یک مکانیسم رایجی است که هر API باید پیاده سازی کند. پیاده سازی آن در ASP.NET Core، به دلیل انعطاف LINQ و یکپارچگی خوب با EF Core، دشوار نیست.

اگر میخواهید ما را در این مقاله همراهی کنید، میتوانید از نقطه شروع پروژه استفاده کنید و اگر میخواهید solution نهایی را بگیرید یا اگر جایی در پروژه برایتان نامفهوم است میتوانید نقطه انتهایی پروژه را ببینید.

توجه: برای دنبال کردن این مقاله، کمی دانش پیشنیاز لازم است. اگر نحوه ایجاد دیتابیس یا نحوه کارکرد معماری مورد نظر را نمیدانید، به شدت توصیه میکنم که مقاله های مورد نظر را رصد نمایید.

در این مقاله ما یاد میگیریم که:

شروع کنیم.

مرتب سازی چیست؟

مرتب سازی، در این حالت، اشاره به چینش ترتیب نتایج ما به شیوه مورد نظر، با استفاده از پارامترهای query string دارد. ما نه در مورد الگوریتم های مرتب سازی صحبت می کنیم و نه به چگونگی پیاده سازی الگوریتم های مرتب سازی می پردازیم.

آنچه که ما دنبال آن هستیم این است که چطور کاری کنیم که API ما، نتایج را به آن صورتی که مدنظر ما است را مرتب سازی کند.

اجازه دهید بگوییم که میخواهیم API ما، owner ها را توسط نامشان در یک ترتیب صعودی (ascending) و سپس توسط تاریخ تولدشان به ترتیب نزولی مرتب کند.

به این منظور، API ما باید چیزی شبیه زیر باشد:

API ما باید تمام پارامترها را در نظر گرفته و نتایج ما را بر اساس آن مرتب کند. در این مورد ما، این به معنای مرتب سازی نتایج بر اساس نام آنها است و سپس اگر owner های همنام وجود داشته باشد ، آنها را بر اساس ویژگی DateOfBirth مرتب سازد.

یک یادآوری داشته باشیم که داده های مربوط به owner های ما در دیتابیس به چه صورت است:

owner های دیتابیس

برای نشان دادن این مثال، میخواهیم یک Anna Bosh دیگر به دیتابیس اضافه کنیم. برای آزمایش نتایج می توانید هر آنچه را که می خواهید را اضافه کنید.

بنابراین اجازه دهید تا Anna Bosh دیگری را اضافه کنیم:

owner های دیتابیس

این Anna جدید، 10 سال از Anna قبلی بزرگتر است و در یک آدرس متفاوت زندگی میکند.

عالی، حالا ما داده های مورد نیاز را برای تست ویژگی عملکردمان در اختیار داریم.

و البته همانند بقیه ویژگیهایی که تا الان پیاده سازی کردیم (صفحه بندی، فیلترینگ و جستجو)، برای خوب کار کردن با بقیه موارد، باید این مورد را نیز پیاده سازی کنیم. برای مثال، ما باید بتوانیم داده ها را صفحه بندی، فیلتر و مرتب سازی نماییم.

بریم تا راه پیاده سازی این ویژگی را با هم ببینیم.

نحوه پیاده سازی مرتب سازی در ASP.NET Core Web API

اول از همه، از آنجایی که می خواهیم هر موجودیتی براساس تعدادی معیار، قابلیت مرتب سازی داشته باشد، از این رو قصد داریم ویژگی OrderBy را به کلاس پایه QueryStringParameters اضافه کنیم:

ما هیچ مقدار پیشفرضی را برای آن تعیین نمیکنیم، به این دلیل که هر کلاسی میتواند مقادیر پیش فرض خودش را داشته باشد.

به عنوان مثال، میخواهیم OwnerParameters، به صورت پیشفرض، نتایج را بر اساس “name” مرتب سازی کند:

و میخواهیم Account های ما، توسط ویژگی DateCreated مرتب سازی شوند:

در مرحله بعد، میخواهیم به مکانیسم مرتب سازی بپردازیم.

نکته ای که باید به آن اشاره کنم این است که میخواهیم از System.Linq.Dynamic.Core NuGet package برای ایجاد کوئری OrderBy به طور داینامیک در آن واحد استفاده کنیم. بنابراین، در صورت تمایل آن را در پروژه Repository نصب کنید و using مورد نظر را در کلاس OwnerRepository اضافه کنید.

یک متد  private جدید به نام ApplySort در کلاس OwnerRepository مان اضافه میکنیم:

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

حال بیاید تا متدمان را کالبدشکافی کنیم.

پیاده سازی – مرحله به مرحله

اول اجازه دهید تا با تعریف متد، کارمان را شروع کنیم. آن، دو ورودی میگیرد. اولی IQueryable<Owner> میباشد که برای گرفتن لیست owner هاست و دومی برای query string جهت مرتب سازی. اگر ما یک درخواست مانند https://localhost:5001/api/owner?orderBy=name,dateOfBirth desc بفرستیم، ورودی orderByQueryString ما، name,dateOfBirth desc خواهد بود.

ما پیاده سازی متدمان را با چند بررسی لازم برای owner ها و queryString شروع میکنیم. اگر هیچ owner ای وجود نداشته باشد، فورا از متد خارج میشویم. اگر لیست ما شامل owner باشد، اما query string خالی باشد، سپس owner ها را توسط نامشان مرتب میکنیم:

در مرحله بعد، query string مان را برای گرفتن فیلدها به صورت جداگانه، split میکنیم:

همچنین برای تهیه لیست آبجکتهای PropertyInfo که ارائه دهنده خصوصیات کلاس Owner ما است کمی از reflection استفاده میکنیم. ما به آنها، برای اینکه بررسی کنیم که فیلد دریافت شده از طریق query string در کلاس Owner  واقعا وجود دارد یا خیر، نیاز داریم:

با داشتن این لیست، در واقع میتوانیم کل پارامترها را بررسی کنیم تا موجودیتشان را بررسی نماییم:

اگر ما پارامتر فعلی را پیدا نکردیم، سپس این مرحله را در حلقه foreach رد میکنیم و به سراغ پارامتر بعدی در لیست میرویم:

اما اگر پارامتر مورد نظر در لیست پارامترها وجود داشت، آن را برگردانده و علاوه بر آن، بررسی میکنیم که آیا پارامتر ما در انتهای رشته، شامل “desc”  است یا خیر. ما این کار را به این خاطر انجام میدهیم تا تعیین نماییم که مرتب سازیمان را بر اساس این ویژگی، به صورت صعودی انجام دهیم یا نزولی:

ما از StringBuilder برای ساخت query در هر تکرار از حلقه استفاده میگنیم:

حال که تمام فیلدها را در حلقه پیمایش کردیم، باید کاما اضافی را در انتهای رشته حذف کنیم و یک بررسی نهایی انجام دهیم و ببینیم که کوئری مورد نظر ما، آیا چیزی را شامل میشود یا خیر:

در آخر، میتوانیم کوئریمان را order کنیم:

در این مرحله ، متغیر orderQuery ما باید شامل رشته “Name ascending, DateOfBirth descending” باشد. این بدان معنی است که نتایج ما را ابتدا بر اساس Name به ترتیب صعودی ، و سپس براساس DateOfBirth به ترتیب نزولی مرتب سازی میکند.

کوئری استاندارد LINQ برای این مورد ما به این صورت میباشد:

این یک ترفند کوچک شسته و رفته به شکل یک کوئری برای زمانی است که از قبل نمی دانید که مرتب سازی بر اساس چه فیلدی باید انجام شود.

فراخوانی متد

اکنون که نحوه کارکرد متد را دیدیم، فقط باید آن را در متد GetOwners خود فراخوانی کنیم:

ما لیستی از owner ها و query string OrderBy را به متد پاس می دهیم.

تمام کاری که باید برای پیاده سازی انجام میدادیم همین است. یا شاید نه؟ اگر بخواهیم از ApplySort در AccountRepository استفاده کنیم باید چیکار کنیم؟ آیا باید این logic را در کلاس repository مان نگه داریم؟

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

بیاید ببینیم که چطور میتوانیم متدمان را عمومی تر بسازیم، طوریکه برای هر دو Account repository و Owner repository (یا هر repository دیگری که بعدا اضافه میشود) بتواند مورد استفاده قرار بگیرد.

بهبود Solution

دو چیز است که ما میخواهیم آن را بهبود دهیم. متد private ApplySort را از OwnerRepository خارج کنید و ApplySort را به صورت generic ایجاد کنید.

اجازه دهید کارمان را با تعریف کلاس SortHelper و اینترفیس ISortHelper در پوشه  Helpers پروژه Entities شروع کنیم.

اینترفیس ISortHelper باید یک متد ApplySort را تعریف کند:

همانطور که میبینید، ISortHelper یک اینترفیس generic است و میتواند بر روی هر نوعی که ما بخواهیم اعمال شود. ما باید یک کالکشنی از موجودیتها و یک رشته مرتب سازی را در این متد لحاظ کنیم.

حال بیاید تا پیاده سازی واقعی را ببینیم:

این پیاده سازی، قابلیت تزریق این کلاس را به درون repository هایمان و فراخوانی ApplySort هرزمان که به آن نیاز داشتیم را میدهد:

همچنین باید RepoWrapper مان را توسعه دهیم چرا که کلاسهای repository مان اینجا نمونه سازی شده اند:

و حالا میتوانیم آن را در متد GetOwners مان فراخوانی کنیم:

همین کار را در AccountRepository نیز انجام دهید. اگر با پیاده سازی آن مشکل دارید، به پروژه نهایی که لینک آن را در ابتدای مقاله برای شما گذاشتم مراجعه کنید.

و چون ما از تزریق وابستگی برای تزریق SortHelper مان استفاده کرده ایم، نباید فراموش کنیم که آن را در کلاس ServiceExtensions ثبت کنیم:

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

حال بیاید تا آن را تست کنیم.

تست پیاده سازی

حالا قسمت جالب ماجرا شروع می شود، بیایید نتیجه تمام تلاش خود را برای آزمایش ببینیم.

اول از همه، کوئری که به عنوان یک مثال استفاده کرده بودیم را امتحان میکنیم:

پاسخی که میگیریم باید به این صورت باشد:

همانطور که میبینید، لیست به ترتیب صعودی توسط نام، مرتب شده است و چون دو Anna داریم، از این رو، آنها توسط تاریخ تولد به صورت نزولی مرتب شده اند.

حالا همین مرتب سازی را به صورت معکوس انجام میدهیم:

نتیجه باید به این صورت باشد:

میبینیم که همه چیز خیلی عالی کار میکند.

حال میتوانید انواع درخواستهای نامعتبر را مانند درخواستهای نیز امتحان کنید:

و نتایج را خودتان ببینید.

مورد دیگری را که نیز باید امتحان کنیم این است که آیا solution زمانیکه با صفحه بندی، فیلترینگ و جستجو ترکیب میشود کار میکند یا خیر.

آیا میتوانید حدس بزنید که نتیجه این کوئری چیست؟ اگر حدس زدید در قسمت نظرات آن را با ما در میان بگذارید.

تمام. اجازه دهید مبحثمان را خلاصه کنیم.

نتیجه گیری

همانطور که مشاهده کردید، پیش پا افتاده ترین نوع مرتب سازی مانند این، به اندازه معینی از business logic، اندکی validation ،reflection و حتی کمی ساخت کوئری به صورت داینامیک نیاز دارد. اما زمانیکه آن را پیاده سازی میکنید، API شما واقعا همه کاره و انعطاف پذیر میشود.

این پیاده سازی به همان سادگی است که انجام می شود، اما کارهای خاصی وجود دارد که می توانیم برای بهبود آن انجام دهیم. ما میتوانیم یک لایه service پیاده سازی کنیم (این لایه را برای اصل سادگی نداریم)، میتوانیم کدمان را generic کنیم، نه اینکه فقط مختص owner باشد و ما البته میتوانیم یک extension method به نام ApplySort برای IQueryable بسازیم که باعث میشود این solution انعطاف پذیری بیشتری داشته باشد.

در این مقاله، ما موارد زیر را پوشش داده ایم:

  • تعریف مرتب سازی و نحوه کارکرد آن
  • یک شیوه پیاده سازی مرتب سازی در ASP.NET Core Web API
  • تست solution مان برای کوئریهای معتبر و نامعتبر
  • تست اینکه آیا solution ما به همراه صفحه بندی، فیلترینگ و جستجو کار میکند یا خیر.

کل مبحث مرتب سازی همین بود. اگر سوال و یا نظری دارید ما در قسمت نظرات در خدمت شما هستیم.

در مقاله بعدی، ما موضوع بسیار خوبی درباره  data shaping را بیان خواهیم کرد.

نویسنده

امید عباسی
من امید عباسی هستم. سالهاست که در زمینه برنامه نویسی با تکنولوژی دات نت فعالیت میکنم و عاشق این هستم که تجربیات و دانش خودم را در این زمینه با دیگران به اشتراک بزارم. خیلی دوست دارم که نظر و انتقاد خودتون رو در مورد این نوشته برای من بنویسید تا بتونم در آینده، مطالب بهتر و ارزشمندتری را برای شما فراهم کنم. در صورت داشتن هرگونه سوال هم در قسمت دیدگاه ها میتونید با بنده در ارتباط باشید