If you stumble into this with no idea who I am or why I’m arrogant enough to think I’m qualified to potentially criticize the ASP.Net Core internals, I’m the primary author of both StructureMap (the IoC container) and an alternative “previous generation” OSS web development framework called FubuMVC that tackled a lot of the same problems that ASP.Net Core addresses now that were not part of older ASP.Net MVC or Web API.
CORS چیست؟
CORS و یا cross origin resource sharing، یک مکانیزم امنیتی است که در تمام مرورگرهای جدید جهت جلوگیری از ارسال اطلاعات یک وب سایت، به وب سایتی دیگر، درنظر گرفته شدهاست. درخواست دسترسی به یک منبع (Resource) مانند تصویر، داده و غیره، خارج از سرآغاز آن، یک درخواست cross-origin نامیده میشود. برای مدیریت یک چنین درخواستهایی، استانداردی به نام CORS طراحی شدهاست. میتوان به آن مانند نگهبانی یک ساختمان نگاه کرد که تا مجوز خاصی به آنها ارائه نشود، امکان دسترسی به منابع ساختمان را صادر نخواهند کرد.
Origin چیست؟
سرآغاز یک درخواست از سه قسمت تشکیل میشود:
- Protocol/Scheme مانند HTTP/HTTPS
- Host مانند نام دومین
- شماره پورت مانند 443 برای پروتکل HTTPS یا پورت 80 برای HTTP که عموما هر دو مورد به علت پیشفرض بودن، ذکر نمیشوند
بنابراین URLای مانند https://www.dntips.ir یک Origin را مشخص میکند. در اینجا به تمام منابعی که از این سرآغاز شروع میشوند و سه قسمت یاد شدهی آنها یکی است، same-origin گفته میشود.
در این حالت اگر منابعی به URLهایی مانند https://www.dntips.ir (پروتکل متفاوت) و یا https://github.com (با host متفاوت) اشاره کنند، به آنها Different-Origin گفته خواهد شد.
سیاست امنیتی Same-Origin چیست؟
سیاست امنیتی Same-Origin که به صورت توکار در تمام مرورگرهای امروزی تعبیه شدهاست، مانع تعامل سرآغازهایی متفاوت، جهت جلوگیری از حملات امنیتی مانند Cross-Site Request Forgery یا CSRF میشود. این سیاست امنیتی بسیار محدود کنندهاست و برای مثال مانع این میشود که بتوان توسط کدهای JavaScript ای، درخواستی را به سرآغاز دیگری ارسال کرد؛ حتی اگر بدانیم درخواست رسیده از منبعی مورد اطمینان صادر شدهاست. برای مثال اگر سایت جاری یک درخواست Ajax ای را به https://anotherwebsite.com/api/users ارسال کند، چون قسمت host مربوط به origin آنها یکی نیست، این عملیات توسط مرورگر غیرمجاز شمرده شده و مسدود میشود.
چگونه میتوان از تنظیمات CORS، برای رفع محدودیتهای سیاستهای دسترسی Same-Origin استفاده کرد؟
استاندارد CORS تعدادی header ویژه را جهت تبادل اطلاعات بین سرور و مرورگر مشخص کردهاست تا توسط آنها بتوان منابع را بین سرآغازهای متفاوتی به اشتراک گذاشت. در این حالت ابتدا کلاینت درخواستی را به سمت سرور ارسال میکند. سپس سرور پاسخی را به همراه هدرهای ویژهای که مشخص میکنند به چه منابعی و چگونه میتوان دسترسی یافت، به سمت کلاینت ارسال خواهد کرد. اکثر این هدرها با Access-Control-Allow شروع میشوند:
• Access-Control-Allow-Origin
در آن لیست سرآغازهایی را که سرور مایل است منابع خود را با آنها به اشتراک بگذارد، مشخص میشوند. در حالت توسعهی برنامه میتوان از مقدار * نیز برای آن استفاده کرد تا هر سرآغازی بتواند به منابع سرور دسترسی پیدا کند. اما از استفادهی این مقدار سراسری و کلی، در حالت انتشار برنامه خودداری کنید.
• Access-Control-Allow-Headers
• Access-Control-Allow-Methods
• Access-Control-Allow-Credentials
درخواستهای ویژهی Pre-Flight
در بسیاری از موارد، مرورگر زمانیکه تشخیص میدهد درخواست صادر شدهی از طرف برنامه، قرار است به یک Origin دیگر ارسال شود، خودش یک درخواست ویژه را به نام Pre-Flight و از نوع OPTIONS به سمت سرور ارسال میکند. علت آن این است که سرورهای قدیمی، مفهوم CORS را درک نمیکنند و در برابر درخواستهای ویژهای مانند Delete که از سرور جاری صادر نشده باشند، مقاومت میکنند (صرفا درخواستهای Same-Origin را پردازش میکنند). به همین جهت است که اگر به برگهی network ابزارهای توسعه دهندگان مرورگر خود دقت کنید، درخواستهای cross-origin به نظر دوبار ارسال شدهاند. بار اول درخواستی که method آن، options است و در خواست دوم که درخواست اصلی است و برای مثال method آن put است. این درخواست اول، Pre-Flight Request نام دارد. هدف آن این است که بررسی کند آیا سرور مقصد، استاندارد CORS را متوجه میشود یا خیر و آیا اینقدر قدیمی است که صرفا درخواستهای Same-Origin را پردازش میکند و مابقی را مسدود خواهد کرد. اگر سرور درخواست Pre-Flight را درک نکند، مرورگر درخواست اصلی را صادر نخواهد کرد.
البته درخواستهای pre-flight بیشتر در حالتهای زیر توسط مرورگر صادر میشوند:
- اگر متد اجرایی مدنظر، متدی بجز GET/POST/HEAD باشد.
- اگر content type درخواستهای از نوع POST، چیزی بجز application/x-www-form-urlencoded, multipart/form-data و یا text/plain باشند.
- اگر درخواست، به همراه یک هدر سفارشی باشد نیز سبب صدور یک pre-flight request خواهد شد.
بنابراین در برنامههای SPA خود که اطلاعات را با فرمت JSON به سمت یک Web API ارسال میکنند (و Origin آنها یکی نیست)، حتما شاهد درخواستهای Pre-Flight نیز خواهید بود.
تنظیمات CORS در ASP.NET Core
اکنون که با مفهوم CORS آشنا شدیم، در ادامه به نحوهی انجام تنظیمات آن در برنامههای ASP.NET Core خواهیم پرداخت.
تنظیمات ابتدایی CORS در فایل Startup.cs و متد ConfigureServices آن انجام میشوند:
public void ConfigureServices(IServiceCollection services) { // Add service and create Policy with options services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() ); }); // Make sure MVC is added AFTER CORS services.AddMvc(); }
در اینجا CorsPolicy که به عنوان پارامتر مشخص شدهاست، نام این سیاست دسترسی سفارشی است و در قسمتهای مختلف برنامه میتوان ارجاعاتی را به این نام تعریف کرد.
و برای تعریف سرآغازی خاص و یا متدی مشخص، میتوان به صورت زیر عمل کرد (ذکر صریح WithOrigins برای حالت توزیع برنامه مناسب است و از دیدگاه امنیتی، استفادهی از AllowAnyOrigin کار صحیحی نیست ):
services.AddCors(options => { options.AddPolicy("AllowOrigin", builder => builder.WithOrigins("http://localhost:55294") .WithMethods("GET", "POST", "HEAD")); });
پس از تنظیم سیاست دسترسی مدنظر، اکنون نوبت به اعمال آن است. اینکار در متد Configure فایل Startup.cs انجام میشود:
public void Configure(IApplicationBuilder app) { // ... app.UseCors("CorsPolicy"); // ... app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
متد app.UseCors، یک متد سراسری است و کل سیستم را داخل این سیاست دسترسی CORS قرار میدهد. اگر میخواهید صرفا به کنترلر خاصی این تنظیمات اعمال شوند، میتوان از فیلتر EnableCors با ذکر نام سیاست دسترسی تعریف شده، استفاده کرد (در این حالت ذکر services.AddCors ضروری است و از ذکر app.UseCors صرفنظر میشود):
[EnableCors("CorsPolicy")] public class MyApiController : Controller
و یا اگر میخواهید از app.UseCors سراسری استفاده کنید، اما آنرا برای کنترلر و یا حتی اکشن متد خاصی غیرفعال نمائید، میتوان از فیلتر [DisableCors] استفاده کرد.
چند نکته:
- به نام رشتهای سیاست دسترسی تعریف شده، دقت داشته باشید. اگر این نام را درست ذکر نکنید، هدری تولید نخواهد شد.
- میانافزار CORS، برای درخواستهای same-origin نیز هدری را تولید نمیکند.
امکان تعریف سیاستهای دسترسی بدون نام نیز وجود دارد
در هر دو روش فوق که یکی بر اساس app.UseCors سراسری و دیگری بر اساس فیلتر EnableCors اختصاصی کار میکنند، ذکر نام سیاست دسترسی options.AddPolicy ضروری است. اگر علاقهای به ذکر این نام ندارید، میتوان به طور کامل از تنظیم services.AddCors صرفنظر کرد (قسمت پارامتر و تنظیمات آنرا ذکر نکرد) و متد app.UseCors را به صورت زیر تنظیم نمود که قابلیت داشتن تنظیمات خاص خود را نیز دارا است:
public void ConfigureServices(IServiceCollection services) { services.AddCors(); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseCors(policyBuilder => { string[] origins = new string[] { "http://localhost:2000", "http://localhost:2001" }; policyBuilder .AllowAnyHeader() .AllowAnyMethod() .WithOrigins(origins); }); app.UseMvc(); }
روش دیگر انجام اینکار، تعریف یک DefaultPolicy است:
public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddDefaultPolicy( builder => { builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); options.AddPolicy("MyCORSPolicy", builder => { builder.WithOrigins("http://localhost:49373") .AllowAnyHeader() .AllowAnyMethod(); }); }); services.AddMvc().AddNewtonsoftJson(); }
خطاهای متداول حین کار با CORS
خطای کار با SignalR و ارسال اطلاعات اعتبارسنجی کاربر
Access to XMLHttpRequest at 'http://localhost:22135/chat/negotiate?jwt=<removed the jwt key>' from origin 'http://localhost:53150' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
services.AddCors(options => { options.AddPolicy("AllowAllOrigins", builder => { builder .AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); });
تمام اعتبارسنجیها و اطلاعات کوکیها در سمت سرور قابل دسترسی نیستند
CORS به صورت پیشفرض اطلاعات کوکیها را به دومینی دیگر ارسال نمیکند. در این حالت اگر حتما نیاز به انجام اینکار است، باید در درخواست Ajax ای ارسالی، خاصیت withCredentials به true تنظیم شود. همچنین سمت سرور نیز هدر Access-Control-Allow-Credentials را تنظیم کند (همان متد AllowCredentials فوق). در اینجا Credentials به کوکیها، هدرهای اعتبارسنجی (مانند هدرهای JWT) کاربران و یا مجوزهای TLS کلاینتها اشاره میکند.
var xhr = new XMLHttpRequest(); xhr.open('get', 'http://www.example.com/api/test'); xhr.withCredentials = true; //In jQuery: $.ajax({ type: 'get', url: 'http://www.example.com/home', xhrFields: { withCredentials: true } //ES6 fetch API: fetch( 'http://remote-url.com/users', { 'PUT', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, mode: 'cors', credentials: 'include', body: JSON.stringify(body) } ) .then((response) => response.json()) .then((response) => console.log(response)) .catch((error) => console.error(error));
CORS حساس به حروف کوچک و بزرگ است
روش دیگر تنظیم فیلتر EnableCors را در اینجا مشاهده میکنید:
[EnableCors(origins: "http://MyWebsite.com", headers: "*", methods: "*")] //is not the same as this: [EnableCors(origins:"http://mywebsite.com", headers: "*", methods: "*")]
در حالت بروز خطای مدیریت نشدهای در سمت سرور، ASP.NET Core هدرهای CORS را ارسال نمیکند
اطلاعات بیشتر
فراخوانی Web API از طریق مرورگر
با فشردن کلید F5، پروژه را اجرا کنید. شکل ذیل ظاهر میشود.
صفحه ای که ظاهر میشود، یک View است که توسط HomeController و متد Index آن برگشت داده شده است. برای فراخوانی متدهای موجود در کلاس Controller مثال قسمت قبل که مربوط به Web API است، باید به یکی از آدرسهای اشاره شده در قسمت قبل برویم. به عنوان مثال، برای به دست آوردن لیست تمامی محصولات، به آدرس http://localhost:xxxx/api/products بروید. xxxx، شمارهی پورتی است که Web Server داخلی Visual Studio در هنگام اجرای پروژه به آن اختصاص میدهد. آن را نسبت به پروژهی خود تغییر دهید.
نتیجهی دریافتی بستگی به نوع مرورگری دارد که استفاده میکنید. Internet Explorer از شما در مورد باز کردن یا ذخیرهی فایلی با نام products پرسش میکند (شکل ذیل).
محتوای فایل، بدنهی پاسخ دریافتی است. اگر این فایل را باز کنید، خواهید دید که که محتوای آن، لیستی از محصولات با فرمت JSON مانند ذیل است.
[{"Id":1,"Name":"Tomato soup","Category":"Groceries","Price":1.39},{"Id":2,"Name": "Yo-yo","Category":"Toys","Price":3.75},{"Id":3,"Name":"Hammer","Category": "Hardware","Price":16.99}]
دلیل تفاوت در نتیجهی دریافتی این است که مرورگر Internet Explorer و Firefox، هر یک مقدار متفاوتی را در هدر Accept درخواست، ارسال میکنند. بنابراین، Web API نیز مقدار متفاوتی را در پاسخ برگشت میدهد.
حال به آدرسهای ذیل بروید:
http://localhost:xxxx/api/products/1
http://localhost:xxxx/api/products?category=hardware
اولین آدرس، باید محصولی با مشخصهی 1 را برگشت دهد و دومین آدرس، لیستی از تمامی محصولاتی که در دستهی hardware قرار دارند را برگشت میدهد (در مثال ما فقط یک آیتم این شرط را دارد).
نکته: در صورتی که در هنگام فراخوانی هر یک از متدهای Web API با خطای ذیل مواجه شدید، دستور [("AcceptVerbs("GET", "POST] را به ابتدای متدها اضافه کنید.
The requested resource does not support http method 'GET'
فراخوانی Web API با استفاده از کتابخانهی jQuery
در قسمت قبل، متدهای Web API را مستقیماً از طریق وارد کردن آدرس آنها در نوار آدرس مرورگر فراخوانی کردیم. اما در اکثر اوقات، این متدها با روشهای برنامه نویسی توسط یک Client فراخوانی میشوند. اجازه بدهید Clientیی ایجاد کنیم که با استفاده از jQuery، متدهای ما را فراخوانی میکند.
در Solution Explorer، از پوشهی Views و سپس Home، فایل Index.cshtml را باز کنید.
تمامی محتویات این View را حذف و کدهای ذیل را در آن قرار دهید.
<!DOCTYPE html> <html> <head> <title>ASP.NET Web API</title> <script src="../../Scripts/jquery-1.7.2.min.js" type="text/javascript"></script> </head> <body> <div> <h1>All Products</h1> <ul id='products' /> </div> <div> <label for="prodId">ID:</label> <input type="text" id="prodId" size="5"/> <input type="button" value="Search" onclick="find();" /> <p id="product" /> </div> </body> </html>
بازیابی لیستی از محصولات
برای بازیابی لیستی از محصولات، فقط کافی است تا یک درخواست از نوع GET به آدرس "/api/products" بفرستید. این کار با jQuery به صورت ذیل انجام میشود.
<script type="text/javascript"> $(document).ready(function () { // Send an AJAX request $.getJSON("api/products/", function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, val) { // Format the text to display. var str = val.Name + ': $' + val.Price; // Add a list item for the product. $('<li/>', { html: str }) .appendTo($('#products')); }); }); }); </script>
متد getJSON، یک درخواست AJAX از نوع GET را ارسال میکند و پاسخ دریافتی آن نیز با فرمت JSON خواهد بود. دومین پارامتر متد getJSON، یک callback است که پس از دریافت موفقیت آمیز پاسخ اجرا میشود.
بازیابی یک محصول با استفاده از مشخصهی آن
برای بازیابی یک محصول با استفاده از مشخصهی آن، یک درخواست از نوع GET به آدرس "api/products/id/" ارسال کنید. id، مشخصهی محصول است. کد ذیل را در ادامهی کد قبل و پیش از تگ <script/> قرار دهید.
function find() { var id = $('#prodId').val(); $.getJSON("api/products/" + id, function (data) { var str = data.Name + ': $' + data.Price; $('#product').html(str); }) .fail( function (jqXHR, textStatus, err) { $('#product').html('Error: ' + err); }); }
باز هم از متد getJSON استفاده کردیم، اما این بار مقدار id برای آدرس از یک Text Box خوانده و آدرس ایجاد میشود. پاسخ دریافتی، یک محصول در قالب JSON است.
اجرای پروژه
پروژه را با فشردن کلید F5 اجرا کنید. پس از نمایش فرم، تمامی محصولات بر روی صفحه نمایش داده میشوند. عدد 1 را وارد و بر روی دکمهی Search کلیک کنید، محصولی که مشخصهی آن 1 است نمایش داده میشود (شکل ذیل).
اگر مشخصه ای را وارد کنید که وجود ندارد، خطای 404 با مضمون "Error: Not Found" بر روی صفحه نمایش داده میشود و در صورتی که به جای عدد، عبارتی غیر عددی وارد کنید، خطای 400 با مضمون: "Error: Bad Request" نمایش داده میشود. در Web API، تمامی پاسخها باید در قالب کدهای وضعیت HTTP باشند (شکل ذیل). این یکی از اصول اساسی کار با وب سرویسها است. وفادار ماندن به مفاهیم پایهی وب، دید بهتری در مورد اتفاقاتی که میافتد به شما میدهد.
در قسمت بعد با مفهوم مسیریابی در ASP.NET Web API آشنا میشوید.
ASP.NET Web API - قسمت اول
Static Reflection
قابلیت Dynamic reflection یا به اختصار همان reflection متداول، از اولین نگارشهای دات نت فریم در دسترس است و امکان دسترسی به اطلاعات مرتبط با کلاسها، متدها، خواص و غیره را در زمان اجرا مهیا میسازد. تابحال به کمک این قابلیت، امکان تهیهی ابزارهای پیشرفتهی زیر مهیا شده است:
انواع و اقسام
- فریم ورکهای آزمون واحد
- code generators
- ORMs
- ابزارهای آنالیز کد
و ...
برای مثال فرض کنید که میخواهید برای یک کلاس به صورت خودکار، متدهای آزمون واحد تهیه کنید (تهیه یک code generator ساده). اولین نیاز این برنامه، دسترسی به امضای متدها به همراه نام آرگومانها و نوع آنها است. برای حل این مساله باید برای مثال یک parser زبان سی شارپ یا اگر بخواهید کاملتر کار کنید، به ازای تمام زبانهای قابل استفاده در دات نت فریم ورک باید parser تهیه کنید که ... کار سادهای نیست. اما با وجود reflection به سادگی میتوان به این نوع اطلاعات دسترسی پیدا کرد و نکتهی مهم آن هم این است که مستقل است از نوع زبان مورد استفاده. به همین جهت است که این نوع ابزارها را در فریم ورکهایی که فاقد امکانات reflection هستند، کمتر میتوان یافت. برای مثال کیفیت کتابخانههای آزمون واحد CPP در مقایسه با آنچه که در دات نت مهیا هستند، اصلا قابل مقایسه نیستند. برای نمونه به یکی از معظمترین فریم ورکهای آزمون واحد CPP که توسط گوگل تهیه شده مراجعه کنید : (+)
قابلیت Reflection ، مطلب جدیدی نیست و برای مثال زبان جاوا هم سالها است که از آن پشتیبانی میکند. اما نگارش سوم دات نت فریم ورک با معرفی lambda expressions ، LINQ و Expressions در یک سطح بالاتر از این Dynamic reflection متداول قرار گرفت.
تعریف Static Reflection :
استفاده از امکانات Reflection API بدون بکارگیری رشتهها، به کمک قابلیت اجرای به تعویق افتادهی LINQ، جهت دسترسی به متادیتای المانهای کد، مانند خواص، متدها و غیره.
برای مثال کد زیر را در نظر بگیرید:
//dynamic reflection
PropertyInfo property = typeof (MyClass).GetProperty("Name");
MethodInfo method = typeof (MyClass).GetMethod("SomeMethod");
چقدر خوب میشد اگر این قابلیت بجای dynamic بودن (مشخص شدن در زمان اجرا)، استاتیک میبود و در زمان کامپایل قابل بررسی میشد. این امکان به کمک lambda expressions و expression trees دات نت سه بعد، میسر شده است. کلیدهای اصلی Static Reflection کلاسهای Func و Expression هستند. با استفاده از کلاس Func میتوان lambda expression ایی را تعریف کرد که مقداری را بر میگرداند و توسط کلاس Expression میتوان به محتوای یک delegate دسترسی یافت. ترکیب این دو، قدرت دستیابی به اطلاعاتی مانند PropertyInfo را در زمان طراحی کلاسها، میدهد؛ با توجه به اینکه:
- کاملا توسط intellisense موجود در VS.NET پشتیبانی میشود.
- با استفاده از ابزارهای refactoring قابل کنترل است.
- از همه مهمتر، دیگری خبری از رشتهها نبوده و همه چیز تحت کنترل کامپایلر قرار میگیرد.
و شاید هیچ قابلیتی به اندازهی Static Reflection در این چندسال اخیر بر روی اکوسیستم دات نت فریم ورک تاثیرگذار نبوده باشد. این روزها کمتر کتابخانه یا فریم ورکی را میتوانید پیدا کنید که از Static Reflection استفاده نکند. سرآغاز استفاده گسترده از آن به Fluent NHibernate بر میگردد؛ سپس در انواع و اقسام mocking frameworks ، ORMs و غیره استفاده شد و مدتی است که در ASP.NET MVC نیز مورد استفاده قرار میگیرد (برای مثال TextBoxFor معروف آن):
public string TextBoxFor<T>(Expression<Func<T,object>> expression);
<%= this.TextBoxFor(model => model.FirstName); %>
یک مثال ساده از تعریف و بکارگیری Static Reflection :
public PropertyInfo GetProperty<T>(Expression<Func<T, object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");
return memberExpression.Member as PropertyInfo;
}
برای نمونه Fluent NHibernate در پشت صحنه متد Map ، به کمک متدی شبیه به GetProperty فوق، a => a.Address1 را به رشته متناظر خاصیت Address1 تبدیل کرده و جهت تعریف نگاشتها مورد استفاده قرار میدهد:
public class AddressMap : DomainMap<Address>
{
public AddressMap()
{
Map(a => a.Address1);
}
}
جهت اطلاع؛ قابلیت استفاده از «کد به عنوان اطلاعات» هم مفهوم جدیدی نیست و برای مثال زبان Lisp چند دهه است که آنرا ارائه داده است!
برای مطالعه بیشتر:
طراحی Url در Restful API
Url بخش اصلی و راه ارتباطی API شما با توسعه دهنده است .بنابراین طراحی یک ساختار مناسب و یکپارچه برای Url ها دارای اهمیت زیادی است .
Url پایه API خود را ساده و خوانا ، حفظ کنید . داشتن یک Url پایه ساده استفاده از API را آسان کرده و خوانایی آن را بالا میبرد و باعث میشود که توسعه دهنده برای استفاده از آن نیاز کمتری به مراجعه به مستندات داشته باشد. پیشنهاد میشود که برای هر منبع تنها دو Url پایه وجود داشته باشد . یکی برای مجموعه ای از منبع موردنظر و دیگری برای یک واحد مشخص از آن منبع . برای مثال اگر منبع موردنظر ما کتاب باشد ، خواهیم داشت :
.../books
.../books/1001
استفاده از این روش یک مزیت دیگر هم به همراه دارد و آن دور کردن افعال از Urlها است.
بسیاری در زمان طراحی Urlها و در نامگذاری از فعلها استفاده میکنند. برای هر منبعی که مدلسازی میکنید هیچ وقت نمیتوانید آن را به تنهایی و جداافتاده در نظر بگیرید. بلکه همیشه منابع مرتبطی وجود دارند که باید در نظر گرفته شوند. در مثال کتاب میتوان منابعی مثل نویسنده ، ناشر ، موضوع و ... را بیان کرد. حالا سعی کنید به تمام Url هایی که برای پوشش دادن تمام درخواستهای مربوط به منبع کتاب نیاز داریم فکر کنید . احتمالا به چیزی شبیه این میرسیم :
.../getAllBooks .../getBook .../newBook .../getNewBooksSince .../getComputerBooks .../BooksNotPublished .../UpdateBookPriceTo .../bookForPublisher .../GetLastBooks .../DeleteBook ….
پس حالا این درخواستهای متنوع را چطور با دو Url اصلی انجام دهیم ؟
1- از افعال Http برای کار کردن بر روی منابع استفاده کنید . با استفاده از افعال Http شامل POST ، GET ، PUT و DELETE و دو Url اصلی ، یک مجموعهی مناسب از عملیاتها در دسترس توسعه دهنده خواهد بود . به جدول زیر نگاه کنید .
توسعه دهندگان احتمالا نیازی به این جدول برای درک اینکه API چطور کار میکند نخواهند داشت.
2- با استفاده از نکته قبلی بخشی از Urlهای بالا حذف خواهند شد. اما هنوز با روابط بین منابع چکار کنیم؟ منابع تقریبا همیشه دارای روابطی با دیگر منابع هستند . یک روش ساده برای بیان این روابط در API چیست ؟ به مثال کتاب برمیگردیم. کتابها دارای نویسنده هستند. اگر بخواهیم کتابهای یک نویسنده را برگردانیم چه باید بکنیم؟ با استفاده از Urlهای پایه و افعال Http میتوان اینکار را انجام داد. یکی از ساختارهای ممکن این است :
GET .../authors/1001/books
POST .../authors/1001/books
3- بیشتر APIها دارای پیچیدگیهای بیشتری نسبت به Url اصلی یک منبع هستند . هر منبع مشخصات
و روابط متنوعی دارد که قابل جستجو کردن، مرتب سازی، بروزرسانی و تغییر هستند. Url
اصلی را ساده نگه دارید و این پیچیدگیها را به کوئری استرینگ منتقل کنید.
برای برگرداندن تمام کتابهای با قیمت پنچ هزار تومان با قطع جیبی که دارای امتیاز 8 به بالا هستند از کوئری زیر میشود استفاده کرد :
GET .../books?price=5000&size=pocket&score=8
و البته فراموش نکنید که لیستی از فیلدهای مجاز را در مستندات خود ارائه کنید.
4 - گفتیم که بهتر است افعال را از Url ها خارج کنیم . ولی در مواردی که درخواست ارسال شده در مورد یک منبع نیست چطور؟ مواردی مثل محاسبه مالیات پرداختی یا هزینه بیمه ، جستجو در کل منابع ، ترجمه یک عبارت یا تبدیل واحدها . هیچکدام از اینها ارتباطی با یک منبع خاص ندارند. در این موارد بهتر است از افعال استفاده شود. و حتما در مستندات خود ذکر کنید که در این موارد از افعال استفاده میشود..../convert?value=25&from=px&to=em .../translate?term=web&from=en&to=fa
5 - استفاده از اسامی جمع یا مفرد
با توجه به ساختاری که تا اینجا طراحی کرده ایم بکاربردن اسامی جمع بامعناتر و خواناتر است. اما مهمتر از روشی که بکار میبرید ، اجتناب از بکاربردن هر دو روش با هم است ، اینکه در مورد یک منبع از اسم منفرد و در مورد دیگری از اسم جمع استفاده کنید . یکدستی API را حفظ کنید و به توسعه دهنده کمک کنید راحتتر API شما را یاد بگیرد.
6- استفاده از نامهای عینی به جای نامهای کلی و انتزاعیAPI ی را در نظر بگیرید که محتواهایی را در فرمتهای مختلف ارائه میدهد. بلاگ ، ویدئو ، اخبار و .... حالا فرض کنیداین API منابع را در بالاتری سطح مدسازی کرده باشد مثل /items یا /assets . درک کردن محتوای این API و کاری که میتوان با این API انجام داد برای توسعه دهنده سخت است . خیلی راحتتر و مفیدتر است که منابع را در قالب بلاگ ، اخبار ، ویدئو مدلسازی کنیم .
using Microsoft.AspNetCore.Mvc; namespace Core3xWebApi.Controllers { [ApiController] [Route("[controller]")] public class HomeController : ControllerBase { [HttpGet] public ActionResult<string> Get() { return null; } } }
همانطور که مشاهده میکنید، هیچ خروجی تولید نشده و تنها به تنظیم status-code مساوی 204 که به معنای no content است، اکتفا کردهاست.
چرا از کشن متدهای ASP.NET Core نمیتوان خروجی نال گرفت؟
فرمت خروجی اکشن متدها در ASP.NET Core بر اساس output formatterهای متفاوتی مانند JSON ،XML و غیره، تعیین میشود. اما زمانیکه به نال میرسد، از یک output formatter خاص به نام HttpNoContentOutputFormatter کمک میگیرد. کار آن تبدیل null، به خروجی مخصوصی است که توضیح داده شد. به این معنا که هیچگاه خروجی نال برنامه، برای مثال به نحو متداولی تبدیل به یک خروجی استاندارد JSON نمیشود و این مساله برای Http Clientهای مختلفی که منتظر یک خروجی استاندارد هستند (مانند انواع برنامههای تک صفحهای وب)، عموما مساله ساز است؛ چون هر status-code دیگری بجز 200 را به صورت بروز یک خطا تفسیر میکنند که در حالت فوق، فقط به معنای نبود اطلاعات است و نه بروز خطایی.
چگونه خروجی نال را به همان شکلی که هست بازگشت دهیم؟
HttpNoContentOutputFormatter به همراه خاصیت TreatNullValueAsNoContent نیز هست که اگر در ابتدای تنظیمات برنامه به false تنظیم شود، دیگر مقادیر نال را به خروجی no content تبدیل نمیکند:
namespace Core3xWebApi { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(options => { // remove formatter that turns nulls into 204 - No Content responses // this formatter breaks SPA's Http response JSON parsing options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>(); options.OutputFormatters.Insert(0, new HttpNoContentOutputFormatter { TreatNullValueAsNoContent = false }); }); }