مذاکره محتوا، فرآیند انتخاب بهترین منبع برای یک response در صورت در دسترس بودن چندین منبع میباشد. مذاکره محتوا یک ویژگی HTTP است که از قبل وجود داشته است، اما به هر دلیلی، شاید کمی کمتر از آن استفاده شده باشد.
به طور خلاصه، مذاکره محتوا به شما این امکان را میدهد که محتوایی را که میخواهید در پاسخ به درخواست REST API دریافت کنید را انتخاب کنید یا بهتره بگوییم «مذاکره کنید».
میتوانید سورس کد را از اینجا دانلود کنید.
چطور مذاکره محتوا کار میکند؟
مذاکره محتوا زمانی اتفاق میافتد که یک کلاینت، نوع رسانهای که میخواهد را به عنوان response در Accept
header درخواست تعیین کند. به طور پیشفرض، ASP.NET Core Web API یک نتیجه فرمتشده JSON را برمیگرداند و هدر Accept مرورگر را نادیده میگیرد.
ASP.NET Core به طور پیش فرض از انواع رسانه های زیر پشتیبانی می کند:
application/json
text/json
text/plain
برای امتحان کردن این، اجازه دهید یک پروژه Web API پیشفرض و یک مدل ساده برای BlogPost ایجاد کنیم:
1 2 3 4 5 6 |
public class BlogPost { public string Title { get; set; } public string MetaDescription { get; set; } public bool Published { get; set; } } |
و یک کلاس به نام Blog که تمام پست های blog را لیست می کند:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Blog { public string Name { get; set; } public string Description { get; set; } public List<BlogPost> BlogPosts { get; set; } public Blog() { BlogPosts = new List<BlogPost>(); } } |
ما قصد داریم یک کنترلر به نام BlogController تنها با یک متد ()Get ایجاد کنیم که یک blog post و یک blog را دربر دارد و آنها را به عنوان نتیجه برمی گرداند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
[Route("api/blog")] public class BlogController : Controller { public IActionResult Get() { var blogs = new List<Blog>(); var blogPosts = new List<BlogPost>(); blogPosts.Add(new BlogPost { Title = "Content Negotiation in Web API", MetaDescription = "Content negotiation is the process of selecting the best resource for a response when multiple resource representations are available.", Published = true }); blogs.Add(new Blog() { Name = "Expert Market", Description = "C#, .NET and Web Development Tutorials", BlogPosts = blogPosts }); return Ok(blogs); } } |
مذاکره محتوا توسط ObjectResult و متد ()Ok که از OkObjectResult ارث بری کرده و خود OkObjectResult نیز از ObjectResult ارث بری میکند پیاده سازی میشود. این بدان معناست که متد controller ما میتواند response محتوای مذاکره شده را برگرداند.
اگرچه منطق ایجاد آبجکت در کنترلر است. شما نباید کنترلرهای خود را اینگونه پیاده سازی کنید. این فقط برای سادگی است. برای کسب اطلاعات بیشتر از بهترین شیوهها، میتوانید مقاله ما را در مورد بهترین شیوههای پیاده سازی REST API بخوانید.
ما نتیجه را با Ok()
helper method برمی گردانیم که همیشه آبجکت و کد وضعیت 200 OK را برمی گرداند.
برگرداندن JSON Response پیشفرض
اگر هم اکنون برنامه خود را اجرا کنیم، هنگام اجرا در مرورگر به صورت پیش فرض پاسخ JSON دریافت خواهیم کرد:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[ { "name": "Expert Market", "description": "C#, .NET and Web Development Tutorials", "blogPosts": [ { "title": "Content Negotiation in Web API", "metaDescription": "Content negotiation is the process of selecting the best resource for a response when multiple resource representations are available.", "published": true } ] } ] |
به منظور تست صحیح response ها، از Postman استفاده می کنیم:
به وضوح میبینید که نتیجه پیشفرض هنگام فراخوانی GET در /api/blog، نتیجه JSON ما را برمیگرداند. آن دسته از کسانی که چشمان تیزبینی دارند حتی ممکن است متوجه شده باشند که ما از هدر Accept با application/xml استفاده کردیم تا سعی کنیم سرور را مجبور کنیم که انواع رسانه های دیگر مانند متن ساده و XML را برگرداند.
اما این کار نمی کند. چرا؟
زیرا ما باید formatter های سرور را پیکربندی کنیم تا response را آنطور که میخواهیم فرمت کند.
بیایید ببینیم چگونه باید این کار را انجام دهیم.
برگرداندن XML Response
یک سرور به صراحت مشخص نمی کند که در کجا response را به JSON قالب بندی می کند. اما میتوانیم با تغییر گزینههای پیکربندی از طریق گزینههای متد ()AddControllers آن را override کنیم. بهطور پیشفرض، میتوان آن را در کلاس Program یافت و به شکل زیر است:
1 |
builder.Services.AddControllers(); |
ما میتوانیم گزینههای زیر را برای فعال کردن سرور برای قالببندی XML response هنگامی که کلاینت سعی میکند برای آن مذاکره کند، اضافه کنیم:
1 2 3 4 |
builder.Services.AddControllers(options => { options.RespectBrowserAcceptHeader = true; }).AddXmlSerializerFormatters(); |
اول از همه، ما باید به یک سرور بگوییم که برای هدر Accept احترام قائل شود. پس از آن، فقط متد ()AddXmlSerializerFormatters را برای پشتیبانی از فرمتکنندههای XML اضافه میکنیم.
اکنون که سرور خود را پیکربندی کرده ایم، اجازه دهید یک بار دیگر مذاکره محتوا را آزمایش کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<ArrayOfBlog xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ContentNegotiation.Models"> <Blog> <BlogPosts> <BlogPost> <MetaDescription>Content negotiation is the process of selecting the best resource for a response when multiple resource representations are available.</MetaDescription> <Published>true</Published> <Title>Content Negotiation in Web API</Title> </BlogPost> </BlogPosts> <Description>C#, .NET and Web Development Tutorials</Description> <Name>Expert Market</Name> </Blog> </ArrayOfBlog> |
بیایید ببینیم اگر همان درخواست را از طریق Postman ارسال کنیم، اکنون چه اتفاقی می افتد:
میبینید که پاسخ XML ما اینجا وجود دارد، این پاسخ، دیگر یک پاسخ پیش فرض نیست. با تغییر هدر Accept میتوانیم response هایی با قالببندی متفاوت دریافت کنیم.
اما اگر با وجود این همه انعطاف، یک کلاینت، نوع رسانه ای را درخواست کند که سرور نداند چگونه آن را قالب بندی کند، تکلیف چه میشود؟
محدود کردن نوع رسانه ها در مذاکره محتوا
در حال حاضر، اگر نوع رسانه شناسایی نشود، پاسخ به طور پیشفرض JSON خواهد بود.
اما می توانیم این رفتار را با افزودن یک خط به configuration محدود کنیم:
1 2 3 4 5 |
builder.Services.AddControllers(options => { options.RespectBrowserAcceptHeader = true; options.ReturnHttpNotAcceptable = true; }).AddXmlSerializerFormatters(); |
ما گزینه ReturnHttpNotAcceptable = true را اضافه کردهایم که به سرور میگوید اگر کلاینت سعی کند برای نوع رسانهای که سرور پشتیبانی نمیکند مذاکره کند، باید کد وضعیت 406 Not Acceptable را برگرداند.
این اپلیکیشن شما را محدودتر می کند و استفاده کننده API را مجبور می کند فقط انواعی را که سرور پشتیبانی می کند را درخواست کند. کد وضعیت 406 برای این منظور ایجاد شده است. میتوانید جزئیات بیشتری در مورد آن در مقاله مرجع HTTP ما بیابید، یا اگر میخواهید حتی در آن عمیقتر شوید، میتوانید RFC2616 را بررسی کنید.
اکنون، بیایید نوع رسانه text/css را با استفاده از Postman واکشی کنیم تا ببینیم چه اتفاقی میافتد:
و همانطور که انتظار می رود، هیچ response body وجود ندارد، و تنها چیزی که به دست می آوریم یک کد وضعیت 406 Not Acceptable است.
Formatter های سفارشی در ASP.NET Core
بیایید تصور کنیم که در حال ساخت یک REST API عمومی هستیم و باید از مذاکره محتوا برای نوعی که «در چارچوب» نیست پشتیبانی کند.
ASP.NET Core از ایجاد formatter های سفارشی پشتیبانی میکند. هدف آنها این است که به شما انعطافپذیری لازم برای ایجاد formatter خود برای هر نوع رسانهای که نیاز به پشتیبانی دارید را بدهد.
با استفاده از روش زیر میتوانیم formatter سفارشی را بسازیم:
- یک کلاس output formatter ایجاد کنید که کلاس TextOutputFormatter را به ارث ببرد
- یک کلاس input formatter ایجاد کنید که کلاس TextInputformatter را به ارث ببرد
- کلاسهای input و output را به collection های InputFormatters و OutputFormatters به همان روشی که برای قالبکننده XML انجام دادیم اضافه کنید.
بیایید یک CSV output formatter سفارشی را برای مثال خود پیادهسازی کنیم.
پیاده سازی یک Formatter سفارشی در ASP.NET Core
از آنجایی که ما در این مقاله فقط به قالب بندی response ها علاقه مندیم، باید فقط یک output formatter پیاده سازی کنیم. ما فقط در صورتی به input formatter نیاز خواهیم داشت که request body حاوی نوع مربوطه باشد.
ایده این است که یک response را برای برگرداندن لیست بلاگ ها و لیست متناظر پست های بلاگ در قالب CSV قالب بندی کنیم.
بیایید یک کلاس CsvOutputFormatter به پروژه خود اضافه کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class CsvOutputFormatter : TextOutputFormatter { public CsvOutputFormatter() { SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/csv")); SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); } protected override bool CanWriteType(Type? type) => typeof(Blog).IsAssignableFrom(type) || typeof(IEnumerable<Blog>).IsAssignableFrom(type); public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { var response = context.HttpContext.Response; var buffer = new StringBuilder(); if (context.Object is IEnumerable<Blog>) { foreach (var Blog in (IEnumerable<Blog>)context.Object) { FormatCsv(buffer, Blog); } } else { FormatCsv(buffer, (Blog)context.Object); } await response.WriteAsync(buffer.ToString(), selectedEncoding); } private static void FormatCsv(StringBuilder buffer, Blog blog) { foreach (var blogPost in blog.BlogPosts) { buffer.AppendLine($"{blog.Name},\"{blog.Description},\"{blogPost.Title},\"{blogPost.Published}\""); } } } |
در اینجا چند نکته قابل توجه است.
در سازنده، ما تعریف کرده ایم که این formatter باید کدام نوع media و همچنین encoding ها را تجزیه کند.
متد override ،CanWriteType شده است و نشان می دهد که آیا می توان نوع Blog را توسط این serializer نوشت یا خیر. متد WriteResponseBodyAsync پاسخ را می سازد. و در نهایت، ما متد FormatCsv را داریم که یک response را آنطور که ما می خواهیم قالب بندی می کند.
پیاده سازی کلاس بسیار ساده است و اصلی ترین چیزی که باید روی آن تمرکز کنید منطق متد FormatCsv است.
اکنون، فقط باید CsvOutputFormatter جدید ساخته شده را به لیست OutputFormatters در متد ()AddMvcOptions اضافه کنیم:
1 2 3 4 5 6 |
builder.Services.AddControllers(options => { options.RespectBrowserAcceptHeader = true; options.ReturnHttpNotAcceptable = true; }).AddXmlSerializerFormatters() .AddMvcOptions(options => options.OutputFormatters.Add(new CsvOutputFormatter())); |
حالا بیایید این را اجرا کنیم و ببینیم آیا واقعاً کار می کند یا خیر. این بار application/csv را به عنوان مقدار هدر Accept در درخواست Postman قرار می دهیم:
کار می کند، فقط اینکه ورودی ما به عنوان یک CSV response قالب بندی شده است.
اگر می خواهید در مورد آنها بیشتر بدانید، یک صفحه عالی در مورد formatter های سفارشی در ASP.NET Core وجود دارد.
نتیجه گیری
در این پست وبلاگ، ما از طریق پیاده سازی دقیق، مکانیسم مذاکره محتوا در یک پروژه ASP.NET Core را بررسی کردیم. ما در مورد formatter ها و نحوه ساخت یک قالب سفارشی و همچنین نحوه تنظیم آنها در پیکربندی پروژه خود یاد گرفتیم.
ما همچنین یاد گرفتیم که چگونه یک برنامه را فقط به انواع خاصی از محتوا محدود کنیم و دیگر نوع ها را نپذیریم.