ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هشت دقیقه

در ASP.NET Core، برخلاف نگارش‌های قبلی ASP.NET که ASP.NET Web API مجزای از ASP.NET MVC و همچنین وب فرم‌ها ارائه شده بود، اکنون جزئی از ASP.NET MVC است و با آن یکپارچه می‌باشد. بنابراین پیشنیازهای راه اندازی Web API با ASP.NET Core شامل سه مورد ذیل هستند که پیشتر آن‌ها را بررسی کردیم:
الف) فعال سازی ارائه‌ی فایل‌های استاتیک
ب) فعال سازی ASP.NET MVC
ج) آشنایی با تغییرات مسیریابی

و مابقی آن صرفا یک سری نکات تکمیلی هستند که در ادامه آن‌ها را بررسی خواهیم کرد.


تعریف مسیریابی کلی کنترلر

در اینجا همانند مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی»، می‌توان در صورت نیاز، مسیریابی کلی کنترلر را توسط ویژگی Route بازنویسی کرد و برای مثال درخواست‌های آن‌را محدود به درخواست‌هایی کرد که با api/ شروع شوند:
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
[controller] هم در اینجا یک توکن پیش فرض است که با نام کنترلر جاری یا همان Test، به صورت خودکار جایگزین می‌شود.
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging» بحث کردیم و از آن می‌توان برای ثبت استثناءهای رخ داده استفاده کرد.


یک کنترلر ، اما با قابلیت‌های متعدد

همانطور که ملاحظه می‌کنید، اینبار کلاس پایه‌ی این کنترلر Test، همان Controller متداول ASP.NET MVC ذکر شده‌است و نه Api Controller سابق. تمام قابلیت‌های موجود در این‌دو توسط همان Controller ارائه می‌شوند.


هنوز پیش فرض‌های سابق Web API برقرار هستند

در مثال ذیل که به نظر یک کنترلر ASP.NET MVC است،
- هنوز متد Get مربوط به Web API که به صورت پیش فرض به درخواست‌های Get ختم شده‌ی به نام کنترلر پاسخ می‌دهد، برقرار است (متد IEnumerable<string> Get). برای مثال اگر شخصی در مرورگر، آدرس http://localhost:7742/api/test را درخواست دهد، متد Get اجرا می‌شود.
- در اینجا می‌توان نوع خروجی متد را دقیقا از همان نوع اشیاء مدنظر، تعیین کرد؛ برای نمونه تعریف  <IEnumerable<string در مثال زیر.
- مهم نیست که از return Json استفاده کنید و یا خروجی را مستقیما با فرمت <IEnumerable<string ارائه دهید.
- اگر نیاز به کنترل بیشتری بر روی HTTP Response Status بازگشتی داشتید، می‌توانید از متدهایی مانند return Ok و یا return BadRequest در صورت بروز مشکلی استفاده نمائید. برای مثال در متد IActionResult GetEpisodes2، استثنای فرضی حاصل، ابتدا توسط سرویس ثبت وقایع ذخیره شده و در آخر یک BadRequest بازگشت داده می‌شود.
- تمام مسیریابی‌ها را توسط ویژگی Route و یا نوع‌های درخواستی مانند HttpGet، می‌توان بازنویسی کرد؛ مانند مسیر /api/path1
- امکان محدود ساختن نوع پارامترهای دریافتی همانند متد Get(int page) ذیل، توسط ویژگی‌های مسیریابی وجود دارد.
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
 
    [HttpGet]
    public IEnumerable<string> Get() // http://localhost:7742/api/test
    {
        return new [] { "value1", "value2" };
    }
 
    [HttpGet("{page:int}")]
    public IActionResult Get(int page) // http://localhost:7742/api/test/1
    {
        return Json(new[] { "value3", "value4" });
    }
 
    [HttpGet("/api/path1")]
    public IActionResult GetEpisodes1() // http://localhost:7742/api/path1
    {
        return Json(new[] { "value5", "value6" });
    }
 
    [HttpGet("/api/path2")]
    public IActionResult GetEpisodes2() // http://localhost:7742/api/path2
    {
        try
        {
            // get data from the DB ...
            return Ok(new[] { "value7", "value8" });
        }
        catch (Exception ex)
        {
            _logger.LogError("Failed to get data from the API", ex);
            return BadRequest();
        }
    } 
}
بنابراین در اینجا اگر می‌خواهید یک کنترلر ASP.NET Web API 2.x را به ASP.NET Core 1.0 ارتقاء دهید، تمام متدهای Get و Put و امثال آن هنوز معتبر هستند و مانند سابق عمل می‌کنند:
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
// GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }
// POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }
// PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }
// DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
در مورد ویژگی FromBody در ادامه بیشتر بحث خواهد شد.

یک نکته: اگر می‌خواهید خروجی Web API شما همواره JSON باشد، می‌توانید ویژگی جدید Produces را به شکل ذیل به کلاس کنترلر اعمال کنید:
 [Produces("application/json")]
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller


تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API

فرض کنید مدل زیر را به برنامه اضافه کرده‌اید:
namespace Core1RtmEmptyTest.Models
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}
و همچنین قصد دارید اطلاعات آن‌را از کاربر توسط یک عملیات POST دریافت کرده و به شکل JSON نمایش دهید:
using Core1RtmEmptyTest.Models;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class PersonController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        public IActionResult Index(Person person)
        {
            return Json(person);
        }
    }
}
برای اینکار، از jQuery به نحو ذیل استفاده می‌کنیم (از این جهت که بیشتر ارسال‌های به سرور جهت کار با Web API نیز Ajax ایی هستند):
@section scripts
{
    <script type="text/javascript">
        $(function () {
            $.ajax({
                type: 'POST',
                url: '/Person/Index',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify({
                    FirstName: 'F1',
                    LastName: 'L1',
                    Age: 23
                }),
                success: function (result) {
                    console.log('Data received: ');
                    console.log(result);
                }
            });
        });
    </script>
}


همانطور که مشاهده می‌کنید، اگر در ابتدای این متد یک break-point قرار دهیم، اطلاعاتی را از سمت کاربر دریافت نکرده‌است و مقادیر دریافتی نال هستند.
این مورد یکی از مهم‌ترین تغییرات Model binding این نگارش از ASP.NET MVC با نگارش‌های قبلی آن است. در اینجا اشیاء پیچیده از request body دریافت و bind نمی‌شوند و باید به نحو ذیل، محل دریافت و تفسیر آن‌ها را دقیقا مشخص کرد:
 public IActionResult Index([FromBody]Person person)
زمانیکه ویژگی FromBody را مشخص می‌کنیم، آنگاه اطلاعات دریافتی از request body دریافتی، به شیء Person نگاشت خواهند شد.


نکته‌ی مهم: حتی اگر FromBody را ذکر کنید ولی از JSON.stringify در سمت کاربر استفاده نکنید، باز هم نال دریافت خواهید کرد. بنابراین در این نگارش ذکر JSON.stringify نیز الزامی است.


حالت‌های دیگر تغییرات Model Binding در ASP.NET Core

تا اینجا مشخص شد که اگر یک درخواست Ajax ایی را به سمت سرور یک برنامه‌ی ASP.NET Core ارسال کنیم، به صورت پیش فرض به اشیاء پیچیده‌ی سمت سرور bind نمی‌شود و باید حتما ویژگی FromBody را نیز مشخص کرد تا اطلاعات را از request body واکشی کند (محل دریافت اطلاعات پیش فرض آن نامشخص است).
یک سؤال: اگر به سمت یک چنین اکشن متدی، اطلاعات فرمی را به حالت معمول ارسال کنیم، چه اتفاقی رخ خواهد داد؟
ارسال اطلاعات فرم‌ها به سرور، همواره شامل دو تغییر ذیل است:
  var dataType = 'application/x-www-form-urlencoded; charset=utf-8';
 var data = $('form').serialize();
اطلاعات فرم سریالایز می‌شوند و data type مخصوصی هم برای آن‌ها تنظیم خواهد شد. در این حالت، ارسال یک چنین اطلاعاتی به سمت اکشن متد فوق، با خطای 415 unsupported media type متوقف می‌شود. برای رفع این مشکل باید از ویژگی دیگری به نام FromForm استفاده کرد:
 [HttpPost]
public IActionResult Index([FromForm]Person person)
حالت‌های دیگر ممکن را در تصویر ذیل ملاحظه می‌کنید:


علت این مساله نیز بالا رفتن میزان امنیت سیستم است. در نگارش‌های قبلی، تمام مکان‌ها و حالت‌های میسر جستجو می‌شوند و اگر یکی از آن‌ها قابلیت تطابق با خواص شیء مدنظر را داشته باشد، کار binding به پایان می‌رسد. اما در اینجا با مشخص شدن محل دقیق منبع اطلاعات، دیگر سایر حالات جستجو نشده و سطح حمله کاهش پیدا می‌کند.
در اینجا باید مشخص کرد که دقیقا اطلاعاتی که قرار است به یک شیء پیچیده Bind شوند، آیا از یک Form تامین می‌شوند، یا از Body و یا از هدر، کوئری استرینگ، مسیریابی و یا حتی از یک سرویس.
تمام این حالت‌ها مشخص هستند (برای مثال دریافت اطلاعات از هدر درخواست HTTP و انتساب آن‌ها به خواص متناظری در شیء مشخص شده)، منهای FromService آن که به نحو ذیل عمل می‌کند:
در این حالت می‌توان در سازنده‌ی کلاس مدل خود، سرویسی را تزریق کرد و توسط آن خاصیتی را مقدار دهی نمود:
public class ProductModel
{
    public ProductModel(IProductService prodService)
    {
        Value = prodService.Get(productId);
    }
    public IProduct Value { get; private set; }
}
این تزریق وابستگی‌ها برای اینکه تکمیل شود، نیاز به ویژگی FromServices خواهد داشت:
 public async Task<IActionResult> GetProduct([FromServices]ProductModel product)
{
}
وجود ویژگی FromServices به این معنا است که سرویس‌های مدل یاد شده را از تنظیمات ابتدایی IoC Container خود خوانده و سپس در اختیار مدل جاری قرار بده. به این ترتیب حتی تزریق وابستگی‌ها در مدل‌های برنامه هم میسر می‌شود.


تغییر تنظیمات اولیه‌ی خروجی‌های ASP.NET Web API

در اینجا حالت ارائه‌ی خروجی XML به صورت پیش فرض فعال نیست. اگر علاقمند به افزودن آن نیز باشید، نحوه‌ی کار را در متد ConfigureServices کلاس آغازین برنامه در کدهای ذیل مشاهده می‌کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); 
 
    }).AddJsonOptions(options =>
    {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;
        options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
    });
همچنین اگر خواستید تنظیمات ابتدایی JSON.NET را تغییر داده و برای مثال خروجی JSON تولیدی را camel case کنید، این‌کار را توسط متد AddJsonOptions به نحو فوق می‌توان انجام داد.
  • #
    ‫۸ سال و ۱ ماه قبل، چهارشنبه ۱۳ مرداد ۱۳۹۵، ساعت ۱۶:۱۶
    نکته‌ای در مورد « تغییر تنظیمات اولیه‌ی خروجی‌های ASP.NET Web API » انتهای بحث
    حالت پیش فرض خروجی ASP.NET Core 1.0 RTM دقیقا همان camel case است. اگر می‌خواهید آن‌را به Pascal Case تغییر دهید، می‌توانید به صورت زیر عمل کنید:
    services.AddMvc().AddJsonOptions(opt =>
    {
        var resolver = opt.SerializerSettings.ContractResolver;
        if (resolver != null)
        {
           var res = resolver as DefaultContractResolver;
           res.NamingStrategy = null; // this is what removes the camelcasing
        }
    });
  • #
    ‫۷ سال و ۱۰ ماه قبل، سه‌شنبه ۱۸ آبان ۱۳۹۵، ساعت ۱۸:۲۳
    در مثال ProductModel شیوه دریافت متغیر productId ذکر نشده، که به عنوان مثال به روش زیر می‌توان مقدار متغیر  productId را دریافت کرد:
    public class ProductModel
    {
        public ProductModel(IContextAccessor<ActionContext> action, IProductService prodService)
        {
            //TODO: Do some error checking...
            var productId = action.Value.RouteData.Values["product"];
            Value = prodService.Get(productId);
        }
    
        public IProduct Value { get; private set; }
    }
  • #
    ‫۷ سال و ۱ ماه قبل، یکشنبه ۲۹ مرداد ۱۳۹۶، ساعت ۰۲:۵۲
    سلام
    آیا از قابلیت FromService و تزریق سرویسها در سازنده مدل‌های برنامه فقط زمانی میتونیم استفاده کنیم که مدل‌ها و سرویس‌ها در یک پروژه باشن ؟
    چون در غیر این صورت معمولا لایه سرویس ما لایه مدل رو به عنوان رفرنس اضافه میکنه و مجددا برعکس این حالت یعنی لایه مدل به سرویس غیر ممکن میشه.
  • #
    ‫۷ سال قبل، پنجشنبه ۹ شهریور ۱۳۹۶، ساعت ۲۳:۴۶
    نکته‌ای در مورد تک پارامترها در ASP.NET Core

    اگر اکشن متدی تنها دارای یک پارامتر باشد:
    public Task<IActionResult> RefreshToken(string refreshToken)
    و این اطلاعات از طریق Ajax و با فرمت زیر ارسال شوند:
    {
        "refreshToken": "test"
    }
    مقدار دریافتی همواره نال خواهد بود. در این حالت تک پارامتری، اطلاعات را باید با فرمت "test" ارسال کرد تا کار کند (فقط ارسال مقدار نهایی، تا FromBody درست کار کند).

    - یک روش دیگر حل این مشکل، استفاده از JToken به عنوان نوع پارامتر است (و حالتیکه اطلاعات با فرمت JSON ارسال می‌شوند):
    public Task<IActionResult> RefreshToken([FromBody]JToken jsonBody)
    {
       var refreshToken = jsonBody.Value<string>("refreshToken");

    - روش دیگر آن، تعریف پارامتر از نوع یک کلاس است که در این حالت از مزایای اعتبارسنجی به همراه ویژگی‌ها نیز می‌توان استفاده کرد:
    public class EntityData
    {
        public string RefreshToken { get; set; }
    }
  • #
    ‫۶ سال و ۱۰ ماه قبل، جمعه ۵ آبان ۱۳۹۶، ساعت ۱۶:۲۴
    یک نکته‌ی تکمیلی: چگونه فقط Web API را در ASP.NET Core بدون اضافات MVC آن داشته باشیم؟

    زمانیکه در کلاس آغازین برنامه، متد AddMvc را فراخوانی می‌کنیم، به همراه آن AddViews، AddRazorViewEngine، AddRazorPages، AddCacheTagHelper و غیره نیز به صورت خودکار به برنامه اضافه می‌شوند و وجود آن‌ها در آخر به معنای یکی بودن MVC و Web API با هم است. اما اگر برنامه‌ی ما فقط یک برنامه‌ی SPA، یا تک صفحه‌ای وب باشد، عملا به هیچکدام از این قابلیت‌ها نیازی نیست.
    برای اینکه فقط Web API را فعال کنیم، باید از متد الحاقی دیگری به نام AddMvcCore استفاده کرد (البته در نگارش 3x این مورد با AddControllers ساده‌تر شده‌است). کار آن فعالسازی routing, attributes, filters, result executors, model binders, controllers است. برای دسترسی به آن تنها نصب سه بسته‌ی ذیل کفایت می‌کنند:
    Microsoft.AspNetCore.Mvc.Core
    Microsoft.AspNetCore.Mvc.Cors
    Microsoft.AspNetCore.Mvc.Formatters.Json
    در این حالت نحوه‌ی استفاده‌ی از MvcCore تنها، به صورت ذیل است؛ بدون به همراه داشتن ویژگی‌های MVC، مانند کار با Views و TagHelpers:
    namespace AspNetCoreWebAPIOnly 
    { 
        public class Startup 
        { 
            public void ConfigureServices(IServiceCollection services) 
            { 
                var mvcCoreBuilder = services.AddMvcCore(); 
                mvcCoreBuilder 
                    .AddFormatterMappings() 
                    .AddJsonFormatters() 
                    .AddCors(); 
            } 
    
            public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
            { 
                app.UseMvc(); 
            } 
        } 
    }

    نکته‌ی مهم:
    در این حالت چون قسمت‌های پردازش View مربوط به MVC حذف می‌شوند، نمی‌توان از کلاس پایه Controller جهت تعریف کنترلرها استفاده کرد؛ چون این کلاس پایه، بازگشت Viewها و Partial Viewها و غیره را نیز شامل می‌شود. اما همین کلاس پایه Controller از کلاس عمومی‌تری به نام ControllerBase مشتق می‌شود که به معنای پشتیبانی از Web API، بدون Viewها است. بنابراین نحوه‌ی تعریف کنترلرها اینبار به صورت ذیل خواهد بود (مشتق شده‌ی از ControllerBase):
    namespace AspNetCoreWebAPIOnly.Controllers 
    { 
        [Route("api/[controller]")] 
        // ControllerBase instead of Controller 
        public class ValuesController : ControllerBase 
        { 
            // GET api/values 
            [HttpGet] 
            public IEnumerable<string> Get() 
            { 
                return new[] { "value1", "value2" }; 
            } 
        } 
    }
    • #
      ‫۶ سال و ۱۰ ماه قبل، شنبه ۶ آبان ۱۳۹۶، ساعت ۱۱:۲۷
      این نکته روی زمان پاسخگویی و همچنین حجم publish نهایی تاثیر گذار هست؟
    • #
      ‫۱۱ ماه قبل، دوشنبه ۳ مهر ۱۴۰۲، ساعت ۱۷:۵۸
      یک نکته تکمیلی
      حین استفاده از ControllerBase میتوانیم از یک تعداد helper برای خروجی اکشن‌ها استفاده کنیم؛ به عنوان مثال میتوانیم بعد از ساخت یک resource جدید از CreatedAtAction استفاده کنیم که به صورت خودکار خروجی 201 را برمیگرداند و همچنین هدر Location را نیز برایمان تنظیم خواهد کرد؛ 
      [ApiController]
      [Route("api/users")]
      public class UserController : ControllerBase
      {
          [HttpGet("{id:int}")]
          public ActionResult GetUserById(int id)
          {
              var user = new { Id = 1, Name = "Sirwan Afifi" };
              return Ok(user);
          }
      
          [HttpPost]
          public ActionResult CreateUser(CreateUserDto user)
          {
              return CreatedAtAction(nameof(GetUserById), new { user.Id }, user);
          }
      }

      اطلاعات بیشتر (+)

  • #
    ‫۶ سال و ۷ ماه قبل، یکشنبه ۱۵ بهمن ۱۳۹۶، ساعت ۱۷:۲۳
    ارتقاء به ASP.NET Core 2.1

    اگر Controller شما صرفا کار Web API را انجام می‌دهد، می‌توانید آن‌را به ویژگی جدید ApiController مزین کنید:
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
    پس از این تغییر، این ویژگی‌ها را به صورت خودکار و بدون کد نویسی اضافه‌تری دریافت خواهید کرد:
    الف) دیگر نیازی نیست تا ModelState.IsValid را بررسی کنید و در صورت معتبر نبودن آن برای مثال BadRequest(ModelState) را بازگشت دهید. این‌کار به صورت خودکار، در صورت بروز شکست اعتبارسنجی، انجام خواهد شد.
    ب) دیگر نیازی به ذکر صریح ویژگی‌های FromBody ،FromRoute و FromQuery برای model binding به پارامترهای یک اکشن متد ندارید و در این زمینه به صورت هوشمند‌تری عمل می‌شود.

    البته باید دقت داشت که این ویژگی تنها با attribute routing کار می‌کند و نه حالت convention-based routes.
    • #
      ‫۵ سال و ۳ ماه قبل، چهارشنبه ۸ خرداد ۱۳۹۸، ساعت ۱۸:۴۱
      یک نکته‌ی تکمیلی: 
       در صورتی که قصد داشته باشیم نتایج حاصل از خطاهای اعتبارسنجی را به صورت سفارشی شده  مثلا  ValidationProblemDetails   به کلاینت بازگشت دهیم می‌توان به صورت زیر عمل کرد:
      services.AddMvc()
          .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
          .ConfigureApiBehaviorOptions(options =>
          {
              options.InvalidModelStateResponseFactory = context =>
              {
                  var problemDetails = new ValidationProblemDetails(context.ModelState)
                  {
                      Type = "https://contoso.com/probs/modelvalidation",
                      Title = "One or more model validation errors occurred.",
                      Status = StatusCodes.Status400BadRequest,
                      Detail = "See the errors property for details.",
                      Instance = context.HttpContext.Request.Path
                  };
      
                  return new BadRequestObjectResult(problemDetails)
                  {
                      ContentTypes = { "application/problem+json" }
                  };
              };
          });

  • #
    ‫۶ سال و ۶ ماه قبل، پنجشنبه ۱۰ اسفند ۱۳۹۶، ساعت ۱۷:۴۸
    ارتقاء به ASP.NET Core 2.1: بهبود اعتبارسنجی پارامترها

    تا پیش از نگارش 2.1، برای اعمال اعتبارسنجی به اطلاعات دریافتی از کاربر باید به صورت زیر عمل کرد:
    public class UserModel   
    {
       [Required, EmailAddress]
       public string Email { get; set; }
     
       [Required, StringLength(1000)]
       public string Name { get; set; }
    }
    اطلاعات مدنظر به صورت یک کلاس مدل تعریف شده و سپس ویژگی‌های اعتبارسنجی به خواص این کلاس اضافه می‌شوند.
    در این حالت در اکشن متد تعریفی با بررسی ModelState.IsValid می‌توان وضعیت اعتبارسنجی اطلاعات دریافتی از سمت کاربر را مشاهده کرد:
    public IActionResult SaveUser(UserModel model)
    {
         if(!ModelState.IsValid)
         {

     در نگارش 2.1 الزامی به تعریف این کلاس مدل نیست و ویژگی‌های اعتبارسنجی را به پارامترهای تعریف اکشن متد هم می‌توان اعمال کرد:
    public IActionResult SaveUser(
         [Required, EmailAddress] string Email  
         [Required, StringLength(1000)] string Name)
    {
        if(!ModelState.IsValid)

    یک نکته‌ی تکمیلی: اعمال ویژگی Required به non-nullable value types تاثیری ندارد. به همین جهت ویژگی دیگری به نام BindRequired نیز در اینجا اضافه شده‌است تا برای نمونه در مثال زیر اطمینان حاصل شود که testId از مقادیر route و qty از مقادیر کوئری استرینگ مقدار دهی شده‌اند:
    public IActionResult Get([BindRequired, FromRoute] Guid testId, [BindRequired, FromQuery] int qty)   
    {
       if(!ModelState.IsValid)

    - به این ترتیب می‌توان تعداد ViewModelهای مورد نیاز یک برنامه را به شدت کاهش داد. البته نکته‌ی «بررسی Bad code smell ها: تعداد زیاد پارامترهای ورودی» و «آشنایی با Refactoring - قسمت 7» را هم مدنظر داشته باشید و زیاده‌روی نکنید!
    - همچنین اگر ویژگی [ApiController] را نیز به کنترلر جاری اعمال کنید، بررسی ModelState.IsValid نیز به صورت خودکار انجام خواهد شد و نیازی به کدنویسی اضافه‌تری نخواهد داشت.
    • #
      ‫۵ سال و ۷ ماه قبل، چهارشنبه ۱۷ بهمن ۱۳۹۷، ساعت ۱۳:۵۳
      چرا با وجود اعمال این نکته تک پارامترها به صورت زیر Bind نمی‌شوند؟ 
      [HttpPost("[action]")]
      public bool Test([Required]string name)
        {
           return true;
        }

      در ضمن در کامنت‌های قبلی هم به نکته‌ای در مورد تک پارامترها در ASP.NET Core  اشاره کرده بودید ولی همواره مقدار دریافتی نال خواهد شد.
      • #
        ‫۵ سال و ۷ ماه قبل، چهارشنبه ۱۷ بهمن ۱۳۹۷، ساعت ۱۴:۴۴
        چون بر اساس اطلاعات header رسیده، اطلاعات دریافتی را به یک JSON Object تبدیل می‌کند که قابل انتساب به یک رشته نیست. به همین جهت برای مثال از JToken و یا یک کلاس سفارشی (که عنوان شد) می‌توانید برای دریافت اطلاعات یک شیء رسیده استفاده کنید.
  • #
    ‫۵ سال و ۷ ماه قبل، شنبه ۲۷ بهمن ۱۳۹۷، ساعت ۱۵:۳۷
    ارتقاء به ASP.NET Core 3.0
    در نگارش سوم، وابستگی مستقیم به JSON.NET در ASP.NET Core حذف شده‌است («معرفی System.Text.Json در NET Core 3.0.»). علت اینجا است که علاوه بر فراهم آوردن امکان استفاده‌ی از کتابخانه‌های JSON ثالث، نویسنده‌ی JSON.NET حاضر نشده API آن‌را بر اساس Span بازنویسی کند تا به حداکثر کارآیی برسد. بنابراین اگر می‌خواهید هنوز هم از JSON.NET استفاده کنید، نیاز است ابتدا بسته‌ی نیوگت Adapter مخصوص آن‌را جداگانه دریافت و سپس آن‌را به سیستم معرفی کنید:
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc()
            .AddNewtonsoftJson();
    }
    • #
      ‫۳ سال و ۱۰ ماه قبل، دوشنبه ۲۱ مهر ۱۳۹۹، ساعت ۲۱:۵۷
      با سلام
      چرا در نگارش 3 با تغییر به JSON.NET  همچنان  باز هم نال دریافت میکنم؟

      public void ConfigureServices(IServiceCollection services)
      {
          services.AddMvc()
              .AddNewtonsoftJson();
      }

      [Route("api/[controller]")]
          [Authorize]
          [ApiController]
          public class UserApiController : Controller
          {
              private readonly IUserService _userService;
      
              public UserApiController(IUserService userService)
              {
                  _userService = userService;
              }
      
      
              [HttpPost("[action]")]
              public async Task<IActionResult> GetCustomers([FromBody] CustomersFilterViewModel filter)
              {
                  var model = await _userService.GetCustomers(filter);
      return Ok(model); }
      const options = {headers: {'Content-Type': 'application/json'}};
      axios.post(url, JSON.stringify({ data}), options)

       
      • #
        ‫۳ سال و ۱۰ ماه قبل، دوشنبه ۲۱ مهر ۱۳۹۹، ساعت ۲۳:۱۳
        مثال post با axios در اینجا ارسال شده و نیازی به JSON.stringify و options ندارد؛ مثال پیوست شده‌ی انتهای بحث آن‌را آزمایش کنید.
  • #
    ‫۵ سال و ۴ ماه قبل، چهارشنبه ۱۸ اردیبهشت ۱۳۹۸، ساعت ۱۷:۰۶
    با توجه به مطالب مطرح شده در متن فوق و  نحوه استفاده از ViewModel در ASP.NET MVC و همچنین توصیه‌هایی که در رابطه با آدرس‌دهی صحیح WebApiها وجود دارد (استفاده از اسم جمع، استفاده از اسم به جای فعل و ...)، در رابطه با آدرس‌دهی صحیح برای تامین اطلاعات مورد نیاز Viewها  (در حالت ویرایش یا افزودن)، در سمت کلاینت که استفاده کننده آن می‌تواند یک کامپوننت Angular یا هر نوع دیگری باشد آیا دوستان نظر و Best Practice دارند؟
    به طور مثال برای ویرایش و افزودن یک محصول به صورت زیر عمل می‌کنیم:
       //ViewModels
    
        public class CustomListItem
        {
            public int Id { get; set; }
            public string Text { get; set; }
        }
    
        public class ProductAddGetViewModel
        {
            public IEnumerable<CustomListItem> Categories { get; set; }
            public IEnumerable<CustomListItem> Groups { get; set; }
        }
    
        public class ProductAddViewModel
        {
            public string Name { get; set; }
            public bool IsActive { get; set; }
    
            public int CategoryId { get; set; }
            public int GroupId { get; set; }
        }
    
        public class ProductEditGetViewModel
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public IEnumerable<CustomListItem> Categories { get; set; }
            public IEnumerable<CustomListItem> Groups { get; set; }
        }
    
        public class ProductEditViewModel
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public int CategoryId { get; set; }
            public int GroupId { get; set; }
        }
    و
        // ProductsController - ApiControler
    
        // GET: api/products/views/add
        [HttpGet("views/add")]
        public async Task<IActionResult> GetAdd()
        {
            ProductAddGetViewModel model = await _productService.GetAddModelAsync();
            return Ok(model)
        }
    
        // POST: api/products
        [HttpPost]
        public async Task<IActionResult> Add(ProductAddViewModel model)
        {
            ...
        }
    
    
        // GET: api/products/5/views/edit
        [HttpGet("{id}/views/edit")]
        public async Task<IActionResult> GetEdit(int id)
        {
            ProductEditGetViewModel model = await _productService.GetEditModelAsync(id);
            return Ok(model)
        }
    
        // PUT: api/products/5
        [HttpPut("{id}")]
        public async Task<IActionResult> Edit(int id, ProductEditViewModel model)
        {
            ...
        }
    با توجه به اینکه حالت فوق، احتمالاً دو متد از چند متد مورد استفاده می‌باشد، آیا دوستان درباره متدهای GetAdd , GetEdit و همچنین آدرس‌دهی صحیح این نوع متدها که قرار است از سمت کلاینت فراخوانی شود نظری دارند؟
    پ.ن: درباره نامگذاری بهتر ViewModelها هم اگر نظری هست ممنون میشم بیان شود.
  • #
    ‫۴ سال و ۸ ماه قبل، سه‌شنبه ۳ دی ۱۳۹۸، ساعت ۰۳:۵۹
    من یک api  طراحی کردم که کنترلر آن رو مزین کردم به خصوصیت ApiController .  این api یک پارامتر از نوع  int داره که با خصوصیت Required و پیام خطای مناسب تنظیم شده ، مشکلی که دارم در صورت null بودن این پارامتر خطای اعتبار سنجی سفارشی برگشت داده نمی‌شود و خطای  زیر صادر میشه :
    The JSON value could not be converted to System.Int32
    آیا باید برای مدیریت این حالت‌ها ، مبدل json مثل این مطلب  نوشت؟
    • #
      ‫۴ سال و ۸ ماه قبل، سه‌شنبه ۳ دی ۱۳۹۸، ساعت ۰۴:۳۸
      یا اعتبارسنجی سمت کلاینت برای فیلدی که اجباری هست.
  • #
    ‫۳ سال و ۹ ماه قبل، یکشنبه ۲۵ آبان ۱۳۹۹، ساعت ۱۷:۳۱
    تغییرات پردازش تاریخ از زمان ارائه‌ی ASP.NET Core 3.1

    از زمان ارائه‌ی کتابخانه‌ی JSON توکار در ASP.NET Core 3x، نحوه‌ی پردازش تاریخ‌های دریافتی در برنامه‌های ASP.NET Core نیز تغییر کرد و در حالت دریافت اطلاعات به صورت JSON در اکشن متدها، فقط بر اساس ISO 8601-1:2019 عمل می‌کند. یعنی اگر تاریخ را با فرمت متداول 2019/06/14 به سمت سرور ارسال کنید، خطای ModelState زیر را دریافت خواهید کرد:
    {
        "$.time": [
            "The JSON value could not be converted to System.DateTime. Path: $.time | LineNumber: 1 | BytePositionInLine: 23."
        ]
    }
    که عنوان می‌کند تاریخ ارسالی قابل پردازش نیست. چون فرمت دریافتی آن باید به این صورت باشد:
    2019-06-14T02:30:04.0576719Z
    و در کل این فرمت‌ها را قبول می‌کند:
    'Full date'
    
        "yyyy'-'MM'-'dd"
    
    "'Full date''T''Hour'':''Minute'"
    
        "yyyy'-'MM'-'dd'T'HH':'mm"
    
    "'Full date''T''Partial time'"
    
        "yyyy'-'MM'-'dd'T'HH':'mm':'ss" (The Sortable ("s") Format Specifier)
        "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFF"
    
    "'Full date''T''Time hour'':''Minute''Time offset'"
    
        "yyyy'-'MM'-'dd'T'HH':'mmZ"
        "yyyy'-'MM'-'dd'T'HH':'mm('+'/'-')HH':'mm"
    
    'Date time'
    
        "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
        "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFFZ"
        "yyyy'-'MM'-'dd'T'HH':'mm':'ss('+'/'-')HH':'mm"
        "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFF('+'/'-')HH':'mm"


    تغییرات پردازش تاریخ در ASP.NET Core 5x

    در نگارش‌های قبلی ASP.NET Core، اگر تاریخی از طریق کوئری استرینگ و با فرمت ISO فوق دریافت می‌شد، مانند:
    https://localhost:44393/time?time=2019-06-14T02:30:04.0576719Z
    زمانیکه در سمت سرور پردازش می‌شد، نوع آن به اشتباه به Local ترجمه می‌شد و UTC آن درنظر گرفته نمی‌شد:
    Model-bound value is: 2019-06-13T19:30:04.0576719-07:00, Kind is: Local
    این مشکل در ASP.NET Core 5x برطرف شده‌است و اینبار Kind تاریخ پردازش شده، از نوع UTC ارسالی است.

    یک نکته: ویژگی‌های BindProperty و DisplayFormat در razor pages، امکان تغییر فرمت ورودی و نمایشی را می‌دهند:
    [BindProperty, DisplayFormat(DataFormatString = "{0:yyyy-MM-ddTHH:mm}", ApplyFormatInEditMode = true)]
    public DateTime DateTime { get; set; }
    
    DateTime: <input asp-for="DateTime"  asp-format="{0:yyyy-MM-ddTHH:mm}" />
  • #
    ‫۲ سال و ۶ ماه قبل، دوشنبه ۲۵ بهمن ۱۴۰۰، ساعت ۲۰:۵۷
    ارتقاء به ASP.NET Core 7x

    تغییر غیرسازگاری با نگارش‌های قبلی، در ASP.NET Core 7x رخ خواهد که در آن ویژگی [FromServices] که در مطلب جاری بحث شد، پیش‌فرض شده‌است؛ یعنی حتی اگر آن‌را ذکر هم نکردید، مهم نیست و به صورت پیش‌فرض در بین سرویس‌های ثبت شده نیز به دنبال پارامتر اکشن متد مدنظر شما می‌گردد:
    Services.AddScoped<SomeCustomType>();
    
    [Route("[controller]")]
    [ApiController]
    public class MyController : ControllerBase
    {
        // Binding from the services
        [HttpPost]
        public ActionResult Post(SomeCustomType service) => Ok();
    }
    در این مثال، نوع SomeCustomType به صورت یک سرویس، در ابتدای برنامه ثبت شده‌است و همانطور که مشاهده می‌کنید، بدون نیاز به ذکر صریح ویژگی [FromServices]، در اکشن متد Post، مورد استفاده قرار گرفته‌است.
    اگر علاقمند به استفاده‌ی از این حالت پیش‌فرض نیستند، روش غیرفعال کردن آن به صورت زیر است:
    services.Configure<ApiBehaviorOptions>(options =>
    {
         options.DisableImplicitFromServicesParameters = true;
    });
    • #
      ‫۱۱ ماه قبل، جمعه ۳۱ شهریور ۱۴۰۲، ساعت ۲۰:۵۰
      در NET 8. نیز یک مفهومی اضافه شده تحت عنوان Keyed DI Services؛ با کمک این قابلیت میتوانیم سرویس‌ها را توسط یک کلید ثبت یا استفاده کنیم؛ برای استفاده از یک سرویس که با کلید هم ذخیره شده میتوانیم از پراپرتی FromKeyedServices استفاده کنیم:
      using Microsoft.Extensions.Caching.Memory;
      using Microsoft.Extensions.Options;
      
      var builder = WebApplication.CreateBuilder(args);
      
      builder.Services.AddSingleton<BigCacheConsumer>();
      builder.Services.AddSingleton<SmallCacheConsumer>();
      
      builder.Services.AddKeyedSingleton<IMemoryCache, BigCache>("big");
      builder.Services.AddKeyedSingleton<IMemoryCache, SmallCache>("small");
      
      var app = builder.Build();
      
      app.MapGet("/big", (BigCacheConsumer data) => data.GetData());
      app.MapGet("/small", (SmallCacheConsumer data) => data.GetData());
      
      app.Run();
      
      class BigCacheConsumer([FromKeyedServices("big")] IMemoryCache cache)
      {
          public object? GetData() => cache.Get("data");
      }
      
      class SmallCacheConsumer(IKeyedServiceProvider keyedServiceProvider)
      {
          public object? GetData() => keyedServiceProvider.GetRequiredKeyedService<IMemoryCache>("small");
      }

  • #
    ‫۱ سال و ۶ ماه قبل، دوشنبه ۱۰ بهمن ۱۴۰۱، ساعت ۱۷:۰۵
    یک نکته‌ی تکمیلی: ساده شدن نحوه‌ی پردازش پیام‌های خالی رسیده در دات نت 7

    تا پیش از دات نت 7، اگر درخواستی با یک بدنه‌ی خالی، یعنی با مشخصات Content-Length == 0 به سمت یک اکشن متد که آن‌را از طریق [FromBody] دریافت می‌کند، ارسال شود، با پیام خطای «A non-empty request body is required» خاتمه خواهد یافت. یک روش رفع سراسری آن، تنظیم زیر است:
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.Configure<MvcOptions>(options =>
    {
         options.AllowEmptyInputInBodyModelBinding = true;
    });
    و روش دیگر آن فقط برای یک اکشن متد خاص، به صورت زیر:
    public IActionResult Post([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] MyBody? body)
    {
       // body will be null if the request Content-Length == 0
    }
    در دات نت 7 این وضعیت ساده شده و بر اساس نال پذیری پارامتر دریافتی، در این مورد تصمیم گیری می‌شود:
    public class ExampleController : Controller
    {
        public IActionResult Post(MyBody? body) // Nullable
        {
            // body will be null if the request Content-Length == 0
        }
        
        public IActionResult Post(MyBody body) // Non-nullable
        {
            // Request will fail with a 400 and "A non-empty request body is required."
            // when Content-Length == 0
        }
    }
    یعنی اگر پارامتری نال‌پذیر بود، قابلیت پردازش یک بدنه‌ی درخواست خالی را به صورت نال دارد و برعکس.