پیاده سازی HATEOAS در ASP.NET Core Web API
شناسه پست: 2569
بازدید: 1619

در این مقاله، ما میخواهیم در مورد یکی از مهمترین مفاهیم در ساخت RESTful API ها صحبت کنیم – HATEOAS  و یاد بگیریم که چطور HATEOAS را در ASP.NET Core Web API پیاده سازی کنیم. این مقاله، به مفاهیمی که تا به الان در صفحه بندی، فیلترینگ، جست و جو، مرتب سازی و بخصوص data-shaping پیاده سازی کرده ایم، وابسته بوده و بر پایه و اساسی که در این مقالات قرار داده ایم بنا می شود.

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

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

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

در این مقاله، ما میخواهیم یاد بگیریم که:

حالا بریم تا ببینیم این HATEOAS  چیست.

HATEOAS چیست و چرا انقدر مهم است؟

HATEOAS (Hypermedia as the Engine of Application State) یک معیار بسیار مهم برای REST است. بدون آن ، REST API را نمی توان RESTful دانست و بسیاری از مزایایی که با پیاده سازی یک معماری REST به دست می آوریم در دسترس نخواهند بود.

Hypermedia به هر نوع از محتوایی که شامل پیوندها به انواع رسانه ها مانند سندها، تصاویر، ویدئوها و … باشد گفته میشود.

معماری REST به ما این امکان را می دهد که پیوندهای Hypermedia را در response های خود به صورت پویا ایجاد کنیم و بنابراین navigation را برای ما بسیار آسان تر میسازد.

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

وب سایتی را تصور کنید که دارای یک صفحه اصلی است و شما در آن قرار دارید، اما هیچ پیوندی در هیچ جای آن وجود ندارد. شما باید وب سایت را بالا و پایین کنید یا روش دیگری را برای پیمایش در آن پیدا کنید تا به محتوای مورد نظر خودتان برسید. ما نمیگوییم که وبسایت مانند یک REST API است، اما اصل موضوع همین است.

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

حال ببینیم چطور آن واقعا کار میکند.

نمونه Response با HATEOAS پیاده سازی شده

بیاید بگوییم که میخواهیم تعدادی Owner را از API مان بگیریم.

اما چطور باید این کار را انجام دهیم؟

ما حتی endpoint مربوط به owner ها را نمیدانیم. خب، اول به سراغ چیزی که نحوه request آن را میدانیم میرویم و آن root برنامه میباشد:

root endpoint ما، باید جزییات لازم را در مورد API ما یا بهتر بگویم اینکه از چه جایی باید شروع به کاوش کنیم را به ما بگوید:

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

و در واقع ما owner ها را دریافت میکنیم:

همانطور که میبینید، ما لیستی از owner ها و تمام اکشنهایی که میتوانیم بر روی هریک از آنها انجام دهیم و غیره را اینجا دریافت کردیم.

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

یک Link چیست؟

بر طبق RFC5988، یک link، “یک اتصال بین دو منبع است که توسط  Internationalised Resource Identifiers (IRIs) شناسایی می شود”. به زبان ساده ما از لینکها برای پیمایش اینترنت یا بهتر بگوییم برای پیمایش منابع موجود در اینترنت استفاده می کنیم.

 response های ما شامل آرایه ای از لینکها است که این لینکها شامل تعدادی خصوصیت بر طبق RFC است.

  • href – بیانگر یک target URI است.
  • rel – بیانگر نوع رابطه لینک مورد نظر است، به این معنی که نحوه رابطه context کنونی را با target resource مورد نظر توصیف میکند.
  • method – ما یک متد HTTP نیاز داریم تا بدانیم که چگونه target URI های یکسان را از هم تشخیص دهیم.

معایب/مزایای پیاده سازی HATEOAS

بنابراین تمام مزایایی که میتوانیم از پیاده سازی HATEOAS بدست آوریم چه چیزهایی هستند؟

پیاده سازی HATEOAS ساده نیست ، اما منفعتهایی که از پیاده سازی آن عاید ما میشود ارزش آن را دارد.. تعدادی از مواردی که از پیاده سازی HATEOAS میتوانیم انتظار داشته باشیم بدست آوریم عبارتند از:

  • API مورد نظر تبدیل به یک API خودکاشف و قابل کاوش میشود.
  • یک client میتواند از لینکها برای پیاده سازی logic خود استفاده کند. این باعث میشود کار راحت تر شود و هر تغییری که در ساختار API اتفاق میفتد به طور مستقیم به داخل client منعکس میشود.
  • سرور، کنترل state برنامه و ساختار URL را در دست میگیرد و نه برعکس.
  • از relation های لینک، می توان برای نشان دادن مستندات توسعه دهنده استفاده کرد.
  • ورژن بندی از طریق hyperlink ها، آسانتر میشود.
  • فراخوانیهای تراکنش وضعیت نامعتبر کاهش میابد.
  • API بدون تفکیک client ها، تکامل یافته میشود.

ما میتوانیم خیلی کارها با HATEOAS انجام دهیم. اما پیاده سازی تمام این feature ها، راحت نیست. ما باید اسکوپ API خود را به خاطر بسپاریم و اگر واقعاً به همه اینها احتیاج داریم، تفاوت زیادی بین یک API عمومی با حجم درخواست بالا و برخی از API های داخلی که برای برقراری ارتباط بین قسمت های همان سیستم مورد نیاز است وجود دارد.

مانند هرچیز دیگری، context همه چیز است. آن را ساده نگه دارید.

در حال حاضر این تئوری، بیش از حد کافی است. اجازه دهید تا وارد عمل شویم و ببینیم تا پیاده سازی واقعی HATEOAS چطور به نظر میرسد.

توسعه Model در آماده سازی برای پیاده سازی HATEOAS

با concept ای که تا الان یاد گرفتیم کارمان را شروع میکنیم و آن Link است. اول از همه، ما موجودیت Link را در پوشه Models پروژه Entities ایجاد میکنیم:

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

در مرحله بعد، ما باید یک کلاس ایجاد کنیم که شامل تمام link های ما خواهد بود – LinkResourceBase:

و در آخر، چون response ما باید root کنترلر را توصیف کند، به یک  wrapper برای link هایمان نیاز داریم:

این کلاس ممکن است در حال حاضر زیاد منطقی نباشد ، اما با ما همراه باشید و بعداً در این مسیر برایتان شفاف خواهد شد. در حال حاضر، فرض میکنیم ما link های خود را در کلاس دیگری برای اهداف ارائه response  قرار داده ایم.

از آنجا که response ما حاوی link ها نیز خواهد بود ، باید قوانین XML serialization را گسترش دهیم، طوریکه XML response ما، link های قالب بندی شده صحیح را برگردانند. بدون آن، ما چیزی مانند این را دریافت خواهیم کرد:

بنابرایم ما باید متد WriteXmlElement برای پشتیبانی از link ها توسعه دهیم:

همانند کاری که در مقاله data shaping انجام داده بودیم، ما اینجا زیاد وارد جزییات نمیشویم، چرا که این خارج از محدوده مقاله میباشد، اما logic آن نیز زیاد پیچیده نیست. به طور خلاصه، ما بررسی کردیم که آیا type از نوع List<Link> است یا خیر. که اگر اینطور است سپس بر روی کل link ها حلقه میزنیم و متد را به صورت بازگشتی برای هریک از خصوصیات: href, method, و rel فراخوانی میکنیم.

این تمام چیزی است که در حال حاضر نیاز داریم. ما اکنون یک پایه محکم برای پیاده سازی  HATEOAS در کنترلمان داریم.

پیاده سازی HATEOAS در ASP.NET Core Web API

حالا سراغ OwnerController میرویم و در واقع HATEOAS را پیاده سازی میکنیم.

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

سپس باید متد GetOwners مان را برای افزودن link های مرتبط با owner های دریافت شده از دیتابیس توسعه دهیم:

ما اینجا، یک حلقه بر روی تمام owner هایی که از دیتابیس دریافت شده زده ایم و لینکهای مربوطه را به آنها اضافه کرده ایم. بعد از آن، ما owners collection را wrap میکنیم و link هایی که برای کل collection مهم هستند را ایجاد میکنیم.

حالا فقط باید متدهای CreateLinksForOwner و CreateLinksForOwners را پیاده سازی کنیم:

ذکر چند نکته اینجا حائز اهمیت است.

ما باید هنگام ایجاد link ها، fields را در نظر بگیریم زیرا ممکن است از آنها در request های خود استفاده کنیم. ما link ها را با استفاده از متد LinkGenerator.GetUriByAction ایجاد میکنیم که HttpContext، نام action و values که باید برای ایجاد URL معتبر مورد استفاده قرار بگیرد را به عنوان ورودی میگیرد. در مورد OwnersController، ما owner id و fields را به عنوان آرگومان ارسال میکنیم.

response که ما اینجا انتظار داریم دقیقا همان response است که ما به عنوان مثال استفاده کرده بودیم.

ما کاری مشابه برای متد GetOwnerById مان انجام میدهیم:

همانطور که میبینید  logic اینجا برای یک owner خیلی راحت تر است، زیرا نیازی به wrap کردن آن نداریم.

ما می خواهیم همین کار را برای AccountController انجام دهیم، اما با کمی تفاوت. همانطور که میدانید endpoint مربوط به AccountController کمی متفاوت است:

باید زمانیکه لینکهای HATEOAS را میسازیم، این را در نظر بگیریم:

پیاده سازی AccountController ما، شامل متدهای CreateUpdate یا Delete نیست، بنابراین پیاده سازی آن راحت تر است. اما همانطور که میتوانید ببینید، علاوه بر account id، باید owner id را نیز اینجا در نظر بگیریم. به منظور تولید صحیح link ها، ما باید fields را نیز در این حالت در نظر بگیریم.

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

تست Solution

با یک query ساده به owners endpoint شروع میکنیم:

این باید لیستی از owner ها را به همراه link های اضافه شده به آنها برگرداند:

همانطور که میبینید، تمام link هایی که برای owner مان تعریف کرده بودیم اینجا وجود دارد. کل کالکشن owner ها، در موجودیت “wrap ،“value شده است، زیرا درست در انتها، ما یک links که “self” یا بهتر بگوییم نحوه رسیدن به کنترلر را توصیف میکند را تعریف میکنیم. ما میتوانیم آن را هرزمان که بخواهیم با دیگر link های مرتبط که برای کنترلر مهم هستند را توسعه دهیم.

این بود کل ماجرای wrapper که پیاده سازی کردیم.

حال تستمان را با یک owner انجام میدهیم:

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

فقط یک owner، بدون هیچگونه wrapper.

حالا سعی میکنیم تا account های مربوط به همین owner را بگیریم:

ما لیستی از account های این owner و link های مربوط به آنها را دریافت میکنیم:

و در آخر، یک تک account را تست میکنیم:

نتیجه باید در این مرحله کاملاً واضح باشد:

یک link، نحوه گرفتن آن account را توصیف می کند. همانطور که مشاهده می کنید، دو id در این link وجود دارد ، بنابراین کار بزرگی را در این پیاده سازی انجام داده ایم.

حالا تنها چیزی که برای تست کردن باقی میماند این است که چطور آن کار میکند زمانیکه فیلدهایی که نیاز داریم را انتخاب میکنیم:

و با کمال تعجب، این کار نمی کند.

چرا؟

بهبود Solution

خب، همانطور که مشاهده کردید، پیاده سازی HATEOAS، به داشتن id های موجود برای ساخت link ها برای response متکی است. از طرفی دیگر،  Data shaping، این امکان را به ما میدهد که فقط field هایی که نیاز داریم را برگردانیم و در سناریوی تستمان، ما فقط نام owner ها را میخواستیم. این بدان معناست که ما id این owner ها را برنمیگرداندیم و ما در حال حاضر نمیتوانیم URI های target را بسازیم.

برای حل این مشکل، ما میخواهیم مطمئن شویم که id همیشه به کنترلر برگردانده میشود.

اول از همه، ما یک wrapper به نام ShapedEntity که علاوه بر entity، شامل ویژگی id نیز است پیاده سازی میکنیم:

سپس، تمام جاهایی که از کلاس Entity در کل پروژه استفاده کرده ایم را با ShapedEntity جایگزین میکنیم. در کلاسهایی که شما باید کلاسهای Entity استفاده شده در آن را جایگزین کنید عبارتند از: AccountRepositoryIAccountRepositoryOwnerRepositoryIOwnerRepositoryDataShaper, و IDataShaper.

علاوه بر این، باید متد FetchDataForEntity در کلاس DataShaper را برای گرفتن id به طور جداگانه، توسعه دهیم:

در حال حاضر، مهم نیست چه خصوصیاتی را با استفاده از data shaping دریافت میکنیم، بلکه تمام چیزهایی که باید با آن link ها را بسازیم را در اختیار داریم.

تنها کاری که باقی مانده است این است که اکشنهای کنترلرمان را کمی تغییر دهیم:

کلاس AccountController نیز همین تغییرات را شامل میشود.

حالا این query را مجددا امتحان میکنیم:

و در پاسخ، این را دریافت میکنیم:

عالی! این دقیقا همان چیزی است که ما میخواستیم.

معرفی Media Type های سفارشی

اکنون، برگرداندن link های HATEOAS، یک امکان اضافی خوب برای API ما به حساب می آید. اما همانطور که می بینید response ها می توانند اینجا کمی طولانی باشند و ما به عمد تعدادی از آنها را کوتاه کردیم تا مقاله قابلیت خوانایی بیشتری داشته باشد.

ما می خواهیم به یک کاربر API توانایی تعیین نوع response را نیز بدهیم.

و چطور میتوانیم این کار را انجام دهیم؟

پاسخ به این سوال، بهره گیری از قدرت media type های سفارشی است.

ما می خواهیم media type های خودمان را ایجاد کنیم تا زمانیکه می خواهیم یک response را که شامل link های HATEOAS است برگردانیم را از آنها استفاده کنیم. اما میخواهیم زمانیکه از آن استفاده نکردیم، response هیچ link را شامل نشود و response خیلی ساده برگدانده شود مانند آنچه که قبلا آن را return میکردیم.

قبل از اینکه شروع کنیم، ببینیم چطور میتوانیم یک media type سفارشی ایجاد کنیم. یک media type سفارشی، باید چیزی به مانند این به نظر برسد:

application/vnd.expertmarket.hateoas+json. برای مقایسه آن با json media type معمولی که به طور پیش فرض از آن استفاده می کنیم:

application/json.

بنابراین اجازه دهید قسمت های مختلف یک  media type سفارشی را تجزیه و تحلیل کنیم:

  • vnd – پیشوند vendor که همیشه وجود دارد.
  • expertmarket – شناسه vendor که ما اینجا expertmarket را انتخاب کرده ایم، چرا که نه.
  • hateoas – نام  media type
  • json – پسوندی که می توانیم از آن به عنوان توصیف استفاده کنیم که مثلاً می خواهیم پاسخ json باشد یا XML.

حالا آن را در اپلیکیشنمان پیاده سازی میکنیم.

ثبت Media Type های سفارشی

اول از همه، ما میخواهیم media type های سفارشی را در middleware ثبت کنیم، در غیر اینصورت، خطای 406 Not Acceptable میگیریم.

اجازه دهید یک متد extension جدید به  ServiceExtensions مان اضافه کنیم:

ما اینجا دو media type سفارشی ثبت کردیم:application/vnd.expertmarket.hateoas+json برای newtonSoftJsonSerializerOutputFormatter و application/vnd.expertmarket.hateoas+xml برای xmlDataContractSerializerOutputFormatter. این باعث میشود که دیگر یک 406 Not Acceptable response دریافت نکنیم.

آن را به Startup.cs در متد ConfigureServices بعد از متد AddControllers اضافه کنید:

این متد، مسئولیت ثبت media type های سفارشی را بر عهده دارد.

پیاده سازی یک Media Type Validation Filter

اکنون، از آنجا که media type های سفارشی را پیاده سازی کرده ایم، میخواهیم Accept header مان در request هایمان وجود داشته باشد تا بتوانیم response پربار HATEOAS را تشخیص دهیم زمانیکه کاربر آن را درخواست کرد.

برای این منظور، ما یک ActionFilter پیاده سازی میکنیم که Accept header و media type های ما را validate خواهد کرد:

ما اینجا اول موجودیت Accept header را بررسی میکنیم. اگر موجود نبود، یک BadRequest برمیگردانیم. اما اگر وجود داشت، media type را parse میکنیم و اگر media type معتبر وجود نداشته باشد نیز یک BadRequest برمیگردانیم.

زمانیکه تستهای validation را با موفقیت پشت سرگذاشتیم، سپس parse media type شده را به HttpContext کنترلر اضافه میکنیم.

فراموش نکنید که این filter را در IoC ثبت کنید:

حالا باید اکشنهای GetOwners و GetOwnerById را با [ServiceFilter(typeof(ValidateMediaTypeAttribute))] برای این validation ها مزین کنیم.

در مورد logic این اکشنها نیز باید آنها را کمی توسعه دهیم:

ما media type که در parse ،ValidateMediaTypeAttribute ActionFilter کردیم را اینجا خواندیم و آن را به نوع MediaTypeHeaderValue تبدیل کردیم. با استفاده از SubTypeWithoutSuffix.EndsWith در کلاس MediaTypeHeaderValue بررسی کرده ایم که آیا HATEOAS مورد درخواست قرار گرفته است یا خیر. اگر اینطور نبود، بلافاصله owner ها را برمیگردانیم. اما اگر اینطور بود، link ها را اضافه میکنیم و آنها را همانند قبل return میکنیم.

همین داستان برای اکشن GetOwnerById نیز صدق میکند:

حالا ما اینها را در خروجی تست میکنیم. یک request ساده برای گرفتن owner ها را امتحان میکنیم و Accept request header آن را به application/json ست میکنیم:

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

و حالا در همین request، میتوانیم Accept header را به application/vnd.codemaze.hateoas+json تغییر دهیم و باید این response را دریافت کنیم:

خب، حالا همه چیز خارق العادست. اکنون ما ابزاری برای انتخاب بین response های از نوع json ، XML و HATEOAS را در اختیار داریم.

حال خلاصه میکنیم.

 نتیجه گیری

HATEOAS در واقع یکی از مفیدترین و در عین حال یکی از پیچیده ترین مفاهیم REST برای پیاده سازی است. اینکه شما چگونه HATEOAS را در API خود پیاده سازی کنید و میزان سرمایه گذاری در صیقل دادن آن به خود شما بستگی دارد. HATEOAS به آسانی یک موردی است که یک API عالی را از نوع خوب یا بد جدا میکند.

حال مواردی که در این مقاله یاد گرفتیم این است که:

  • HATEOAS چیست و به چه اندازه در دنیای RESTful اهمیت دارد
  • نحوه پیاده سازی HATEOAS در پروژه ASP.NET Core WebAPI به چه صورت است
  • نحوه بهبود solution جهت اینکه بتواند با data shaping خوب کار کند چگونه است
  • نحوه پشتیبانی از XML serialization آبجکتهای داینامیک با link های HATEOAS به چه صورت است
  • media type های سفارشی چیست و چطور با استفاده از آنها میتوانیم HATEOAS را به عنوان یک گزینه اختیاری در response هایمان بگنجانیم

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

نویسنده

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