Data Shaping در ASP.NET Core Web API
شناسه پست: 2547
بازدید: 841

در این مقاله، ما در مورد یک مفهوم مبتکرانه به نام data shaping و نحوه پیاده سازی آن در ASP.NET Core Web API صحبت میکنیم. برای این منظور، ما قصد داریم از ابزارهای مشابهی که در مقاله مرتب سازی در ASP.NET Core Web API استفاده کردیم استفاده کنیم. Data shaping چیزی نیست که در هر API مورد نیاز باشد، اما در بعضی موارد میتواند خیلی مفید باشد.

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

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

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

بحثمان را با این موضوع شروع میکنیم که data shaping دقیقا چیست.

Data Shaping چیست

Data shaping یک روش عالی برای کاهش میزان ترافیک ارسال شده از API به client است. این به استفاده کننده API، این امکان را می دهد تا داده ها را با انتخاب فیلدها از طریق query string انتخاب کند (shape کند).

منظور ما از این امر چیزی شبیه به این است:

این به API میگوید که لیستی از owner ها را فقط با فیلدهای Name و DateOfBirth از موجودیت owner برگرداند. در این سناریوی ما، موجودیت owner، فیلدهای خیلی زیادی برای انتخاب ندارد، اما میتوانید ببینید زمانیکه یک موجودیت، فیلدهای خیلی زیادی دارد، آنگاه این رویکرد میتواند خیلی مفید باشد. این غیر معمول نیست.

با در اختیار قراردادن این امکان برای استفاده کننده API که فقط بتواند فیلدهای مورد نیاز خود را انتخاب کند، میتوانیم به طور چشمگیری، فشار را بر روی API کاهش دهیم. از طرفی هم، این چیزی نیست که هر API به آن نیاز داشته باشد، بنابراین، ما باید خوب فکر کنیم و تصمیم بگیریم که اگر بخواهیم آن را پیاده سازی کنیم، پیاده سازی آن به کمی reflection نیاز دارد.

و ما به یقین می دانیم که reflection عوارض خودش را دارد و باعث کند شدن برنامه ما می شود.

در نهایت، مانند همیشه، data-shaping باید با مفاهیمی که تاکنون توضیح داده ایم به خوبی کار کند، یعنی با مفاهیم صفحه بندی، فیلترینگ، جست و جو و مرتب سازی.

حالا کارمان رو شروع میکنیم.

نحوه پیاده سازی Data Shaping در ASP.NET Core Web API

اول از همه، ما باید کلاس QueryStringParameters خود را توسعه دهیم ، زیرا ما می خواهیم یک ویژگی جدید به query string خود اضافه کنیم و می خواهیم این ویژگی برای هر موجودیتی در دسترس باشد:

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

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

ما IDataShaper.cs و DataShaper.cs را در پوشه Helpers در پروژه Entities ایجاد میکنیم.

اول از همه، اینترفیس IDataShaper را ایجاد میکنیم:

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

اینجا توجه داشته باشید که چطور از نوع ExpandoObject به عنوان نوع برگشتی استفاده میکنیم. ما باید این کار را انجام دهیم تا داده های خود را آنطور که می خواهیم shape کنیم.

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

حال بیاید این کلاس را به تکه های کوچک تقسیم کنیم.

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

ما در این کلاس، یک ویژگی public به نام Properties داریم. این یک آرایه ای از نوع PropertyInfo است که ما میخواهیم ویژگیهای موجود در ورودی مورد نظرمان (موجودیت) را گرفته و درون این آرایه قرار دهیم. حالا این ورودی هرچه که میخواهد باشد که در این سناریوی ما، Account یا Owner میباشد.

بنابراین در اینجا ، با نمونه کلاس مورد نظر، تمام خصوصیات یک کلاس ورودی را بدست می آوریم.

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

این هر دو متد، شبیه به هم هستند و به متد GetRequiredProperties  متکی هستند که این متد برای تجزیه کردن رشته ورودی که شامل فیلدهایی که میخواهیم واکشی کنیم استفاده میشود.

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

همانطور که میبینید، چیز خاصی اینجا وجود ندارد. اگر fieldsString خالی نباشد، آن را split کرده و بررسی میکنیم که آیا فیلدها با خصوصیات موجود در موجودیت ما مطابقت دارد یا خیر. هرکدام از آنها که مطابقت داشته باشد، آن را به لیست requiredProperties اضافه میکنیم.

از سوی دیگر، اگر fieldsString خالی باشد، تمام خصوصیات درون موجودیت را به عنوان خصوصیات مورد نیاز در نظر میگیریم.

حالا متدهای FetchData و FetchDataForEntity، متدهای private هستند که مقادیر را از این required properties که آن را آماده کرده ایم استخراج میکند:

متد FetchDataForEntity، این کار را برای یک موجودیت انجام میدهد:

همانطور که میبینید، ما بر روی requiredProperties حلقه میزنیم و سپس کمی از reflection استفاده میکنیم. ما مقادیر را استخراج کرده و آنها را به ExpandoObject مان اضافه میکنیم. IDictionary<string,object> ،ExpandoObject را پیاده سازی میکند. بنابراین ما میتوانیم از متد TryAdd برای اضافه کردن خصوصیت مورد نظر با استفاده از name آن به عنوان کلید و value آن به عنوان مقدار برای dictionary استفاده کنیم.

با این شیوه، به طور داینامیک، ما فقط خصوصیاتی که نیاز داریم را به آبجکت داینامیکمان اضافه میکنیم.

متد FetchData همان متد FetchDataForEntity منتها برای چند آبجکت است. این متد، متد FetchDataForEntity که پیاده سازی کردیم را به کار میبرد:

هیچ چیز پیچیده ای اینجا وجود ندارد. ما فقط بر روی لیست entities حلقه زده و یک کالکشنی از shaped entities را به عنوان نتیجه برمیگردانیم.

کل پیاده سازی همین بود. حالا ببینیم که چطور ما همه اینها را به solution موجود کانکت میکنیم.

اتصال نقطه ها

حال که logic مورد نظر را پیاده سازی کردیم، میتوانیم data shaper مان را در repository ها، همانطور که با ISortHelper انجام دادیم، inject کنیم:

و سپس data shaping را در متد GetOwners اعمال میکنیم:

متد دیگری به نام GetOwnerById ایجاد کنید که داده ها را shape کند. چون ما هنوز به متد معمولیمان برای چک کردن اعتبارسنجی ها در اکشنهای کنترلر نیاز داریم:

و فراموش نکنید که اینترفیس IOwnerRepository را برای انعکاس این تغییرات تغییر دهید:

شما میتوانید سعی کنید AccountRepository را به عنوان تمرین خودتان تغییر دهید. اگر جایی را گیر کردید میتوانید پروژه تکمیل شده را بررسی کنید.

و البته، چون ما سازنده های کلاسهای repository را تغییر داده ایم، باید RepositoryWrapper مان را نیز تغییر دهیم:

استفاده از تزریق وابستگی، به این معنی است که ما باید data shaper هایمان را در متد ConfigureRepositoryWrapper کلاس ServiceExtensions برای resolve آنها ثبت کنیم:

و از آنجایی که ما همچین کاری را عالی انجام داده ایم، نیازی نداریم که اکشن GetOwners در کنترلر OwnerController را تغییر دهیم، اما باید یک تغییر جزئی برای بررسی validation در اکشن GetOwnerById را انجام دهیم:

همانطور که می بینید تغییرات چندان چشمگیر نیستند.

حالا همه چیز setup شده است. ما باید این را اجرا کنیم و آن مطمئننا کار خواهد کرد. آیا همینطوره؟

در حقیقت ، اینگونه نخواهد بود.

حل مشکلات Json Serialization

اکنون ، اگر سعی کنیم برنامه را با این وضعیت اکنون اجرا کنیم ، یک InvalidCastException خیلی بد دریافت خواهیم کرد. دلیلش هم این است که System.Text از cast کردن ExpandoObject به IDictionary پشتیبانی نمیکند و زمانیکه نتیجه را از کنترلر برمیگردانیم این خطا چیزی است که باید اتفاق بیفتد.

برای جلوگیری از این مشکل، ما باید Microsoft.AspNetCore.Mvc.NewtonsoftJson را از NuGet package اضافه کنیم و سپس در متد ConfigureServices در کلاس Startup.cs، خط ()services.AddControllers را پیدا کنیم و ()AddNewtonsoftJson. را به انتهای آن اضافه کنیم:

کاری که این کد برای ما انجام میدهد این است که System.Text.Json serializer پیشفرض را با Newtonsoft.Json جایگزین میکند. حالا دیگر exception  باید رفع شده باشد و برنامه به درستی کار کند.

از آنجایی که ما در این مرحله از کار هستیم، قصد داریم برنامه را طوری پیکربندی کنیم که بر روی browser header ها حساس باشد و اگر یک media type ناشناخته مورد درخواست قرار بگیرد، 406 Not Acceptable به عنوان پاسخ برگشت داده شود و از یک XML serializer به عنوان قرارداد برای داده ها استفاده کند زیرا ما میخواهیم از نقل و انتقال محتوا پشتیبانی کنیم.

این، مشکل را با json serialization حل میکند.

اما بیایید solution خود را تست کنیم تا ببینیم آیا کاری را که انجام داده ایم آیا واقعا کار میکند یا خیر.

تست Solution

قبل از اینکه solution را تست کنیم، یک نگاه سریع به جدول Owner بندازیم و ببینیم که چطور به نظر میرسد:

owner های دیتابیس

عالیه. این ورودی ها باید از عهده تست ما بربیایند.

ابتدا اجازه دهید یک درخواست ساده GET به owners endpoint خود ارسال کنیم:

در پاسخ باید یک full response را دریافت کنیم:

بنابراین عملکرد data shaping ما، اختلالی در رفتار پیشفرض برنامه ایجاد نکرده است.

حالا data shaping را در عمل بینیم:

API باید اینجا، داده های shape شده را برگرداند:

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

این کوئری، فقط باید یک نتیجه را برگرداند. آیا میتوانید حدس بزنید که کدام یک از owner ها را به عنوان نتیجه برمیگرداند؟ اگر حدس زدید نتیجه درست چیست آن را در قسمت نظرات برای ما بنویسید.

تمام. ما API مان را با موفقیت تست کردیم.

یک چیز دیگر را نیز تست کنیم. بیایید سعی کنیم یک نتیجه XML را درخواست کنیم.

حل مشکلات XML Serialization

Accept header را به application/xml تغییر میدهیم و یک درخواست را میفرستیم. میخواهیم خروجی را تست کنیم و ببینیم که آیا نقل و انتقال محتوا کار میکند یا خیر.

اینبار قصد داریم یک درخواست ساده بفرستیم:

و response دریافتی به این صورت میباشد:

همانطور که میبینید این response بسیار بد و غیرقابل خوانا میباشد. اما این نشان میدهد که XmlDataContractSerializerOutputFormatter چطور ExpandoObject ما را به صورت پیشفرض serialize میکند.

بنابراین آیا می خواهیم این مسئله را برطرف کنیم و اگر اینطور است چطور باید آن را انجام دهیم؟

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

یک توضیح ساده این است که ما باید آبجکت dynamic خود را ایجاد کنیم و قوانین XML serialization را برای آن تعریف کنیم.

بنابراین ما باید چیزی همانند این را ایجاد کنیم:

مهمترین نکته ای که اینجا باید به آن توجه کنید این است که ما از DynamicObject ارث بری کرده ایم که آبجکت ما را از نوع dynamic ایجاد میکند. همچنین از اینترفیس IXmlSerializable ارث بری کرده ایم که ما باید قوانین serialization سفارشی را پیاده سازی کنیم و همچنین به دلیل متد Add که برای XML serialization مورد نیاز است از IDictionary<string, object> ارث بری کرده ایم.

تنها چیزی که باقیمانده است این است که نوع ExpandoObject را با نوع Entity در کل پروژه جایگزین کنیم.

حالا باید یک response به مانند زیر دریافت کنیم:

حالا این خیلی بهتر است. آیا اینطور نیست؟

اگر XML serialization برای شما مهم نیست، میتوانید از همان ExpandoObject استفاده کنید. اما اگر یک XML response را بخواهید که به خوبی فرمت بندی شده باشد، این راه حلش میباشد.

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

نتیجه گیری

Data shaping یک ویژگی کوچک جذاب و شگفت انگیز است که واقعاً می تواند API های ما را انعطاف پذیر کرده و از ترافیک شبکه ما بکاهد. اگر ما دارای یک API با ترافیک حجم بالا هستیم ، Data shaping باید به خوبی برایمان کار کند. از طرف دیگر، این ویژگی نیست که ما به راحتی از آن استفاده کنیم، زیرا آن برای انجام کارها از reflection و dynamic type استفاده می کند.

مانند سایر ویژگیهای دیگر، ما باید حواسمان باشد که چه زمانی و اینکه آیا اصلا باید Data shaping را پیاده سازی کنیم یا خیر. حتی اگر آن را پیاده سازی کنیم هم تست های عملکرد ممکن است مفید باشد.

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

  • data shaping چیست
  • نحوه پیاده سازی یک data shaping solution به صورت generic در ASP.NET Core Web API به چه صورت است
  • نحوه حل مشکلات json serialization با ExpandoObject چگونه است
  • تست solution با ارسال تعدادی درخواست ساده
  • قالب بندی XML response هایمان

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

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

نویسنده

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