namespace OpenAPISwaggerDoc.Web.Controllers { [Route("api/[controller]")] [ApiController] public class ConventionTestsController : ControllerBase { // GET: api/ConventionTests/5 [HttpGet("{id}", Name = "Get")] [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))] public string Get(int id) { return "value"; }
using Microsoft.AspNetCore.Mvc.ApiExplorer; namespace Microsoft.AspNetCore.Mvc { public static class DefaultApiConventions { [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)] [ProducesDefaultResponseType] [ProducesResponseType(200)] [ProducesResponseType(404)] public static void Get( [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)] [ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)] object id); } }
امکان اعمال DefaultApiConventions به تمام متدهای یک کنترلر API نیز به صورت زیر با استفاده از ویژگی ApiConventionType اعمال شدهی به کلاس کنترلر میسر است:
namespace OpenAPISwaggerDoc.Web.Controllers { [Route("api/[controller]")] [ApiController] [ApiConventionType(typeof(DefaultApiConventions))] public class ConventionTestsController : ControllerBase
[assembly: ApiConventionType(typeof(DefaultApiConventions))] namespace OpenAPISwaggerDoc.Web { public class Startup
ایجاد ApiConventions سفارشی
همانطور که عنوان شد، اگر متدهای API شما دقیقا همان نامهای پیشفرض Get/Post/Put/Delete را داشته باشند، توسط DefaultApiConventions مدیریت خواهند شد. در سایر حالات، مثلا اگر بجای نام Post، از نام Insert استفاده شد، باید ApiConventions سفارشی را ایجاد کرد:
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; namespace OpenAPISwaggerDoc.Web.AppConventions { public static class CustomConventions { [ProducesDefaultResponseType] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)] public static void Insert( [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)] [ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)] object model) { } } }
پس از آن برای اعمال این ApiConventions جدید میتوان به صورت زیر عمل کرد:
namespace OpenAPISwaggerDoc.Web.Controllers { [Route("api/[controller]")] [ApiController] public class ConventionTestsController : ControllerBase { [HttpPost] [ApiConventionMethod(typeof(CustomConventions), nameof(CustomConventions.Insert))] public void Insert([FromBody] string value) { }
سؤال: آیا استفادهی از این ApiConventions ایدهی خوبی است؟
همانطور که در ابتدای بحث نیز عنوان شد، اگر فیلترهای سراسری را مانند قسمت قبل فعال کرده باشیم، از اعمال ApiConventions صرفنظر میشود. همچنین حالت پیشفرض آنها برای حالتهای متداول و ساده مفید هستند و برای سایر حالات باید کدهای زیادی را نوشت. به همین جهت خود مایکروسافت هم استفادهی از ApiConventions را صرفا برای کنترلرهای API ای که دقیقا مطابق با قالب پیشفرض آنها تهیه شدهاند، توصیه میکند. بنابراین استفادهی از Attributes که در قسمت قبل آنها را بررسی کردیم، مقدم هستند بر استفادهی از ApiConventions و تعدادی از بهترین تجربههای کاربری در این زمینه به شرح زیر میباشند:
- از API Analyzers که در قسمت قبل معرفی شد، برای یافتن کمبودهای نقایص مستندات استفاده کنید.
- از ویژگی ProducesDefaultResponseType استفاده کنید؛ اما تا جائیکه میتوانید، جزئیات ممکن را به صورت صریحی مستند نمائید.
- Attributes را به صورت سراسری معرفی کنید.
بهبود مستندات Content negotiation
فرض کنید میخواهید لیست کتابهای یک نویسنده را دریافت کنید. در اینجا خروجی ارائه شده، با فرمت JSON تولید میشود؛ اما ممکن است XML ای نیز باشد و یا حالتهای دیگر، بسته به تنظیمات برنامه. کار Content negotiation این است که مصرف کنندهی یک API، دقیقا مشخص کند، چه نوع فرمت خروجی را مدنظر دارد. هدری که برای این منظور استفاده میشود، accept header نامدارد و ذکر آن اجباری است؛ هر چند تعدادی از APIها بدون وجود آن نیز سعی میکنند حالت پیشفرضی را ارائه دهند.
Swagger-UI به نحوی که در تصویر فوق ملاحظه میکنید، امکان انتخاب Accept header را مسیر میکند. در این حالت اگر application/json را انتخاب کنیم، خروجی JSON ای را دریافت میکنیم. اما اگر text/plain را انتخاب کنیم، چون توسط API ما پشتیبانی نمیشود، خروجی از نوع 406 یا همان Status406NotAcceptable را دریافت خواهیم کرد. بنابراین وجود گزینهی text/plain در اینجا غیرضروری و گمراه کنندهاست و نیاز است این مشکل را برطرف کرد:
namespace OpenAPISwaggerDoc.Web.Controllers { [Produces("application/json")] [Route("api/authors/{authorId}/books")] [ApiController] public class BooksController : ControllerBase
پس از اعمال این ویژگی، تاثیر آنرا بر روی Swagger-UI در شکل زیر مشاهده میکنید که اینبار تنها به یک مورد مشخص، محدود شدهاست:
در اینجا اگر قصد داشته باشیم خروجی XML را نیز پشتیبانی کنیم، میتوان به صورت زیر عمل کرد:
- ابتدا در کلاس Startup، نیاز است OutputFormatter متناظری را به سیستم معرفی نمود:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(setupAction => { setupAction.OutputFormatters.Add(new XmlSerializerOutputFormatter());
namespace OpenAPISwaggerDoc.Web.Controllers { [Produces("application/json", "application/xml")] [Route("api/authors/{authorId}/books")] [ApiController] public class BooksController : ControllerBase
در این حالت اگر Controls Accept header را در UI از نوع xml انتخاب کنیم و سپس با کلیک بر روی دکمهی try it out و ذکر id یک نویسنده، لیست کتابهای او را درخواست کنیم، خروجی نهایی XML ای آن قابل مشاهده خواهد بود:
البته تا اینجا فقط Swagger-UI را جهت محدود کردن به دو نوع خروجی با فرمت JSON و XML، اصلاح کردهایم؛ اما این مورد به معنای محدود کردن سایر ابزارهای آزمایش یک API مانند postman نیست. در این نوع موارد، تمام مدیاتایپهای ارسالی پشتیبانی نشده، سبب تولید خروجی با فرمت JSON میشوند. برای محدود کردن آنها به خروجی از نوع 406 میتوان تنظیم ReturnHttpNotAcceptable را به true انجام داد تا اگر برای مثال درخواست application/xyz ارسال شد، صرفا یک استثناء بازگشت داده شود و نه خروجی JSON:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(setupAction => { setupAction.ReturnHttpNotAcceptable = true; // Status406NotAcceptable
بهبود مستندات نوع بدنهی درخواست
تا اینجا فرمت accept header را دقیقا مشخص و مستند کردیم؛ اما اگر به تصویر فوق دقت کنید، در حین ارسال اطلاعاتی از نوع POST به سرور، چندین نوع Request body را میتوان انتخاب کرد که الزاما تمام آنها توسط API ما پشتیبانی نمیشود. برای رفع این مشکل میتوان از ویژگی Consumes استفاده کرد که نوع مدیتاتایپهای مجاز ورودی را مشخص میکند:
namespace OpenAPISwaggerDoc.Web.Controllers { [Produces("application/json", "application/xml")] [Route("api/authors/{authorId}/books")] [ApiController] public class BooksController : ControllerBase { /// <summary> /// Create a book for a specific author /// </summary> /// <param name="authorId">The id of the book author</param> /// <param name="bookForCreation">The book to create</param> /// <returns>An ActionResult of type Book</returns> [HttpPost()] [Consumes("application/json")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult<Book>> CreateBook( Guid authorId, [FromBody] BookForCreation bookForCreation) {
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-05.zip