نظرات مطالب
Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها
public class Result
{
    public bool Success { get; private set; }
    public string Error { get; private set; }
    public bool Failure { /* … */ }
    protected Result(bool success, string error) { /* … */ }
    public static Result Fail(string message) { /* … */ }
    public static Result<T> Ok<T>(T value) {  /* … */ }
}

public class Result<T> : Result
{
    public T Value { get; set; }
    protected internal Result(T value, bool success, string error)
        : base(success, error)
    {
        /* … */
    }
}

مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 8 - فعال سازی ASP.NET MVC
پیشنیازهای بحث (از قسمت 8 به بعد این سری)
اگر پیشتر سابقه‌ی کار کردن با ASP.NET MVC را ندارید، نیاز است «15 مورد» ابتدایی مطالب ASP.NET MVC سایت را پیش از ادامه‌ی این سری مطالعه کنید؛ از این جهت که این سری از مطالب «ارتقاء» نام دارند و نه «بازنویسی مجدد». دراینجا بیشتر تفاوت‌ها و روش‌های تبدیل کدهای قدیمی، به جدید را بررسی خواهیم کرد؛ تا اینکه بخواهیم تمام مطالبی را که وجود دارند از صفر بازنویسی کنیم.


فعال سازی ASP.NET MVC

تا اینجا خروجی برنامه را صرفا توسط میان افزار app.Run نمایش دادیم. اما در نهایت می‌خواهیم یک برنامه‌ی ASP.NET MVC را برفراز ASP.NET Core 1.0 اجرا کنیم و این قابلیت نیز به صورت پیش فرض غیرفعال است. برای فعال سازی آن نیاز است ابتدا بسته‌ی نیوگت آن‌را نصب کرد. سپس سرویس‌های مرتبط با آن‌را ثبت و معرفی نمود و در آخر میان افزار خاص آن‌را فعال کرد.


نصب وابستگی‌های ASP.NET MVC

برای این منظور بر روی گره references کلیک راست کرده و گزینه‌ی manage nuget packages را انتخاب کنید. سپس در برگه‌ی browse آن Microsoft.AspNetCore.Mvc را جستجو کرده و نصب نمائید:


انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
 {
    "dependencies": {
      //same as before  
      "Microsoft.AspNetCore.Mvc": "1.0.0"
 },


تنظیم سرویس‌ها و میان افزارهای ASP.NET MVC

پس از نصب بسته‌ی نیوگت ASP.NET MVC، دو تنظیم ذیل در فایل آغازین برنامه، برای شروع به کار با ASP.NET MVC کفایت می‌کنند:
الف) ثبت یکجای سرویس‌های ASP.NET MVC
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

ب) معرفی میان افزار ASP.NET MVC
public void Configure(IApplicationBuilder app)
{
   app.UseFileServer();
   app.UseMvcWithDefaultRoute();
در مورد متد UseFileServer در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک» بیشتر بحث شد.
در اینجا دو متد UseMvc و UseMvcWithDefaultRoute را داریم. اولی، امکان تعریف مسیریابی‌های سفارشی را میسر می‌کند و دومی به همراه یک مسیریابی پیش فرض است.


افزودن اولین کنترلر برنامه و معرفی POCO Controllers


در ویژوال استودیو بر روی نام پروژه کلیک راست کرده و پوشه‌ی جدیدی را به نام کنترلر اضافه کنید (تصویر فوق). سپس به این پوشه کلاس جدید HomeController را با این محتوا اضافه کنید:
namespace Core1RtmEmptyTest.Controllers
{
    public class HomeController
    {
        public string Index()
        {
            return "Running a POCO controller!";
        }
    }
}
در ادامه برای اینکه فایل index.html موجود در پوشه‌ی wwwroot بجای محتوای اکشن متد Index ما نمایش داده نشود (با توجه به تقدم و تاخر میان افزارهای ثبت شده‌ی در کلاس آغازین برنامه)، این فایل را حذف کره و یا تغییر نام دهید.
سپس برنامه را اجرا کنید. این خروجی باید قابل مشاهده باشد:


اگر با نگارش‌های قبلی ASP.NET MVC کار کرده باشید، تفاوت این کنترلر با آن‌ها، در عدم ارث بری آن از کلاس پایه‌ی Controller است. به همین جهت به آن POCO Controller نیز می‌گویند (plain old C#/CLR object).
در ASP.NET Core، همینقدر که یک کلاس public غیر abstract را که نامش به Controller ختم شود، داشته باشید و این کلاس در اسمبلی باشد که ارجاعی را به وابستگی‌های ASP.NET MVC داشته باشد، به عنوان یک کنترلر معتبر شناخته شده و مورد استفاده قرار خواهد گرفت. در نگارش‌های قبلی، شرط ارث بری از کلاس پایه Controller نیز الزامی بود؛ اما در اینجا خیر. هدف از آن نیز کاهش سربارهای وهله سازی یک کنترلر است. اگر صرفا می‌خواهید یک شیء را به صورت JSON بازگشت دهید، شاید وهله سازی یک کلاس ساده، بسیار بسیار سریعتر از نمونه سازی یک کلاس مشتق شده‌ی از Controller، به همراه تمام وابستگی‌های آن باشد.

 البته هنوز هم مانند قبل، کنترلرهای مشتق شده‌ی از کلاس پایه‌ی Controller قابل تعریف هستند:
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class AboutController : Controller
    {
        public IActionResult Index()
        {
            return Content("Hello from DNT!");
        }
    }
}
با این خروجی:


تفاوت دیگری را که ملاحظه می‌کنید، خروجی IActionResult بجای ActionResult نگارش‌های قبلی است. در اینجا هنوز هم ActionResult را می‌توان بکار برد و اینبار ActionResult، پیاده سازی پیش فرض اینترفیس IActionResult است.
و اگر بخواهیم در POCO Controllers شبیه به return Content فوق را پیاده سازی کنیم، نیاز است تا تمام جزئیات را از ابتدا پیاده سازی کنیم (چون کلاس پایه و ساده ساز Controller در اختیار ما نیست):
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class HomeController
    {
        [ActionContext]
        public ActionContext ActionContext { get; set; }
 
        public HttpContext HttpContext => ActionContext.HttpContext;
 
        public string Index()
        {
            return "Running a POCO controller!";
        }
 
        public IActionResult About()
        {
            return new ContentResult
            {
                Content = "Hello from DNT!",
                ContentType = "text/plain; charset=utf-8"
            };
        }
    }
}
همانطور که ملاحظه می‌کنید اینبار بجای return Content ساده، باید وهله سازی شیء ContentResult از ابتدا صورت گیرد؛ به همراه تمام جزئیات آن.
به علاوه در اینجا نحوه‌ی دسترسی به HttpContext را هم مشاهده می‌کنید. ویژگی ActionContext سبب تزریق اطلاعات آن به کنترلر جاری شده و سپس از طریق آن می‌توان به HttpContext و تمام قابلیت‌های آن دسترسی یافت.
اینجا است که می‌توان میزان سبکی و سریعتر بودن POCO Controllers را احساس کرد. شاید در کنترلری نیاز به این وابستگی‌ها نداشته باشید. اما زمانیکه کنترلری از کلاس پایه‌ی Controller مشتق می‌شود، تمام این وابستگی‌ها را به صورت پیش فرض و حتی در صورت عدم استفاده، در اختیار خواهد داشت و این در اختیار داشتن یعنی وهله سازی شدن تمام وابستگی‌های مرتبط با شیء پایه‌ی Controller. به همین جهت است که POCO Controllers بسیار سبک‌تر و سریع‌تر از کنترلرهای متداول مشتق شده‌ی از کلاس پایه‌ی Controller عمل می‌کنند.
مطالب
ASP.NET MVC #17

فیلترهای امنیتی ASP.NET MVC

ASP.NET MVC به همراه تعدادی فیلتر امنیتی توکار است که در این قسمت به بررسی آ‌ن‌ها خواهیم پرداخت.


بررسی اعتبار درخواست (Request Validation) در ASP.NET MVC

ASP.NET MVC امکان ارسال اطلاعاتی را که دارای تگ‌های HTML باشند، نمی‌دهد. این قابلیت به صورت پیش فرض فعال است و جلوی ارسال انواع و اقسام اطلاعاتی که ممکن است سبب بروز حملات XSS Cross site scripting attacks شود را می‌گیرد. نمونه‌ای از خطای نمایش داده:

A potentially dangerous Request.Form value was detected from the client (Html="<a>"). 

بنابراین تصمیم گرفته شده صحیح است؛ اما ممکن است در قسمتی از سایت نیاز باشد تا کاربران از یک ویرایشگر متنی پیشرفته استفاده کنند. خروجی این نوع ویرایشگرها هم HTML است. در این حالت می‌توان صرفا برای متدی خاص امکانات Request Validation را به کمک ویژگی ValidateInput غیرفعال کرد:

[HttpPost]
[ValidateInput(false)]
public ActionResult CreateBlogPost(BlogPost post)

از ASP.NET MVC 3.0 به بعد راه حل بهتری به کمک ویژگی AllowHtml معرفی شده است. غیرفعال کردن ValidateInput ‌ایی که معرفی شد، بر روی تمام خواص شیء BlogPost اعمال می‌شود. اما اگر فقط بخواهیم که مثلا خاصیت Text آن از مکانیزم بررسی اعتبار درخواست خارج شود، بهتر است دیگر از ویژگی ValidateInput استفاده نشده و به نحو زیر عمل گردد:

using System;
using System.Web.Mvc;

namespace MvcApplication14.Models
{
public class BlogPost
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }

[AllowHtml]
public string Text { set; get; }
}
}

در اینجا فقط خاصیت Text مجاز به دریافت محتوای HTML ایی خواهد بود. اما خاصیت Title چنین مجوزی را ندارد. همچنین دیگر نیازی به استفاده از ویژگی ValidateInput غیرفعال شده نیز نخواهد بود.
به علاوه همانطور که در قسمت‌های قبل نیز ذکر شد، خروجی Razor به صورت پیش فرض Html encoded است مگر اینکه صریحا آن‌را تبدیل به HTML کنیم (مثلا استفاده از متد Html.Raw). به عبارتی خروجی Razor در حالت پیش فرض در مقابل حملات XSS مقاوم است مگر اینکه آگاهانه بخواهیم آن‌را غیرفعال کنیم.

مطلب تکمیلی
مقابله با XSS ؛ یکبار برای همیشه!



فیلتر RequireHttps

به کمک ویژگی یا فیلتر RequireHttps، تمام درخواست‌های رسیده به یک متد خاص باید از طریق HTTPS انجام شوند و حتی اگر شخصی سعی به استفاده از پروتکل HTTP معمولی کند، به صورت خودکار به HTTPS هدایت خواهد شد:

[RequireHttps]
public ActionResult LogOn()
{
}


فیلتر ValidateAntiForgeryToken

نوع دیگری از حملات که باید در برنامه‌های وب به آن‌ها دقت داشت به نام CSRF یا Cross site request forgery معروف هستند.
برای مثال فرض کنید کاربری قبل از اینکه بتواند در سایت شما کار خاصی را انجام دهد، نیاز به اعتبار سنجی داشته باشد. پس از لاگین شخص و ایجاد کوکی و سشن معتبر، همین شخص به سایت دیگری مراجعه می‌کند که در آن مهاجمی بر اساس وضعیت جاری اعتبار سنجی او مثلا لینک حذف کاربری یا افزودن اطلاعات جدیدی را به برنامه ارائه می‌دهد. چون سشن شخص و کوکی مرتبط به سایت اول هنوز معتبر هستند و شخص سایت را نبسته است، «احتمال» اجرا شدن درخواست مهاجم بالا است (خصوصا اگر از مرورگرهای قدیمی استفاده کند).
بنابراین نیاز است بررسی شود آیا درخواست رسیده واقعا از طریق فرم‌های برنامه ما صادر شده است یا اینکه شخصی از طریق سایت دیگری اقدام به جعل درخواست‌ها کرده است.
برای مقابله با این نوع خطاها ابتدا باید داخل فرم‌های برنامه از متد Html.AntiForgeryToken استفاده کرد. کار این متد ایجاد یک فیلد مخفی با مقداری منحصربفرد بر اساس اطلاعات سشن جاری کاربر است، به علاوه ارسال یک کوکی خودکار تا بتوان از تطابق اطلاعات اطمینان حاصل کرد:

@using (Html.BeginForm()) {
@Html.AntiForgeryToken()

در مرحله بعد باید فیلتر ValidateAntiForgeryToken جهت بررسی مقدار token دریافتی به متد ثبت اطلاعات اضافه شود:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateBlogPost(BlogPost post)

در اینجا مقدار دریافتی از فیلد مخفی فرم :

<input name="__RequestVerificationToken" type="hidden" value="C0iPfy/3T....=" />

با مقدار موجود در کوکی سایت بررسی و تطابق داده خواهند شد. اگر این مقادیر تطابق نداشته باشند، یک استثنا صادر شده و از پردازش اطلاعات رسیده جلوگیری می‌شود.
علاوه بر این‌ها بهتر است حین استفاده از متد و فیلتر یاد شده، از یک salt مجزا نیز به ازای هر فرم، استفاده شود:

[ValidateAntiForgeryToken(Salt="1234")]

@Html.AntiForgeryToken(salt:"1234")

به این ترتیب tokenهای تولید شده در فرم‌های مختلف سایت یکسان نخواهند بود.
به علاوه باید دقت داشت که ValidateAntiForgeryToken فقط با فعال بودن کوکی‌ها در مرورگر کاربر کار می‌کند و اگر کاربری پذیرش کوکی‌ها را غیرفعال کرده باشد، قادر به ارسال اطلاعاتی به برنامه نخواهد بود. همچنین این فیلتر تنها در حالت HttpPost قابل استفاده است. این مورد هم در قسمت‌های قبل تاکید گردید که برای مثال بهتر است بجای داشتن لینک delete در برنامه که با HttpGet ساده کار می‌کند،‌ آن‌را تبدیل به HttpPost نمود تا میزان امنیت برنامه بهبود یابد. از HttpGet فقط برای گزارشگیری و خواندن اطلاعات از برنامه استفاده کنید و نه ثبت اطلاعات.
بنابراین استفاده از AntiForgeryToken را به چک لیست اجباری تولید تمام فرم‌های برنامه اضافه نمائید.

مطلب مشابه
Anti CSRF module for ASP.NET



فیلتر سفارشی بررسی Referrer

یکی دیگر از روش‌های مقابله با CSRF، بررسی اطلاعات هدر درخواست ارسالی است. اگر اطلاعات Referrer آن با دومین جاری تطابق نداشت، به معنای مشکل دار بودن درخواست رسیده است. فیلتر سفارشی زیر می‌تواند نمونه‌ای باشد جهت نمایش نحوه بررسی UrlReferrer درخواست رسیده:

using System.Web.Mvc;

namespace MvcApplication14.CustomFilter
{
public class CheckReferrerAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext != null)
{
if (filterContext.HttpContext.Request.UrlReferrer == null)
throw new System.Web.HttpException("Invalid submission");

if (filterContext.HttpContext.Request.UrlReferrer.Host != "mysite.com")
throw new System.Web.HttpException("This form wasn't submitted from this site!");
}

base.OnAuthorization(filterContext);
}
}
}

و برای استفاده از آن:
[HttpPost]
[CheckReferrer]
[ValidateAntiForgeryToken]
public ActionResult DeleteTask(int id)


نکته‌ای امنیتی در مورد آپلود فایل‌ها در ASP.NET

هر جایی که کاربر بتواند فایلی را به سرور شما آپلود کند، مشکلات امنیتی هم از همانجا شروع خواهند شد. مثلا در متد Upload قسمت 11 این سری، منعی در آپلود انواع فایل‌ها نیست و کاربر می‌تواند انواع و اقسام شل‌ها را جهت تحت کنترل گرفتن سایت و سرور آپلود و اجرا کند. راه حل چیست؟
از همان روش امنیتی مورد استفاده توسط تیم ASP.NET MVC استفاده می‌کنیم. فایل web.config قرار گرفته در پوشه Views را باز کنید (نه فایل وب کانفیگ ریشه اصلی سایت‌را). چنین تنظیمی را می‌توان مشاهده کرد:
برای IIS6 :

<system.web>
<httpHandlers>
<add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
</system.web>
برای IIS7 :
<system.webServer>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>


تنظیم فوق، موتور اجرایی ASP.NET را در این پوشه خاص از کار می‌اندازد. به عبارتی اگر شخصی مسیر یک فایل aspx یا cshtml یا هر فایل قرار گرفته در پوشه Views را مستقیما در مرورگر خود وارد کند، با پیغام HttpNotFound مواجه خواهد شد.
این روش هم با ASP.NET Web forms سازگار است و هم با ASP.NET MVC؛ چون مرتبط است به موتور اجرایی ASP.NET که هر دوی این فریم ورک‌ها برفراز آن معنا پیدا می‌کنند.
بنابراین در پوشه فایل‌های آپلودی به سرور خود یک web.config را با محتوای فوق ایجاد کنید (و فقط باید مواظب باشید که این فایل حین آپلود فایل‌های جدید، overwrite نشود. مهم!). به این ترتیب این مسیر دیگر از طریق مرورگر قابل دسترسی نخواهد بود (با هر محتوایی). سپس برای ارائه فایل‌های آپلودی به کاربران از روش زیر استفاده کنید:

public ActionResult Download()
{
return File(Server.MapPath("~/Myfiles/test.txt"), "text/plain");
}

مزیت مهم روش ذکر شده این است که کاربران مجاز به آپلود هر نوع فایلی خواهند بود و نیازی نیست لیست سیاه تهیه کنید که مثلا فایل‌هایی با پسوند‌های خاص آپلود نشوند (که در این بین ممکن است لیست سیاه شما کامل نباشد ...).




علاوه بر تمام فیلترهای امنیتی که تاکنون بررسی شدند،‌ فیلتر دیگری نیز به نام Authorize وجود دارد که در قسمت‌های بعدی بررسی خواهد شد.
مطالب
بررسی بهبودهای ProblemDetails در ASP.NET Core 7x
در زمان ارائه‌ی ASP.NET Core 2.1، ویژگی جدیدی به نام [ApiController] ارائه شد که با استفاده از آن، یکسری اعمال توکار جهت سهولت کار با Web API توسط خود فریم‌ورک انجام می‌شوند؛ برای مثال عدم نیاز به بررسی وضعیت ModelState و بررسی خودکار آن با علامتگذاری یک کنترلر به صورت ApiController. یکی دیگر از این ویژگی‌های توکار، تبدیل خروجی تمام status codeهای بزرگتر و یا مساوی 400 یا همان Bad Request، به شیء جدید و استاندارد ProblemDetails است:
{
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "status": 403,
}
 بازگشت یک چنین خروجی یک‌دست و استانداردی، استفاده‌ی از آن‌را توسط کلاینت‌ها، ساده و قابل پیش‌بینی می‌کند. البته باید درنظر داشت که اگر در این‌حالت، برنامه یک استثنای معمولی را سبب شود، ProblemDetails ای بازگشت داده نمی‌شود. اگر برنامه در حالت توسعه اجرا شود، با استفاده از میان‌افزار app.UseDeveloperExceptionPage، یک صفحه‌ی نمایش جزئیات خطا ظاهر می‌شود و اگر برنامه در حالت تولید و ارائه‌ی نهایی اجرا شود، یک صفحه‌ی خالی (بدون داشتن response body) با status code مساوی 500 بازگشت داده می‌شود. این کمبود ویژه و امکانات سفارشی سازی بیشتر آن، به صورت توکار به ASP.NET Core 7x اضافه شده‌اند و دیگر نیازی به استفاده از کتابخانه‌های ثالث دیگری برای انجام آن نیست.


ProblemDetails بر اساس RFC7807 طراحی شده‌است

RFC7807، قالب استانداردی را برای ارائه‌ی خطاهای HTTP APIها تعریف می‌کند تا نیازی به وجود تعاریف متعددی در این زمینه نباشد و خروجی آن قابل پیش‌بینی و قابل بررسی توسط تمام کلاینت‌های یک API باشد. کلاس ProblemDetails در ASP.NET Core نیز بر همین اساس طراحی شده‌است.
این RFC دو فرمت خروجی را بر اساس مقدار مشخص شده‌ی در هدر Content-Type بازگشت داده شده، مجاز می‌داند:
  • JSON: “application/problem+json” media type
  • XML: “application/problem+xml” media type

که با توجه به این هدر ارسالی، اگر از یک کلاینت از نوع HttpClient استفاده کنیم، می‌توان بر اساس مقدار ویژه‌ی «application/problem+json» تشخیص داد که خروجی API دریافتی، به همراه خطا است و نحوه‌ی پردازش آن به صورت زیر خواهد بود:
var mediaType = response.Content.Headers.ContentType?.MediaType;
if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase))
{
   var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(null, ct) ?? new ProblemDetails();
   // ...
}
در اینجا بدنه‌ی اصلی شیء ProblemDetails بازگشت داده شده، می‌تواند به همراه اعضای زیر باشد:
- type: یک رشته‌است که به آدرس مستندات HTML ای مرتبط با خطای بازگشت داده شده، اشاره می‌کند.
- title: رشته‌ای است که خلاصه‌ی خطای رخ‌داده را بیان می‌کند.
- detail: رشته‌ای است که توضیحات بیشتری را در مورد خطای رخ‌داده، بیان می‌کند.
- instance: رشته‌ای است که به آدرس محل بروز خطا اشاره می‌کند.
- status: عددی است که بیانگر HTTP status code بازگشتی از سمت سرور است.


البته اگر ویژگی ApiController بر روی کنترلرهای خود استفاده نمی‌کنید، می‌توانید این خروجی را به صورت زیر هم با استفاده از return Problem، تولید کنید:
[HttpPost("/sales/products/{sku}/availableForSale")]
public async Task<IActionResult> AvailableForSale([FromRoute] string sku)
{
   return Problem(
            "Product is already Available For Sale.",
            "/sales/products/1/availableForSale",
            400,
            "Cannot set product as available.",
            "http://example.com/problems/already-available");
}


امکان افزودن اعضای سفارشی به شیء ProblemDetails

امکان بسط این خروجی، با افزودن اعضای سفارشی نیز پیش‌بینی شده‌است. یک نمونه‌ی متداول و پرکاربرد آن، بازگشت خطاهای مرتبط با اعتبارسنجی اطلاعات رسیده‌است:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Content-Language: en
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "errors": {
        "User": [
            "The user name is not verified."
        ]
    }
}
در اینجا عضو جدید errors را بنابر نیاز این مساله‌ی خاص، مشاهده می‌کنید که در صورت استفاده از ویژگی ApiController بر روی کنترلرهای Web API، به صورت خودکار توسط ASP.NET Core تولید می‌شود و نیازی به تنظیم خاصی و یا کدنویسی اضافه‌تری ندارد. کلاس مخصوص آن نیز ValidationProblemDetails‌ است.


جهت افزودن اعضای سفارشی دیگری به شیء ProblemDetails می‌توان به صورت زیر عمل کرد:
namespace WebApplication.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class DemoController : ControllerBase
    {
        [HttpPost]
        public ActionResult Post()
        {
            var problemDetails = new ProblemDetails
            {
                Detail = "The request parameters failed to validate.",
                Instance = null,
                Status = 400,
                Title = "Validation Error",
                Type = "https://example.net/validation-error",
            };

            problemDetails.Extensions.Add("invalidParams", new List<ValidationProblemDetailsParam>()
            {
                new("name", "Cannot be blank."),
                new("age", "Must be great or equals to 18.")
            });

            return new ObjectResult(problemDetails)
            {
                StatusCode = 400
            };
        }
    }

    public class ValidationProblemDetailsParam
    {
        public ValidationProblemDetailsParam(string name, string reason)
        {
            Name = name;
            Reason = reason;
        }

        public string Name { get; set; }
        public string Reason { get; set; }
    }
}
شیء ProblemDetails، به همراه خاصیت Extensions است که می‌توان به آن یک <Dictionary<string, object را انتساب داد و نمونه‌ای از آن‌را در مثال فوق مشاهده می‌کنید. این مثال سبب می‌شود تا عضو جدیدی با کلید دلخواه invalidParams، به همراه لیستی از name و reasonها به خروجی نهایی اضافه شود. مقدار این کلید، از نوع object است؛ یعنی هر شیء دلخواهی را در اینجا می‌توان تعریف و استفاده کرد.


معرفی سرویس جدید ProblemDetails در دات نت 7

در دات نت 7 می‌توان سرویس‌های جدید ProblemDetails را به نحو زیر به برنامه اضافه کرد:
services.AddProblemDetails();
پس از آن به 3 روش مختلف می‌توان از امکانات این سرویس‌ها استفاده کرد:
الف) با اضافه کردن میان‌افزار مدیریت خطاها
app.UseExceptionHandler();
پس از آن، هر استثنای مدیریت نشده‌ای نیز به صورت یک ProblemDetails ظاهر می‌شود و دیگر همانند قبل، سبب نمایش یک صفحه‌ی خالی نخواهد شد.

ب) با افزودن میان‌افزار StatusCodePages
app.UseStatusCodePages();
در این حالت مواردی که استثناء شمرده نمی‌شوند مانند 404، در صورت بروز رسیدن به یک مسیریابی یافت نشده و یا 405، در صورت درخواست یک HTTP method غیرمعتبر نیز توسط یک ProblemDetails استاندارد مدیریت می‌شوند.

ج) با افزودن میان‌افزار صفحه‌ی استثناءهای توسعه دهنده‌ها
app.UseDeveloperExceptionPage();
به این ترتیب در خروجی ProblemDetails، اطلاعات بیشتری از استثناء رخ‌داده، مانند استک‌تریس آن ظاهر خواهد شد.


امکان بازگشت ساده‌تر یک ProblemDetails سفارشی در دات نت 7

برای سفارشی سازی خروجی ProblemDetails، علاوه بر راه‌حلی که پیشتر در این مطلب مطرح شد، می‌توان در دات نت 7 از روش تکمیلی ذیل نیز استفاده کرد:
builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("MachineName", Environment.MachineName));
به این ترتیب در صورت لزوم می‌توان یک عضو سفارشی سراسری را به تمام اشیاء ProblemDetails برنامه به صورت خودکار اضافه کرد و یا اگر می‌خواهیم این مورد را کمی اختصاصی‌تر کنیم، می‌توان به صورت زیر عمل کرد:

الف) تعریف یک ErrorFeature سفارشی
public class MyErrorFeature
{
    public ErrorType Error  { get; set; }
}
​
public enum ErrorType
{
    ArgumentException
}
در ASP.NET Core می‌توان به شیء HttpContext.Features قابل تنظیم در هر اکشن متدی، اشیاء دلخواهی را مانند شیء سفارشی فوق، اضافه کرد و سپس در قسمت options.CustomizeProblemDetails تنظیماتی که ذکر شد، به دریافت و تنظیم آن، واکنش نشان داد.

ب) تنظیم مقدار ErrorFeature سفارشی در اکشن متدها
    [HttpGet("{value}")]
    public IActionResult MyErrorTest(int value)
    {
        if (value <= 0)
        {
            var errorType = new MyErrorFeature
            {
                Error = ErrorType.ArgumentException
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }
​
        return Ok(value);
    }
پس از تعریف شیءایی که قرار است به HttpContext.Features اضافه شود، اکنون روش تنظیم و مقدار دهی آن‌را در یک اکشن متد، در مثال فوق مشاهده می‌کنید.

ج) واکنش نشان دادن به دریافت ErrorFeature سفارشی
services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
    {
        var MyErrorFeature = ctx.HttpContext.Features.Get<MyErrorFeature>();
​
        if (MyErrorFeature is not null)
        {
            (string Title, string Detail, string Type) details = MyErrorFeature.Error switch
            {
                ErrorType.ArgumentException =>
                (
                    nameof(ArgumentException),
                    "This is an argument-exception.",
                    "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1"
                ),
                _ =>
                (
                    nameof(Exception),
                    "default-exception",
                    "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1"
                )
            };
​
            ctx.ProblemDetails.Title = details.Title;
            ctx.ProblemDetails.Detail = details.Detail;
            ctx.ProblemDetails.Type = details.Type;
        }
    }
);
پس از تنظیم HttpContext.Features در اکشن متدی، می‌توان در options.CustomizeProblemDetails فوق، توسط متد ctx.HttpContext.Features.Get به آن شیء خاص تنظیم شده، در صورت وجود دسترسی یافت و سپس جزئیات بیشتری را از آن استخراج و مقادیر ctx.ProblemDetails جاری را که قرار است به کاربر بازگشت داده شوند، بازنویسی کرد و یا تغییر داد.
 

امکان تبدیل ساده‌تر اطلاعات استثناءهای سفارشی به یک ProblemDetails سفارشی در دات نت 7

بجای استفاده از تنظیمات services.AddProblemDetails جهت بازنویسی مقدار شیء ProblemDetails بازگشتی، می‌توان جزئیات میان‌افزار app.UseExceptionHandler را نیز سفارشی سازی کرد و به بروز استثناءهای خاصی واکنش نشان داد. برای مثال فرض کنید یک استثنای سفارشی را به صورت زیر طراحی کرده‌اید:
public class MyCustomException : Exception
{
    public MyCustomException(
        string message,
        HttpStatusCode statusCode = HttpStatusCode.BadRequest
    ) : base(message)
    {
        StatusCode = statusCode;
    }
​
    public HttpStatusCode StatusCode { get; }
}
و سپس در اکشن متدی، سبب بروز آن شده‌اید:
    [HttpGet("{value}")]
    public IActionResult MyErrorTest(int value)
    {
        if (value <= 0)
        {
            throw new MyCustomException("The value should be positive!");
        }
​
        return Ok(value);
    }
اکنون می‌توان در میان‌افزار مدیریت استثناءهای برنامه، نسبت به مدیریت این استثناء خاص، واکشن نشان داد و ProblemDetails متناظری را تولید و بازگشت داد:
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        context.Response.ContentType = "application/problem+json";
​
        if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
        {
            var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
            var exceptionType = exceptionHandlerFeature?.Error;
​
            if (exceptionType is not null)
            {
                (string Title, string Detail, string Type, int StatusCode) details = exceptionType switch
                {
                    MyCustomException MyCustomException =>
                    (
                        exceptionType.GetType().Name,
                        exceptionType.Message,
                        "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1",
                        context.Response.StatusCode = (int)MyCustomException.StatusCode
                    ),
                    _ =>
                    (
                        exceptionType.GetType().Name,
                        exceptionType.Message,
                        "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1",
                        context.Response.StatusCode = StatusCodes.Status500InternalServerError
                    )
                };
​
                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = details.Title,
                        Detail = details.Detail,
                        Type = details.Type,
                        Status = details.StatusCode
                    }
                });
            }
        }
    });
});
​
در اینجا نحوه‌ی کار با سرویس توکار IProblemDetailsService و سپس دسترسی به IExceptionHandlerFeature و استثنای صادر شده را مشاهده می‌کنید. پس از آن بر اساس نوع و اطلاعات این استثناء، می‌توان یک ProblemDetails مخصوص را تولید و در خروجی ثبت کرد.
مطالب
C# 7 - More Expression-Bodied Members
یکی از امکانات جالب سی‌شارپ که در نسخه 6 معرفی شد، قابلیت Expression-Bodied Members بود. در نسخه 7 سی‌شارپ، امکانات جدیدتری اضافه شده است؛ به عنوان مثال اکنون می‌توان برای constructors, finalizers و همچنین get and set برای پراپرتی‌ها و ایندکسرها نیز از این قابلیت استفاده کرد.

 
استفاده از expression body برای constructors 
public class Person
{
    public string FirstName { get; set; }
    public Person(string firstName)
    {
        this.FirstName = firstName;
    }
}
به عنوان مثال اکنون سازنده‌ی کلاس فوق را می‌توانیم از روش block body متداول، به روش expression body، به صورت خلاصه‌تری بنویسیم:
public class Person
{
     public string FirstName { get; set; }
     public Person(string firstName) => this.FirstName = firstName;
}
البته محدودیت این روش این است که تنها برای یک پارامتر می‌توانیم به اینصورت عمل کنیم؛ اما در نسخه‌ 7.1  قرار است قابلیت استفاده از expression body برای بیشتر از یک پارامتر نیز اضافه شود:
public class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age) => (Name, Age) = (name, age);
}

اما اگر نیاز داشتید برای بیشتر از دو متغیر از expression body استفاده کنید می‌توانید از Tuple برای شبیه‌سازی آن استفاده کنید(+):
public class Person
{
    private readonly (string name, int age) _tuple;    

    public string Name => _tuple.name;
    public int Age => _tuple.age;

    public Person(string name, int age) => _tuple = (name, age);
}

استفاده از expression body برای destructors 
public class Resource
{
    ~Resource() => Console.WriteLine("destructor");
}


 استفاده از expression body در get / set accessors 
 در سی‌شارپ 7 برای accessors نیز می‌توانیم از سینتکس جدید expression body استفاده کنیم. به عنوان مثال کد زیر را در نظر بگیرید:
private int _x;
public int X 
{
    get
    {
        return _x;
    }
    set
    {
        _x = value;
    }
}
کد فوق را می‌توانیم در سی‌شارپ 7 به صورت خلاصه‌تری بنویسیم:
private int _x;
public int X 
{
    get => _x;
    set => _x = value;
}

در ویژوال‌استودیوی 2017 نیز با قرار دادن ماوس بر روی پراپرتی x_، استفاده‌ی از سینتکس expression body به شما پیشنهاد داده خواهد شد:


همچنین برای Event Accessors نیز می‌توانیم از این قابلیت استفاده کنیم:

private EventHandler _someEvent;
public event EventHandler SomeEvent
{
    add => _someEvent += value;
    remove => _someEvent -= value;
}


نظرات مطالب
فعال سازی قسمت آپلود تصویر و فایل Kendo UI Editor
اگر thumb‌ها به درستی  نمایش داده نمی‌شود و فقط قسمتی از عکس رو مشاهده می‌کنید ، با استفاده از قطعه کد زیر این مشکل رفع خواهد شد :
1- تعریف کلاس به صورت زیر
public class ImageSize
    {
        public int Height
        {
            get;
            set;
        }

        public int Width
        {
            get;
            set;
        }
    }
2- تعریف کلاس ImageResizer همانند زیر :
public class ImageResizer
    {
        public ImageSize Resize(ImageSize originalSize, ImageSize targetSize)
        {
            var aspectRatio = (float)originalSize.Width / (float)originalSize.Height;
            var width = targetSize.Width;
            var height = targetSize.Height;

            if (originalSize.Width > targetSize.Width || originalSize.Height > targetSize.Height)
            {
                if (aspectRatio > 1)
                {
                    height = (int)(targetSize.Height / aspectRatio);
                }
                else
                {
                    width = (int)(targetSize.Width * aspectRatio);
                }
            }
            else
            {
                width = originalSize.Width;
                height = originalSize.Height;
            }

            return new ImageSize
            {
                Width = Math.Max(width, 1),
                Height = Math.Max(height, 1)
            };
        }
    }

3- تعریف کلاس ThumbnailCreator  همانند نمونه زیر :
 public class ThumbnailCreator
    {
        private static readonly IDictionary<string, ImageFormat> ImageFormats = new Dictionary<string, ImageFormat>{
            {"image/png", ImageFormat.Png},
            {"image/gif", ImageFormat.Gif},
            {"image/jpeg", ImageFormat.Jpeg}
        };

        private readonly ImageResizer resizer;

        public ThumbnailCreator()
        {
            this.resizer = new ImageResizer();
        }

        public byte[] Create(Stream source, ImageSize desiredSize, string contentType)
        {
            using (var image = Image.FromStream(source))
            {
                var originalSize = new ImageSize
                {
                    Height = image.Height,
                    Width = image.Width
                };

                var size = resizer.Resize(originalSize, desiredSize);

                using (var thumbnail = new Bitmap(size.Width, size.Height))
                {
                    ScaleImage(image, thumbnail);

                    using (var memoryStream = new MemoryStream())
                    {
                        thumbnail.Save(memoryStream, ImageFormats[contentType]);

                        return memoryStream.ToArray();
                    }
                }
            }
        }

        private void ScaleImage(Image source, Image destination)
        {
            using (var graphics = Graphics.FromImage(destination))
            {
                graphics.CompositingMode = CompositingMode.SourceCopy;
                graphics.CompositingQuality = CompositingQuality.HighQuality;
                graphics.SmoothingMode = SmoothingMode.AntiAlias;
                graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
                graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

                graphics.DrawImage(source, 0, 0, destination.Width, destination.Height);
            }
        }
    }

4- تعریف اکشن Thumbnail همانند زیر :
private FileContentResult CreateThumbnail(string physicalPath)
        {
            using (var fileStream = System.IO.File.OpenRead(physicalPath))
            {
                var desiredSize = new ImageSize
                {
                    Width = ThumbnailWidth,
                    Height = ThumbnailHeight
                };


                string contentType = MimeMapping.GetMimeMapping(physicalPath);
                return File(thumbnailCreator.Create(fileStream, desiredSize, contentType), contentType);
            }
        }

و در پایان اکشن GetThumbnail  را همانند زیر تغییر خواهیم داد :
 public virtual ActionResult GetThumbnail(string path)
        {
            path = GetSafeFileAndDirPath(path);
          //  return File(path, contentType); 
            return CreateThumbnail(path);
        }



مطالب
Blazor 5x - قسمت 24 - تهیه API مخصوص Blazor WASM - بخش 1 - ایجاد تنظیمات ابتدایی
تا اینجا با اصول توسعه‌ی برنامه‌های مبتنی بر Blazor Server آشنا شدیم. در ادامه‌ی این سری، روش توسعه برنامه‌های مبتنی بر Blazor WASM را بررسی خواهیم کرد و پیش از شروع آن، باید بتوان امکانات سمت سرور مورد نیاز این نوع برنامه‌های سمت کلاینت را از طریق یک Web API تامین کرد که شامل دریافت و ارائه‌ی اطلاعات و همچنین اعتبارسنجی و احراز هویت مبتنی بر JWT یکپارچه‌ی با ASP.NET Core Identity است.


ایجاد پروژه‌ی ASP.NET Core Web API

برای تامین اطلاعات برنامه‌ی سمت کلاینت Blazor WASM و همچنین فراهم آوردن زیرساخت اعتبارسنجی کاربران آن، نیاز به یک پروژه‌ی ASP.NET Core Web API داریم که آن‌را با اجرای دستور dotnet new webapi در یک پوشه‌ی خالی، برای مثال به نام BlazorWasm.WebApi ایجاد می‌کنیم.
البته این پروژه، از زیرساختی که در برنامه‌ی Blazor Server بررسی شده‌ی تا این قسمت، ایجاد کردیم نیز استفاده خواهد کرد. همانطور که پیشتر نیز عنوان شد، هدف از قسمت Blazor Server مثال این سری، آشنایی با مدل برنامه نویسی خاص آن بود؛ وگرنه می‌توان کل این پروژه را با Blazor Server و یا کل آن‌را با Web API + Blazor WASM نیز پیاده سازی کرد. در این مثال، قسمت‌های مدیریتی برنامه‌ی مدیریت هتل را توسط Blazor Server (مانند قسمت‌های تعریف اتاق‌ها و امکانات رفاهی هتل) و قسمت مخصوص کاربران آن‌را مانند رزرو کردن اتاق‌ها، توسط Blazor WASM پیاده سازی می‌کنیم. به همین جهت قسمت‌هایی از این دو پروژه، مانند سرویس‌های استفاده شده‌ی در پروژه‌ی Blazor server، در پروژه‌ی Web API مکمل Blazor WASM، قابلیت استفاده‌ی مجدد را دارند.


افزودن سرویس‌های آغازین مورد نیاز، به پروژه‌ی Web API

در فایل آغازین BlazorWasm\BlazorWasm.WebApi\Startup.cs، برای شروع به تکمیل Web API، نیاز به این سرویس‌ها را داریم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        //...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAutoMapper(typeof(MappingProfile).Assembly);

            services.AddScoped<IHotelRoomService, HotelRoomService>();
            services.AddScoped<IAmenityService, AmenityService>();
            services.AddScoped<IHotelRoomImageService, HotelRoomImageService>();

            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

            services.AddIdentity<IdentityUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            //...
در اینجا سرویس‌های AutoMapper، تنظیمات ابتدایی DbContext برنامه، به همراه سرویس‌های Identity (بدون UI آن) و افزودن سرویس‌های اتاق‌ها و امکانات رفاهی هتل را نیاز داریم. به همین جهت ارجاعات و وابستگی‌های زیر را به فایل csproj جاری اضافه می‌کنیم تا پروژه‌های DataAccess ،Services و Mappings قابل دسترسی و استفاده شوند:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" />
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.Services\BlazorServer.Services.csproj" />
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.Models.Mappings\BlazorServer.Models.Mappings.csproj" />
  </ItemGroup>
</Project>
همچنین در این پروژه نیز از همان بانک اطلاعاتی پروژه‌ی Blazor Server که تاکنون تکمیل کردیم، استفاده می‌کنیم. بنابراین محتوای فایل BlazorWasm\BlazorWasm.WebApi\appsettings.json آن نیز مشابه‌است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}


تعریف کنترلر HotelRoom

در ادامه کدهای اولین کنترلر Web API را مشاهده می‌کنید که مرتبط است با بازگشت اطلاعات تمام اتاق‌های ثبت شده و یا بازگشت اطلاعات یک اتاق ثبت شده:
using BlazorServer.Models;
using BlazorServer.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]")]
    public class HotelRoomController : ControllerBase
    {
        private readonly IHotelRoomService _hotelRoomService;

        public HotelRoomController(IHotelRoomService hotelRoomService)
        {
            _hotelRoomService = hotelRoomService;
        }

        [HttpGet]
        public IAsyncEnumerable<HotelRoomDTO> GetHotelRooms()
        {
            return _hotelRoomService.GetAllHotelRoomsAsync();
        }

        [HttpGet("{roomId}")]
        public async Task<IActionResult> GetHotelRoom(int? roomId)
        {
            if (roomId == null)
            {
                return BadRequest(new ErrorModel
                {
                    Title = "",
                    ErrorMessage = "Invalid Room Id",
                    StatusCode = StatusCodes.Status400BadRequest
                });
            }

            var roomDetails = await _hotelRoomService.GetHotelRoomAsync(roomId.Value);
            if (roomDetails == null)
            {
                return BadRequest(new ErrorModel
                {
                    Title = "",
                    ErrorMessage = "Invalid Room Id",
                    StatusCode = StatusCodes.Status404NotFound
                });
            }

            return Ok(roomDetails);
        }
    }
}
- این کنترلر، از سرویس IHotelRoomService که در قسمت‌های قبل تکمیل کردیم، استفاده می‌کند.
- ErrorModel آن‌را در همان پروژه‌ی قبلی مدل‌ها، در فایل BlazorServer\BlazorServer.Models\ErrorModel.cs به صورت زیر ایجاد کرده‌ایم:
namespace BlazorServer.Models
{
    public class ErrorModel
    {
        public string Title { get; set; }

        public int StatusCode { get; set; }

        public string ErrorMessage { get; set; }
    }
}
در این حالت اگر برنامه‌ی Web API را اجرا کنیم، به خروجی Swagger زیر می‌رسیم که جزئیات این فناوری را در سری «مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger» پیشتر بررسی کردیم:


یکی از مزایای آن، امکان آزمایش API تهیه شده، بدون نیاز به تهیه‌ی هیچ نوع کلاینت خاصی است. برای مثال اگر بر روی api​/hotelroom آن کلیک کنیم، گزینه‌ی «try it out» آن ظاهر شده و با کلیک بر روی آن، اینبار دکمه‌ی execute ظاهر می‌شود. در ادامه با کلیک بر روی دکمه‌ی اجرای آن، اکشن متد GetHotelRooms اجرا شده و خروجی زیر ظاهر می‌شود:


و یا اگر بخواهیم متد GetHotelRoom را توسط آن آزمایش کنیم، بر اساس پارامترهای آن، رابط کاربری زیر را تشکیل می‌دهد که امکان دریافت شماره‌ی اتاق را دارد:



انجام تنظیمات ابتدایی CORS و خروجی JSON برنامه

قرار است این API را از طریق پروژه‌ی Blazor سمت کلاینت خود استفاده کنیم که آدرس آن، با آدرس API یکی نیست. به همین جهت نیاز است تنظیمات CORS را به صورت زیر اضافه کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
         // ... 

            services.AddCors(o => o.AddPolicy("HotelManagement", builder =>
            {
                builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
            }));

            services.AddControllers()
                    .AddJsonOptions(options =>
                    {
                        options.JsonSerializerOptions.PropertyNamingPolicy = null;
                        // To avoid `JsonSerializationException: Self referencing loop detected error`
                        options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
                    });
         // ... 
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ... 

            app.UseCors("HotelManagement");
            app.UseRouting();

            app.UseAuthentication();
            // ...
        }
    }
}
در اینجا علاوه بر تنظیمات CORS، تنظیمات JsonSerializer را هم تغییر داده‌ایم تا خطاهای Self referencing loop را در حین ارائه‌ی خروجی‌های Web API، مشاهده نکنیم (همان نکته‌ی «تهیه خروجی JSON از مدل‌های مرتبط، بدون Stack overflow»).


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-24.zip
مطالب
پیاده سازی یک MediaTypeFormatter برای پشتیبانی از MultiPart/form-data در Web API

Media Type یا MIME Type نشان دهنده فرمت یک مجموعه داده است. در HTTP، مدیا تایپ بیان کننده فرمت message body یک درخواست / پاسخ است و به دریافت کننده اعلام می‌کند که چطور باید پیام را بخواند. محل استاندارد تعیین Mime Type در هدر Content-Type است. درخواست کننده می‌تواند با استفاده از هدر Accept لیستی از MimeType‌های قابل قبول را به عنوان پاسخ، به سرور اعلام کند.

Asp.net Web API  از MimeType برای تعیین نحوه serialize یا deserialize کردن محتوای دریافتی / ارسالی استفاده می‌کند


MediaTypeFormatter

Web API برای خواندن/درج پیام در بدنه درخواست/پاسخ از MediaTypeFormmater‌‌ها استفاده می‌کند. اینها کلاس‌هایی هستند که نحوه‌ی Serialize کردن و deserialize کردن اطلاعات به فرمت‌های خاص را تعیین می‌کنند. Web API به صورت توکار دارای formatter هایی برای نوع‌های XML ، JSON، BSON و Form-UrlEncoded می‌باشد. همه این‌ها کلاس پایه MediaTypeFormatter را پیاده سازی می‌کنند. 


مسئله

یک پروژه Web API بسازید و view model زیر را در آن تعریف کنید:

public class NewProduct
    {
        [Required]
        public string Name { get; set; }

        public double Price { get; set; }

        public byte[] Pic { get; set; }
    }

همانطور که می‌بینید یک فیلد از نوع byte[] برای تصویر محصول در نظر گرفته شده است.

حالا یک کنترلر API  ساخته و اکشنی برای دریافت اطلاعات محصول جدید از کاربر می‌نویسیم :
public class ProductsController : ApiController
    {
        [HttpPost]
        public HttpResponseMessage PostProduct(NewProduct model)
        {
            if (ModelState.IsValid)
            {
                //  ثبت محصول 

                return new HttpResponseMessage(HttpStatusCode.Created);
            }

            return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
        }

    }

و یک صفحه html به نام index.html که حاوی یک فرم برای ارسال اطلاعات باشد :

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
    <h1>ساخت MediaTypeFormatter برای Multipart/form-data</h1>
    <h2>محصول جدید</h2>

    <form id="newProduct" method="post" action="/api/products" enctype="multipart/form-data">
        <div>
            <label for="name">نام محصول : </label>
            <input type="text" id="name" name="name" />
        </div>
        <div>
            <label for="price">قیمت : </label>
            <input type="number" id="price"  name="price" />
        </div>
        <div>
            <label for="pic">تصویر : </label>
            <input type="file" id="pic" name="pic" />
        </div>
        <div>
            <button type="submit">ثبت</button>
        </div>
    </form>
</body>
</html>

زمانی که فرم حاوی فایلی برای آپلود باشد مشخصه encType باید برابر با Multipart/form-data مقداردهی شود تا اطلاعات فایل به درستی کد شوند. در زمان ارسال فرم Content-type درخواست برابر با Multipart/form-data و فرمت اطلاعات درخواست ارسالی به شکل زیر خواهد بود :


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

- Content-Disposition که نام فیلد و نام فایل را شامل می‌شود .

- content-type که mime type مخصوص آن بخش از داده‌ها را مشخص می‌کند.

پس از اینکه فرم را تکمیل کرده و ارسال کنید ، با پیام خطای زیر مواجه می‌شوید :

خطای روی داده اعلام می‌کند که Web API فاقد MediaTypeFormatter برای خواندن اطلاعات ارسال شده با فرمتMultiPart/Form-data  است.  Web API برای خواندن و بایند کردن پارامترهای complex Type از درون بدنه پیام یک درخواست از MediaTypeFormatter استفاده می‌کند و همانطور که گفته شد Web API فاقد Formatter توکار برای deserialize کردن داده‌های با فرمت Multipart/form-data است.

راه حل‌ها :

روشی که در سایت asp.net برای آپلود فایل در web api استفاده شده، عدم استفاده از پارامترها و خواندن محتوای Request در درون کنترلر است. که به طبع در صورتی که بخواهیم کنترلرهای تمیز و کوچکی داشته باشیم روش مناسبی نیست. از طرفی امتیاز parameter binding و modelstate را هم از دست خواهیم داد.

روش دیگری که می‌خواهیم در اینجا پیاده سازی کنیم ساختن یک MediaTypeFormatter برای خواندن فرمت Multipart/form-data است. با این روش کد موردنیاز کپسوله شده و امکان استفاده از binding و modelstate را خواهیم داشت.

برای ساختن یک MediaTypeFormatter یکی از 2 کلاس MediaTypeFormatter یا  BufferedMediaTypeFormatter  را باید پیاده سازی کنیم . تفاوت این دو در این است که BufferedMediaTypeFormatter برخلاف MediaTypeFormatter از متدهای synchronous استفاده می‌کند.

 
پیاده سازی :

یک کلاس به نام MultiPartMediaTypeFormatter می‌سازیم و کلاس MediaTypeFormatter را به عنوان کلاس پایه آن قرار می‌دهیم .

public class MultiPartMediaTypeFormatter : MediaTypeFormatter
{
    ...
}

ابتدا در تابع سازنده کلاس فرمت هایی که می‌خواهیم توسط این کلاس خوانده شوند را تعریف می‌کنیم :

        public MultiPartMediaTypeFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
        }
در اینجا Multipart/form-data را به عنوان تنها نوع مجاز تعریف کرده ایم.

سپس با پیاده سازی توابع CanReadType و CanWriteType مربوط به کلاس MediaTypeFormatter مشخص می‌کنیم که چه مدل‌هایی را می‌توان توسط این کلاس  serialize / deserialize کرد. در اینجا چون می‌خواهیم این کلاس محدود به یک مدل خاص نباشد، از یک اینترفیس برای شناسایی کلاس‌های مجاز استفاده می‌کنیم .

    public interface INeedMultiPartMediaTypeFormatter
    {
    }

و آنرا به کلاس newProduct اضافه می‌کنیم :

public class NewProduct : INeedMultiPartMediaTypeFormatter
{
 ...
}
از آنجا که تنها نیاز به خواندن اطلاعات داریم و قصد نوشتن نداریم، در متد CanWriteType مقدار false را برمی گردانیم .
        public override bool CanReadType(Type type)
        {
            return typeof(INeedMultiPartMediaTypeFormatter).IsAssignableFrom(type);
        }

        public override bool CanWriteType(Type type)
        {
            return false;
        }

و اما تابع ReadFromStreamAsync که کار خواندن محتوای ارسال شده و بایند کردن آنها به پارامترها را برعهده دارد 

public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
که در آن پارامتر type مربوط به مدل مشخص شده به عنوان پارامتر اکشن (NewProduct)است و پارامتر content محتوای درخواست را در خود دارد.

ابتدا محتوای ارسال شده را خوانده و اطلاعات فرم را استخراج می‌کنیم و از طرف دیگر با استفاده از کلاس Activator یک نمونه از مدل جاری را ساخته و لیست property‌های آنرا استخراج می‌کنیم.

            MultipartMemoryStreamProvider provider = await content.ReadAsMultipartAsync();
            IEnumerable<HttpContent> formData = provider.Contents.AsEnumerable();

            var modelInstance = Activator.CreateInstance(type);
            IEnumerable<PropertyInfo> properties = type.GetProperties();

سپس در یک حلقه به ترتیب برای هر property متعلق به مدل، در میان اطلاعات فرم جستجو می‌کنیم. برای پیدا کردن اطلاعات متناظر با هر property در هدر Content-Disposition که در بالا توضیح داده شد، به دنبال فیلد همنام با property می‌گردیم .

            foreach (PropertyInfo prop in properties)
            {
                var propName = prop.Name.ToLower();
                var propType = prop.PropertyType;

                var data = formData.FirstOrDefault(d => d.Headers.ContentDisposition.Name.ToLower().Contains(propName));
در صورتی که فیلدی وجود داشته باشد کار را ادامه می‌دهیم .

 گفتیم که هر فیلد یک هدر، Content-Type هم می‌تواند داشته باشد. این هدر به صورت پیش فرض معادل text/plain است و برای فیلدهای عادی قرار داده نمی‌شود . در این مثال چون فقط یک فیلد غیر رشته ای داریم فرض را بر این گرفته ایم که در صورت وجود Content-Type، فیلد مربوط به تصویر است. در صورتیکهContentType  وجود داشته باشد، محتوای فیلد را به شکل Stream خوانده به byte[] تبدیل و با استفاده از متد SetValue در property مربوطه قرار می‌دهیم.

                if (data != null)
                {
                    if (data.Headers.ContentType != null)
                    {
                        using (var fileStream = await data.ReadAsStreamAsync())
                        {
                            using (MemoryStream ms = new MemoryStream())
                            {
                                fileStream.CopyTo(ms);
                                prop.SetValue(modelInstance, ms.ToArray());
                            }                            
                        }
                    }

در صورتی که Content-Type غایب باشد بدین معنی است که محتوای فیلد از نوع رشته است ( عدد ، تاریخ ، guid ، رشته ) و باید به نوع مناسب تبدیل شود. ابتدا آن را به صورت یک رشته می‌خوانیم و با استفاده از Convert.ChangeType آنرا به نوع مناسب تبدیل می‌کنیم و در property متناظر قرار می‌دهیم .

                if (data != null)
                {
                    if (data.Headers.ContentType != null)
                    {
                        //...
                    }
                    else
                    {
                        string rawVal = await data.ReadAsStringAsync();
                        object val = Convert.ChangeType(rawVal, propType);

                        prop.SetValue(modelInstance, val);
                    }
                }
و در نهایت نمونه ساخته شده از مدل را برگشت می‌دهیم.
return modelInstance;
برای فعال کردن این Formatter باید آنرا به لیست formmater‌های web api اضافه بکنیم. فایل WebApiConfig در App_Start را باز کرده و خط زیر را به آن اضافه می‌کنیم:
config.Formatters.Add(new MultiPartMediaTypeFormatter());
حال اگر مجددا فرم را به سرور ارسال کنیم، با پیام خطایی، مواجه نشده و عمل binding با موفقیت انجام می‌گیرد.

نظرات مطالب
افزودن یک DataType جدید برای نگه‌داری تاریخ خورشیدی - 3
[Column(TypeName = "xml")]  
public string XmlValue { get; set; }  
  
[NotMapped]  
public XElement XmlValueWrapper  
{  
    get { return XElement.Parse(XmlValue); }  
    set { XmlValue = value.ToString(); }  
}

روش عمومی کار با نوع‌های خاصی که در EF تعریف نشدن، استفاده از ویژگی Column و مشخص کردن Type آن است؛ مانند مثالی که در بالا ملاحظه می‌کنید. البته این نوع خاص، در سمت کدها باید به صورت رشته تعریف شود. مثلا از سال 2005 به این طرف فیلد XML به SQL Server اضافه شده. اما نمی‌شود ازش در EF به همون شکل XML استفاده کرد. باید تبدیلش کنی به String تا قابل استفاده بشه. یک نمونه دیگرش نوع خاص Spatial هست که در نگارش‌های اخیر SQL Server اضافه شده (geography و geometry). این مورد فقط از EF 5.0 به بعد پشتیبانی توکاری ازش ارائه شده. یا برای hierarchyID در EF معادلی وجود نداره. برای تعریف این مورد نیز در یک مدل باید از string استفاده کرد.

بعد اگر این نوع خاص (که الان به صورت رشته دریافت شده) قابل نگاشت به نوعی مشخص در سمت کدهای برنامه بود (یعنی صرفا یک رشته ساده نبود) مثلا می‌شود از ویژگی NotMapped برای تبدیل آن و تعریف آن به شکل یک فیلد محاسباتی استفاده کرد.

بازخوردهای دوره
تزریق وابستگی‌های AutoMapper در لایه سرویس برنامه
ممنون از شما،
یک سوال: بنده کلاس ObjectFactory را همانطور که فرمودید به  این  صورت تغییر دادم. در لایه سرویس نیز این متد را تهیه کرده‌ام:
public IList<AdvertismentViewModel> GetAdvertisementsByMe(int userId)
{
            var adsList = _advertisements.Where(x => x.UserId == userId).ToList();
            var adsViewModel = new List<AdvertismentViewModel>();
            _mappingEngine.Map(source: adsList, destination: adsViewModel);
            return adsViewModel;
}
در متد فوق کلاس Advertisment به کلاس زیر نگاشت داده شده است:
public class AdvertismentViewModel
    {
        public string Image { get; set; }
        public string Title { get; set; }
        public string ExpireDate { get; set; }
    }
اما با فراخوانی متد GetAdvertisementsByMe استثناء AutoMapperMappingException صادر می‌شود:
Missing type map configuration or unsupported mapping.

Mapping types:
Advertisement -> AdvertismentViewModel
Project.DomainClasses.Advertisement -> Project.Models.AdvertismentViewModel

Destination path:
List`1[0]

Source value:
System.Data.Entity.DynamicProxies.Advertisement_E82DFF273E08C95AA785F8F7A0D2B5ABC8E54C4566DFE1C8A92D8D3C447608AE