مطالب
ایجاد صفحات راهنما برای ASP.NET Web API
وقتی یک Web API می‌سازید بهتر است صفحات راهنمایی هم برای آن در نظر بگیرید، تا توسعه دهندگان بدانند چگونه باید سرویس شما را فراخوانی و استفاده کنند. گرچه می‌توانید مستندات را بصورت دستی ایجاد کنید، اما بهتر است تا جایی که ممکن است آنها را بصورت خودکار تولید نمایید.

بدین منظور فریم ورک ASP.NET Web API کتابخانه ای برای تولید خودکار صفحات راهنما در زمان اجرا (run-time) فراهم کرده است.


ایجاد صفحات راهنمای API

برای شروع ابتدا ابزار ASP.NET and Web Tools 2012.2 Update را نصب کنید. اگر از ویژوال استودیو 2013 استفاده می‌کنید این ابزار بصورت خودکار نصب شده است. این ابزار صفحات راهنما را به قالب پروژه‌های ASP.NET Web API اضافه می‌کند.

یک پروژه جدید از نوع ASP.NET MVC Application بسازید و قالب Web API را برای آن انتخاب کنید. این قالب پروژه کنترلری بنام ValuesController را بصورت خودکار برای شما ایجاد می‌کند. همچنین صفحات راهنمای API هم برای شما ساخته می‌شوند. تمام کد مربوط به صفحات راهنما در قسمت Areas قرار دارند.

اگر اپلیکیشن را اجرا کنید خواهید دید که صفحه اصلی لینکی به صفحه راهنمای API دارد. از صفحه اصلی، مسیر تقریبی Help/ خواهد بود.

این لینک شما را به یک صفحه خلاصه (summary) هدایت می‌کند.

نمای این صفحه در مسیر Areas/HelpPage/Views/Help/Index.cshtml قرار دارد. می‌توانید این نما را ویرایش کنید و مثلا قالب، عنوان، استایل‌ها و دیگر موارد را تغییر دهید.

بخش اصلی این صفحه متشکل از جدولی است که API‌‌ها را بر اساس کنترلر طبقه بندی می‌کند. مقادیر این جدول بصورت خودکار و توسط اینترفیس IApiExplorer تولید می‌شوند. در ادامه مقاله بیشتر درباره این اینترفیس صحبت خواهیم کرد. اگر کنترلر جدیدی به API خود اضافه کنید، این جدول بصورت خودکار در زمان اجرا بروز رسانی خواهد شد.

ستون "API" متد HTTP و آدرس نسبی را لیست می‌کند. ستون "Documentation" مستندات هر API را نمایش می‌دهد. مقادیر این ستون در ابتدا تنها placeholder-text است. در ادامه مقاله خواهید دید چگونه می‌توان از توضیحات XML برای تولید مستندات استفاده کرد.

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


افزودن صفحات راهنما به پروژه ای قدیمی

می توانید با استفاده از NuGet Package Manager صفحات راهنمای خود را به پروژه‌های قدیمی هم اضافه کنید. این گزینه مخصوصا هنگامی مفید است که با پروژه ای کار می‌کنید که قالب آن Web API نیست.

از منوی Tools گزینه‌های Library Package Manager, Package Manager Console را انتخاب کنید. در پنجره Package Manager Console فرمان زیر را وارد کنید.

Install-Package Microsoft.AspNet.WebApi.HelpPage
این پکیج اسمبلی‌های لازم برای صفحات راهنما را به پروژه اضافه می‌کند و نماهای MVC را در مسیر Areas/HelpPage می‌سازد. اضافه کردن لینکی به صفحات راهنما باید بصورت دستی انجام شود. برای اضافه کردن این لینک به یک نمای Razor از کدی مانند لیست زیر استفاده کنید.

@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)

همانطور که مشاهده می‌کنید مسیر نسبی صفحات راهنما "Help/" می‌باشد. همچنین اطمینان حاصل کنید که ناحیه‌ها (Areas) بدرستی رجیستر می‌شوند. فایل Global.asax را باز کنید و کد زیر را در صورتی که وجود ندارد اضافه کنید.
protected void Application_Start()
{
    // Add this code, if not present.
    AreaRegistration.RegisterAllAreas();

    // ...
}

افزودن مستندات API

بصورت پیش فرض صفحات راهنما از placeholder-text برای مستندات استفاده می‌کنند. می‌توانید برای ساختن مستندات از توضیحات XML استفاده کنید. برای فعال سازی این قابلیت فایل Areas/HelpPage/App_Start/HelpPageConfig.cs را باز کنید و خط زیر را از حالت کامنت درآورید:

config.SetDocumentationProvider(new XmlDocumentationProvider(
    HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));
حال روی نام پروژه کلیک راست کنید و Properties را انتخاب کنید. در پنجره باز شده قسمت Build را کلیک کنید.

زیر قسمت Output گزینه XML documentation file را تیک بزنید و در فیلد روبروی آن مقدار "App_Data/XmlDocument.xml" را وارد کنید.

حال کنترلر ValuesController را از مسیر Controllers/ValuesController.cs/ باز کنید و یک سری توضیحات XML به متدهای آن اضافه کنید. بعنوان مثال:

/// <summary>
/// Gets some very important data from the server.
/// </summary>
public IEnumerable<string> Get()
{
    return new string[] { "value1", "value2" };
}

/// <summary>
/// Looks up some data by ID.
/// </summary>
/// <param name="id">The ID of the data.</param>
public string Get(int id)
{
    return "value";
}

اپلیکیشن را مجددا اجرا کنید و به صفحات راهنما بروید. حالا مستندات API شما باید تولید شده و نمایش داده شوند.

صفحات راهنما مستندات شما را در زمان اجرا از توضیحات XML استخراج می‌کنند. دقت کنید که هنگام توزیع اپلیکیشن، فایل XML را هم منتشر کنید.


توضیحات تکمیلی

صفحات راهنما توسط کلاس ApiExplorer تولید می‌شوند، که جزئی از فریم ورک ASP.NET Web API است. به ازای هر API این کلاس یک ApiDescription دارد که توضیحات لازم را در بر می‌گیرد. در اینجا منظور از "API" ترکیبی از متدهای HTTP و مسیرهای نسبی است. بعنوان مثال لیست زیر تعدادی API را نمایش می‌دهد:

  • GET /api/products
  • {GET /api/products/{id
  • POST /api/products

اگر اکشن‌های کنترلر از متدهای متعددی پشتیبانی کنند، ApiExplorer هر متد را بعنوان یک API مجزا در نظر خواهد گرفت. برای مخفی کردن یک API از ApiExplorer کافی است خاصیت ApiExplorerSettings را به اکشن مورد نظر اضافه کنید و مقدار خاصیت IgnoreApi آن را به true تنظیم نمایید.

[ApiExplorerSettings(IgnoreApi=true)]
public HttpResponseMessage Get(int id) {  }

همچنین می‌توانید این خاصیت را به کنترلر‌ها اضافه کنید تا تمام کنترلر از ApiExplorer مخفی شود.

کلاس ApiExplorer متن مستندات را توسط اینترفیس IDocumentationProvider دریافت می‌کند. کد مربوطه در مسیر Areas/HelpPage/XmlDocumentation.cs/ قرار دارد. همانطور که گفته شد مقادیر مورد نظر از توضیحات XML استخراج می‌شوند. نکته جالب آنکه می‌توانید با پیاده سازی این اینترفیس مستندات خود را از منبع دیگری استخراج کنید. برای اینکار باید متد الحاقی SetDocumentationProvider را هم فراخوانی کنید، که در HelpPageConfigurationExtensions تعریف شده است.

کلاس ApiExplorer بصورت خودکار اینترفیس IDocumentationProvider را فراخوانی می‌کند تا مستندات API‌ها را دریافت کند. سپس مقادیر دریافت شده را در خاصیت Documentation ذخیره می‌کند. این خاصیت روی آبجکت‌های ApiDescription و ApiParameterDescription تعریف شده است.


مطالعه بیشتر

مسیرراه‌ها
ASP.NET MVC
              مطالب
              روش آپلود فایل‌ها به همراه اطلاعات یک مدل در برنامه‌های Blazor WASM 5x
              از زمان Blazor 5x، امکان آپلود فایل به صورت استاندارد به Blazor اضافه شده‌است که نمونه‌ی Blazor Server آن‌را پیشتر در مطلب «Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر» مطالعه کردید. در تکمیل آن، روش آپلود فایل‌ها در برنامه‌های WASM را نیز بررسی خواهیم کرد. این برنامه از نوع hosted است؛ یعنی توسط دستور dotnet new blazorwasm --hosted ایجاد شده‌است و به صورت خودکار دارای سه بخش Client، Server و Shared است.



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

              فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کرده‌ایم:
              using System.ComponentModel.DataAnnotations;
              
              namespace BlazorWasmUpload.Shared
              {
                  public class User
                  {
                      [Required]
                      public string Name { get; set; }
              
                      [Required]
                      [Range(18, 90)]
                      public int Age { get; set; }
                  }
              }

              ساختار کنترلر Web API دریافت کننده‌ی مدل برنامه

              در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
              namespace BlazorWasmUpload.Server.Controllers
              {
                  [ApiController]
                  [Route("api/[controller]/[action]")]
                  public class FilesController : ControllerBase
                  {
                      [HttpPost]
                      public async Task<IActionResult> CreateUser(
                          [FromForm] User userModel,
                          [FromForm] IList<IFormFile> inputFiles = null)
              یعنی در سمت Web API، قرار است اطلاعات مدل User و همچنین لیستی از فایل‌های آپلودی (احتمالی و اختیاری) را یکجا و در طی یک عملیات Post، دریافت کنیم. در اینجا نام پارامترهایی را هم که انتظار داریم، دقیقا userModel و inputFiles هستند. همچنین فایل‌های آپلودی باید بتوانند ساختار IFormFile استاندارد ASP.NET Core را تشکیل داده و به صورت خودکار به پارامترهای تعریف شده، bind شوند. به علاوه content-type مورد انتظار هم FromForm است.


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

              کدهای کامل سرویسی که می‌تواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
              using System.Collections.Generic;
              using System.Linq;
              using System.Net.Http;
              using System.Net.Http.Headers;
              using System.Text;
              using System.Text.Json;
              using System.Threading.Tasks;
              using Microsoft.AspNetCore.Components.Forms;
              
              namespace BlazorWasmUpload.Client.Services
              {
                  public interface IFilesManagerService
                  {
                      Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri,
                          IEnumerable<IBrowserFile> browserFiles,
                          string fileParameterName,
                          T model,
                          string modelParameterName);
                  }
              
                  public class FilesManagerService : IFilesManagerService
                  {
                      private readonly HttpClient _httpClient;
              
                      public FilesManagerService(HttpClient httpClient)
                      {
                          _httpClient = httpClient;
                      }
              
                      public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>(
                          string requestUri,
                          IEnumerable<IBrowserFile> browserFiles,
                          string fileParameterName,
                          T model,
                          string modelParameterName)
                      {
                          var requestContent = new MultipartFormDataContent();
                          requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");
              
                          if (browserFiles?.Any() == true)
                          {
                              foreach (var file in browserFiles)
                              {
                                  var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000);
                                  requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name);
                              }
                          }
              
                          requestContent.Add(
                              content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"),
                              name: modelParameterName);
              
                          var result = await _httpClient.PostAsync(requestUri, requestContent);
                          result.EnsureSuccessStatusCode();
                          return result;
                      }
                  }
              }
              توضیحات:
              - کامپوننت استاندارد InputFiles در Blazor Wasm، می‌تواند لیستی از IBrowserFile‌های انتخابی توسط کاربر را در اختیار ما قرار دهد.
              - fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.
              - model جنریک، برای نمونه وهله‌ای از شیء User است که به یک فرم Blazor متصل است.
              - modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.

              - در ادامه یک MultipartFormDataContent را تشکیل داده‌ایم. توسط این ساختار می‌توان فایل‌ها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContent‌ها را اضافه می‌کنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل می‌شوند. متد OpenReadStream به صورت پیش‌فرض فقط فایل‌هایی تا حجم 500 کیلوبایت را پردازش می‌کند و اگر فایلی حجیم‌تر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت می‌توان توسط پارامتر maxAllowedSize آن، این مقدار پیش‌فرض را تغییر داد.

              - در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شده‌است. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشته‌ی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشته‌ی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی می‌توان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال می‌شود؛ چون MultipartFormDataContent ویژه‌ای را داریم، model-binder پیش‌فرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.

              - تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده می‌کنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.

              - در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.

              پس از تهیه‌ی سرویس ویژه‌ی فوق که می‌تواند اطلاعات فایل‌ها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگی‌ها در فایل Program.cs برنامه‌ی کلاینت است:
              namespace BlazorWasmUpload.Client
              {
                  public class Program
                  {
                      public static async Task Main(string[] args)
                      {
                          var builder = WebAssemblyHostBuilder.CreateDefault(args);
                          // ...
              
                          builder.Services.AddScoped<IFilesManagerService, FilesManagerService>();
              
                          // ...
                      }
                  }
              }


              تکمیل فرم ارسال اطلاعات مدل و فایل‌های همراه آن در برنامه‌ی Blazor WASM

              در ادامه پس از تشکیل IFilesManagerService، نوبت به استفاده‌ی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر می‌دهیم:
              @code
              {
                  IReadOnlyList<IBrowserFile> SelectedFiles;
                  User UserModel = new User();
                  bool isProcessing;
                  string UploadErrorMessage;
              در اینجا فیلدهای مورد استفاده‌ی در فرم برنامه مشخص شده‌اند:
              - SelectedFiles همان لیست فایل‌های انتخابی توسط کاربر است.
              - UserModel شیءای است که به EditForm جاری متصل خواهد شد.
              - توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص می‌کنیم.
              - UploadErrorMessage، خطای احتمالی انتخاب فایل‌ها مانند «فقط تصاویر را انتخاب کنید» را تعریف می‌کند.

              بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل می‌دهیم:
              @page "/"
              
              @using System.IO
              @using BlazorWasmUpload.Shared
              @using BlazorWasmUpload.Client.Services
              
              @inject IFilesManagerService FilesManagerService
              
              <h3>Post a model with files</h3>
              
              <EditForm Model="UserModel" OnValidSubmit="CreateUserAsync">
                  <DataAnnotationsValidator />
                  <div>
                      <label>Name</label>
                      <InputText @bind-Value="UserModel.Name"></InputText>
                      <ValidationMessage For="()=>UserModel.Name"></ValidationMessage>
                  </div>
                  <div>
                      <label>Age</label>
                      <InputNumber @bind-Value="UserModel.Age"></InputNumber>
                      <ValidationMessage For="()=>UserModel.Age"></ValidationMessage>
                  </div>
                  <div>
                      <label>Photos</label>
                      <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" />
                      @if (!string.IsNullOrWhiteSpace(UploadErrorMessage))
                      {
                          <div>
                              @UploadErrorMessage
                          </div>
                      }
                      @if (SelectedFiles?.Count > 0)
                      {
                          <table>
                              <thead>
                                  <tr>
                                      <th>Name</th>
                                      <th>Size (bytes)</th>
                                      <th>Last Modified</th>
                                      <th>Type</th>
                                  </tr>
                              </thead>
                              <tbody>
                                  @foreach (var selectedFile in SelectedFiles)
                                  {
                                      <tr>
                                          <td>@selectedFile.Name</td>
                                          <td>@selectedFile.Size</td>
                                          <td>@selectedFile.LastModified</td>
                                          <td>@selectedFile.ContentType</td>
                                      </tr>
                                  }
                              </tbody>
                          </table>
                      }
                  </div>
                  <div>
                      <button disabled="@isProcessing">Create user</button>
                  </div>
              </EditForm>
              توضیحات:
              - UserModel که وهله‌ی از شیء اشتراکی User است، به EditForm متصل شده‌است.
              - سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت می‌کنیم.
              - InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر می‌کند. پس از انتخاب فایل‌ها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
                  private void OnInputFileChange(InputFileChangeEventArgs args)
                  {
                      var files = args.GetMultipleFiles(maximumFileCount: 15);
                      if (args.FileCount == 0 || files.Count == 0)
                      {
                          UploadErrorMessage = "Please select a file.";
                          return;
                      }
              
                      var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
                      if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
                      {
                          UploadErrorMessage = "Please select .jpg/.jpeg/.png files only.";
                          return;
                      }
              
                      SelectedFiles = files;
                      UploadErrorMessage = string.Empty;
                  }
              - در اینجا امضای متد رویداد گردان OnChange را مشاهده می‌کنید. توسط متد GetMultipleFiles می‌توان لیست فایل‌های انتخابی توسط کاربر را دریافت کرد. نیاز است پارامتر maximumFileCount آن‌را نیز تنظیم کنیم تا دقیقا مشخص شود چه تعداد فایلی مدنظر است؛ بیش از آن، یک استثناء را صادر می‌کند.
              - در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش می‌دهیم.
              - در پایان این متد، لیست فایل‌های دریافتی را به فیلد SelectedFiles انتساب می‌دهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.

              مرحله‌ی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
                  private async Task CreateUserAsync()
                  {
                      try
                      {
                          isProcessing = true;
                          await FilesManagerService.PostModelWithFilesAsync(
                                      requestUri: "api/Files/CreateUser",
                                      browserFiles: SelectedFiles,
                                      fileParameterName: "inputFiles",
                                      model: UserModel,
                                      modelParameterName: "userModel");
                          UserModel = new User();
                      }
                      finally
                      {
                          isProcessing = false;
                          SelectedFiles = null;
                      }
                  }
              - در اینجا زمانیکه isProcessing به true تنظیم می‌شود، دکمه‌ی ارسال اطلاعات، غیرفعال خواهد شد؛ تا از کلیک چندباره‌ی بر روی آن جلوگیری شود.
              - سپس روش استفاده‌ی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده می‌کنید که اطلاعات فایل‌ها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال می‌کند.
              - در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آماده‌ی دریافت اطلاعات بعدی می‌کنیم.


              تکمیل کنترلر Web API دریافت کننده‌ی مدل برنامه

              در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کننده‌ی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آن‌را مشاهده می‌کنید:
              using System.IO;
              using Microsoft.AspNetCore.Mvc;
              using BlazorWasmUpload.Shared;
              using Microsoft.AspNetCore.Hosting;
              using System.Threading.Tasks;
              using Microsoft.AspNetCore.Http;
              using System.Collections.Generic;
              using Microsoft.Extensions.Logging;
              using System.Text.Json;
              using BlazorWasmUpload.Server.Utils;
              using System.Linq;
              
              namespace BlazorWasmUpload.Server.Controllers
              {
                  [ApiController]
                  [Route("api/[controller]/[action]")]
                  public class FilesController : ControllerBase
                  {
                      private const int MaxBufferSize = 0x10000;
              
                      private readonly IWebHostEnvironment _webHostEnvironment;
                      private readonly ILogger<FilesController> _logger;
              
                      public FilesController(
                          IWebHostEnvironment webHostEnvironment,
                          ILogger<FilesController> logger)
                      {
                          _webHostEnvironment = webHostEnvironment;
                          _logger = logger;
                      }
              
                      [HttpPost]
                      public async Task<IActionResult> CreateUser(
                          //[FromForm] string userModel, // <-- this is the actual form of the posted model
                          [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
                          [FromForm] IList<IFormFile> inputFiles = null)
                      {
                          /*var user = JsonSerializer.Deserialize<User>(userModel);
                          _logger.LogInformation($"userModel.Name: {user.Name}");
                          _logger.LogInformation($"userModel.Age: {user.Age}");*/
              
                          _logger.LogInformation($"userModel.Name: {userModel.Name}");
                          _logger.LogInformation($"userModel.Age: {userModel.Age}");
              
                          var uploadsRootFolder = Path.Combine(_webHostEnvironment.WebRootPath, "Files");
                          if (!Directory.Exists(uploadsRootFolder))
                          {
                              Directory.CreateDirectory(uploadsRootFolder);
                          }
              
                          if (inputFiles?.Any() == true)
                          {
                              foreach (var file in inputFiles)
                              {
                                  if (file == null || file.Length == 0)
                                  {
                                      continue;
                                  }
              
                                  var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                                  using var fileStream = new FileStream(filePath,
                                                                          FileMode.Create,
                                                                          FileAccess.Write,
                                                                          FileShare.None,
                                                                          MaxBufferSize,
                                                                          useAsync: true);
                                  await file.CopyToAsync(fileStream);
                                  _logger.LogInformation($"Saved file: {filePath}");
                              }
                          }
              
                          return Ok();
                      }
                  }
              }
              نکات تکمیلی این کنترلر را در مطلب «بررسی روش آپلود فایل‌ها در ASP.NET Core» می‌توانید مطالعه کنید و از این لحاظ هیچ نکته‌ی جدیدی را به همراه ندارد؛ بجز پارامتر userModel آن:
              [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
              همانطور که عنوان شد، userModel ارسالی به سمت سرور چون به همراه تعدادی فایل است، به صورت خودکار به شیء User نگاشت نخواهد شد. به همین جهت نیاز است model-binder سفارشی زیر را برای آن تهیه کرد:
              using System;
              using System.Text.Json;
              using System.Threading.Tasks;
              using Microsoft.AspNetCore.Mvc.ModelBinding;
              
              namespace BlazorWasmUpload.Server.Utils
              {
                  public class JsonModelBinder : IModelBinder
                  {
                      public Task BindModelAsync(ModelBindingContext bindingContext)
                      {
                          if (bindingContext == null)
                          {
                              throw new ArgumentNullException(nameof(bindingContext));
                          }
              
                          var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
                          if (valueProviderResult != ValueProviderResult.None)
                          {
                              bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
              
                              var valueAsString = valueProviderResult.FirstValue;
                              var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType);
                              if (result != null)
                              {
                                  bindingContext.Result = ModelBindingResult.Success(result);
                                  return Task.CompletedTask;
                              }
                          }
              
                          return Task.CompletedTask;
                      }
                  }
              }
              در اینجا مقدار رشته‌ای پارامتر مزین شده‌ی توسط JsonModelBinder فوق، توسط متد استاندارد JsonSerializer.Deserialize تبدیل به یک شیء شده و به آن پارامتر انتساب داده می‌شود. اگر نخواهیم از این model-binder سفارشی استفاده کنیم، ابتدا باید پارامتر دریافتی را رشته‌ای تعریف کنیم و سپس خودمان کار فراخوانی متد JsonSerializer.Deserialize را انجام دهیم:
              [HttpPost]
              public async Task<IActionResult> CreateUser(
                          [FromForm] string userModel, // <-- this is the actual form of the posted model
                          [FromForm] IList<IFormFile> inputFiles = null)
              {
                var user = JsonSerializer.Deserialize<User>(userModel);


              یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمی‌شود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه می‌شود و این API استاندارد، هنوز از streaming پشتیبانی نمی‌کند . حتی ممکن است با کمی جستجو به راه‌حل‌هایی که سعی کرده‌اند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راه‌حل‌ها تنها کاری را که انجام می‌دهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده می‌شود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.



              کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmUpload.zip
              مطالب
              بررسی تغییرات Blazor 8x - قسمت هفتم - امکان تعریف جزیره‌های تعاملی Blazor WASM
              در قسمت‌های قبل، نحوه‌ی تعریف جزیره‌های تعاملی Blazor Server را به همراه نکات مرتبط با آن‌ها بررسی کردیم. برای مثال مشاهده کردیم که چون Blazor Server و SSR هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی دارند و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت. در Blazor 8x، امکان تعریف جزیره‌های تعاملی Blazor WASM نیز وجود دارد که به همراه تعدادی نکته‌ی ویژه، در مورد نحوه‌ی مدیریت سرویس‌های مورد استفاده‌ی در این کامپوننت‌ها است.


              معرفی برنامه‌ی Blazor WASM این مطلب

              در این مطلب قصد داریم دقیقا قسمت جزیره‌ی تعاملی Blazor Server همان برنامه‌ی مطلب قبل را توسط یک جزیره‌ی تعاملی Blazor WASM بازنویسی کنیم و با نکات و تفاوت‌های ویژه‌ی آن آشنا شویم. یعنی زمانیکه صفحه‌ی SSR نمایش جزئیات یک محصول ظاهر می‌شود، نحوه‌ی رندر و پردازش کامپوننت نمایش محصولات مرتبط و مشابه، اینبار یک جزیره‌ی تعاملی Blazor WASM باشد. بنابراین قسمت عمده‌ای از کدهای این دو قسمت یکی است؛ فقط نحوه‌ی دسترسی به سرویس‌ها و محل قرارگیری تعدادی از فایل‌ها، متفاوت خواهد بود.


              ایجاد یک پروژه‌ی جدید Blazor WASM تعاملی در دات نت 8

              بنابراین در ادامه، در ابتدای کار نیاز است یک پوشه‌ی جدید را برای این پروژه، ایجاد کرده و بجای انتخاب interactivity از نوع Server:
              dotnet new blazor --interactivity Server
              اینبار برای اجرای در مرورگر توسط فناوری وب‌اسمبلی، نوع WebAssembly را انتخاب کنیم:
              dotnet new blazor --interactivity WebAssembly
              در این حالت، Solution ای که ایجاد می‌شود، به همراه دو پروژه‌‌است (برخلاف پروژه‌های Blazor Server تعاملی که فقط شامل یک پروژه‌ی سمت سرور هستند):
              الف) یک پروژه‌ی سمت سرور (برای تامین backend و API و سرویس‌های مرتبط)
              ب) یک پروژه‌ی سمت کلاینت (برای اجرای مستقیم درون مرورگر کاربر؛ بدون داشتن وابستگی مستقیمی به اجزای برنامه‌ی سمت سرور)

              این ساختار، خیلی شبیه به ساختار پروژه‌های نگارش قبلی Blazor از نوع Hosted Blazor WASM است که در آن، یک پروژه‌ی ASP.NET Core هاست کننده‌ی پروژه‌ی Blazor WASM وجود دارد و یکی از کارهای اصلی آن، فراهم ساختن Web API مورد استفاده‌ی در پروژه‌ی WASM است.

              در حالتیکه نوع تعاملی بودن پروژه را Server انتخاب کنیم (مانند مثال قسمت پنجم)، فایل Program.cs آن به همراه دو تعریف مهم زیر است که امکان تعریف کامپوننت‌های تعاملی سمت سرور را میسر می‌کنند:
              // ...
              
              builder.Services.AddRazorComponents()
                     .AddInteractiveServerComponents();
              
              // ...
              
              app.MapRazorComponents<App>()
                 .AddInteractiveServerRenderMode();
              مهم‌ترین قسمت‌های آن، متدهای AddInteractiveServerComponents و AddInteractiveServerRenderMode هستند که server-side rendering را به همراه امکان داشتن کامپوننت‌های تعاملی، ممکن می‌کنند.

              این تعاریف در فایل Program.cs (پروژه‌ی سمت سرور) قالب جدید Blazor WASM به صورت زیر تغییر می‌کنند تا امکان تعریف کامپوننت‌های تعاملی سمت کلاینت از نوع وب‌اسمبلی، میسر شود:
              // ...
              
              builder.Services.AddRazorComponents()
                  .AddInteractiveWebAssemblyComponents();
              
              // ...
              
              app.MapRazorComponents<App>()
                  .AddInteractiveWebAssemblyRenderMode()
                  .AddAdditionalAssemblies(typeof(Counter).Assembly);

              نیاز به تغییر معماری برنامه جهت کار با جزایر Blazor WASM

              همانطور که در قسمت پنجم مشاهده کردیم، تبدیل کردن یک کامپوننت Blazor، به کامپوننتی تعاملی برای اجرای در سمت سرور، بسیار ساده‌است؛ فقط کافی است rendermode@ آن‌را به InteractiveServer تغییر دهیم تا ... کار کند. اما تبدیل همان کامپوننت نمایش محصولات مرتبط، به یک جزیره‌ی وب‌اسمبلی، نیاز به تغییرات قابل ملاحظه‌ای را دارد؛ از این لحاظ که اینبار این قسمت قرار است بر روی مرورگر کاربر اجرا شود و نه بر روی سرور. در این حالت دیگر کامپوننت ما دسترسی مستقیمی را به سرویس‌های سمت سرور ندارد و برای رسیدن به این مقصود باید از یک Web API در سمت سرور کمک بگیرد و برای کار کردن با آن API در سمت کلاینت، از سرویس HttpClient استفاده کند. به همین جهت، پیاده سازی معماری این روش، نیاز به کار بیشتری را دارد:


              همانطور که ملاحظه می‌کنید، برای فعالسازی یک جزیره‌ی تعاملی وب‌اسمبلی، نمی‌توان کامپوننت RelatedProducts آن‌را مستقیما در پروژه‌ی سمت سرور قرار داد و باید آن‌را به پروژه‌ی سمت کلاینت منتقل کرد. در ادامه پیاده سازی کامل این پروژه را با توجه به این تغییرات بررسی می‌کنیم.


              مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول

              از این جهت که مدل برنامه (که در قسمت پنجم معرفی شد) در دو پروژه‌ی Client و سرور قابل استفاده‌است، به همین جهت مرسوم است یک پروژه‌ی سوم Shared را نیز به جمع دو پروژه‌ی جاری solution اضافه کرد و فایل این مدل را در آن قرار داد. بنابراین این فایل را از پوشه‌ی Models پروژه‌ی سرور به پوشه‌ی Models پروژه‌ی جدید BlazorDemoApp.Shared در مسیر جدید BlazorDemoApp.Shared\Models\Product.cs منتقل می‌کنیم. مابقی کدهای آن با قسمت پنجم تفاوتی ندارد.
              سپس به فایل csproj. پروژه‌ی کلاینت مراجعه کرده و ارجاعی را به پروژه‌ی جدید BlazorDemoApp.Shared اضافه می‌کنیم:
              <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
              
                <PropertyGroup>
                  <TargetFramework>net8.0</TargetFramework>
                </PropertyGroup>
              
                <ItemGroup>
                  <ProjectReference Include="..\BlazorDemoApp.Shared\BlazorDemoApp.Shared.csproj" />
                </ItemGroup>
              
              </Project>
              نیازی نیست تا اینکار را برای پروژه‌ی سرور نیز تکرار کنیم؛ از این جهت که ارجاعی به پروژه‌ی کلاینت، در پروژه‌ی سرور وجود دارد که سبب دسترسی به این پروژه‌ی Shared هم می‌شود.


              سرویس برنامه: سرویسی برای بازگشت لیست محصولات

              چون Blazor Server و صفحات SSR آن هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی وجود داشته و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت. یعنی هنوز هم همان مسیر قبلی سرویس Services\ProductStore.cs در این پروژه‌ی سمت سرور نیز برقرار است و نیازی به تغییر مسیر آن نیست. البته بدیهی است چون این پروژه جدید است، باید این سرویس را در فایل Program.cs برنامه‌ی سمت سرور به صورت زیر معرفی کرد تا در فایل razor برنامه‌ی آن قابل دسترسی شود:
              builder.Services.AddScoped<IProductStore, ProductStore>();


              تکمیل فایل Imports.razor_ پروژه‌ی سمت سرور

              جهت سهولت کار با برنامه، یک سری مسیر و using را نیاز است به فایل Imports.razor_ پروژه‌ی سمت سرور اضافه کرد:
              @using static Microsoft.AspNetCore.Components.Web.RenderMode
              // ...
              @using BlazorDemoApp.Client.Components.Store
              @using BlazorDemoApp.Client.Components
              سطر اول سبب می‌شود تا بتوان به سادگی به اعضای کلاس استاتیک RenderMode، در برنامه‌ی سمت سرور دسترسی یافت. دو using جدید دیگر سبب سهولت دسترسی به کامپوننت‌های قرارگرفته‌ی در این مسیرها در صفحات SSR برنامه‌ی سمت سرور می‌شوند.


              تکمیل صفحه‌ی نمایش لیست محصولات

              کدها و مسیر کامپوننت ProductsList.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحه‌ی SSR بوده و در همان سمت سرور اجرا می‌شود و دسترسی آن به سرویس‌های سمت سرور نیز ساده بوده و همانند قبل است.


              تکمیل صفحه‌ی نمایش جزئیات یک محصول

              کدها و مسیر کامپوننت ProductDetails.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحه‌ی SSR بوده و در همان سمت سرور اجرا می‌شود و دسترسی آن به سرویس‌های سمت سرور نیز ساده بوده و همانند قبل است ... البته بجز یک تغییر کوچک:
              <RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
              در اینجا حالت رندر این کامپوننت، به InteractiveWebAssembly تغییر می‌کند. یعنی اینبار قرار است تبدیل به یک جزیره‌ی وب‌اسمبلی شود و نه یک جزیره‌ی Blazor Server که آن‌را در قسمت پنجم بررسی کردیم.


              تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط

              پس از این توضیحات، به اصل موضوع این قسمت رسیدیم! کامپوننت سمت سرور RelatedProducts.razor قسمت پنجم ، از آنجا cut شده و به مسیر جدید BlazorDemoApp.Client\Components\Store\RelatedProducts.razor منتقل می‌شود. یعنی کاملا به پروژه‌ی وب‌اسمبلی منتقل می‌شود. بنابراین کدهای آن دیگر دسترسی مستقیمی به سرویس دریافت اطلاعات محصولات ندارند و برای اینکار نیاز است در سمت سرور، یک Web API Controller را تدارک ببینیم:
              using BlazorDemoApp.Services;
              using Microsoft.AspNetCore.Mvc;
              
              namespace BlazorDemoApp.Controllers;
              
              [ApiController]
              [Route("/api/[controller]")]
              public class ProductsController : ControllerBase
              {
                  private readonly IProductStore _store;
              
                  public ProductsController(IProductStore store) => _store = store;
              
                  [HttpGet("[action]")]
                  public IActionResult Related([FromQuery] int productId) => Ok(_store.GetRelatedProducts(productId));
              }
              این کلاس در مسیر Controllers\ProductsController.cs پروژه‌ی سمت سرور قرار می‌گیرد و کار آن، بازگشت اطلاعات محصولات مشابه یک محصول مشخص است.
              برای اینکه مسیریابی این کنترلر کار کند، باید به فایل Program.cs برنامه، مراجعه و سطرهای زیر را اضافه کرد:
              builder.Services.AddControllers();
              // ...
              app.MapControllers();

              یک نکته: همانطور که مشاهده می‌کنید، در Blazor 8x، امکان استفاده از دو نوع مسیریابی یکپارچه، در یک پروژه وجود دارد؛ یعنی Blazor routing  و  ASP.NET Core endpoint routing. بنابراین در این پروژه‌ی سمت سرور، هم می‌توان صفحات SSR و یا Blazor Server ای داشت که مسیریابی آن‌ها با page@ مشخص می‌شوند و همزمان کنترلرهای Web API ای را داشت که بر اساس سیستم مسیریابی ASP.NET Core کار می‌کنند.

              بر این اساس در پروژه‌ی سمت کلاینت، کامپوننت RelatedProducts.razor باید با استفاده از سرویس HttpClient، اطلاعات درخواستی را از Web API فوق دریافت و همانند قبل نمایش دهد که تغییرات آن به صورت زیر است:

              @using BlazorDemoApp.Shared.Models
              @inject HttpClient Http
              
              <button class="btn btn-outline-secondary" @onclick="LoadRelatedProducts">Related products</button>
              
              @if (_loadRelatedProducts)
              {
                  @if (_relatedProducts == null)
                  {
                      <p>Loading...</p>
                  }
                  else
                  {
                      <div class="mt-3">
                          @foreach (var item in _relatedProducts)
                          {
                              <a href="/ProductDetails/@item.Id">
                                  <div class="col-sm">
                                      <h5 class="mt-0">@item.Title (@item.Price.ToString("C"))</h5>
                                  </div>
                              </a>
                          }
                      </div>
                  }
              }
              
              @code{
              
                  private IList<Product>? _relatedProducts;
                  private bool _loadRelatedProducts;
              
                  [Parameter]
                  public int ProductId { get; set; }
              
                  private async Task LoadRelatedProducts()
                  {
                      _loadRelatedProducts = true;
                      var uri = $"/api/products/related?productId={ProductId}";
                      _relatedProducts = await Http.GetFromJsonAsync<IList<Product>>(uri);
                  }
              
              }
              و ... همین! اکنون برنامه قابل اجرا است و به محض نمایش صفحه‌ی جزئیات یک محصول انتخابی، کامپوننت RelatedProducts، در حالت وب‌اسمبلی جزیره‌ای اجرا شده و لیست این محصولات مرتبط را نمایش می‌دهد.
              در ادامه یکبار برنامه را اجرا می‌کنیم و ... بلافاصله پس از انتخاب صفحه‌ی نمایش جزئیات یک محصول، با خطای زیر مواجه خواهیم شد!
              System.InvalidOperationException: Cannot provide a value for property 'Http' on type 'RelatedProducts'.
              There is no registered service of type 'System.Net.Http.HttpClient'.


              اهمیت درنظر داشتن pre-rendering در حالت جزیره‌های وب‌اسمبلی

              استثنائی را که مشاهده می‌کنید، به علت pre-rendering سمت سرور این کامپوننت، رخ‌داده‌است.
              زمانیکه کامپوننتی را به این نحو رندر می‌کنیم:
              <RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
              به صورت پیش‌فرض در آن pre-rendering نیز فعال است؛ یعنی این کامپوننت دوبار رندر می‌شود:
              الف) یکبار در سمت سرور تا HTML حداقل قالب آن، به همراه سایر قسمت‌های صفحه‌ی SSR جاری به سمت مرورگر کاربر ارسال شود.
              ب) یکبار هم در سمت کلاینت، زمانیکه Blazor WASM بارگذاری شده و فعال می‌شود.

              استثنائی را که مشاهده می‌کنیم، مربوط به حالت الف است. یعنی زمانیکه برنامه‌ی ASP.NET Core هاست برنامه، سعی می‌کند کامپوننت RelatedProducts را در سمت سرور رندر کند، اما ... ما سرویس HttpClient را در آن ثبت و فعالسازی نکرده‌ایم. به همین جهت است که عنوان می‌کند این سرویس را پیدا نکرده‌است. برای رفع این مشکل، چندین راه‌حل وجود دارند که در ادامه آن‌ها را بررسی می‌کنیم.


              راه‌حل اول: ثبت سرویس HttpClient در سمت سرور

              یک راه‌حل مواجه شدن با مشکل فوق، ثبت سرویس HttpClient در فایل Program.cs برنامه‌ی سمت سرور به صورت زیر است:
              builder.Services.AddScoped(sp => new HttpClient 
              { 
                  BaseAddress = new Uri("http://localhost/") 
              });
              پس از این تعریف، کامپوننت RelatedProducts، در حالت prerendering ابتدایی سمت سرور هم کار می‌کند و برنامه با استثنائی مواجه نخواهد شد.


              راه‌حل دوم: استفاده از polymorphism یا چندریختی

              برای اینکار اینترفیسی را طراحی می‌کنیم که قرارداد نحوه‌ی تامین اطلاعات مورد نیاز کامپوننت RelatedProducts را ارائه می‌کند. سپس یک پیاده سازی سمت سرور را از آن خواهیم داشت که مستقیما به بانک اطلاعاتی رجوع می‌کند و همچنین یک پیاده سازی سمت کلاینت را که از HttpClient جهت کار با Web API استفاده خواهد کرد.
              از آنجائیکه این قرارداد نیاز است توسط هر دو پروژه‌ی سمت سرور و سمت کلاینت استفاده شود، باید آن‌را در پروژه‌ی Shared قرار داد تا بتوان ارجاعاتی از آن‌را به هر دو پروژه اضافه کرد؛ برای مثال در فایل BlazorDemoApp.Shared\Data\IProductStore.cs به صورت زیر:
              using BlazorDemoApp.Shared.Models;
              
              namespace BlazorDemoApp.Shared.Data;
              
              public interface IProductStore
              {
                  IList<Product> GetAllProducts();
                  Product GetProduct(int id);
                  Task<IList<Product>?> GetRelatedProducts(int productId);
              }
              این همان اینترفیسی است که پیشتر در فایل ProductStore.cs سمت سرور تعریف کرده بودیم؛ با یک تفاوت: متد GetRelatedProducts آن async تعریف شده‌است که نمونه‌ی سمت کلاینت آن باید با متد GetFromJsonAsync کار کند که async است.
              پیاده سازی سمت سرور این اینترفیس، کاملا مهیا است و فقط نیاز به تغییر زیر را دارد تا با خروجی Task دار هماهنگ شود:
              public Task<IList<Product>?> GetRelatedProducts(int productId)
                  {
                      var product = ProductsDataSource.Single(x => x.Id == productId);
                      return Task.FromResult<IList<Product>?>(ProductsDataSource.Where(p => product.Related.Contains(p.Id))
                                                                               .ToList());
                  }
              و اکشن متد متناظر هم باید به صورت زیر await دار شود تا خروجی صحیحی را ارائه دهد:
              [HttpGet("[action]")]
              public async Task<IActionResult> Related([FromQuery] int productId) =>
                      Ok(await _store.GetRelatedProducts(productId));
              همچنین پیشتر سرویس آن در فایل Program.cs برنامه‌ی سمت سرور، ثبت شده‌است و نیاز به نکته‌ی خاصی ندارد.

              در ادامه نیاز است یک پیاده سازی سمت کلاینت را نیز از آن تهیه کنیم که در فایل BlazorDemoApp.Client\Data\ClientProductStore.cs درج خواهد شد:
              public class ClientProductStore : IProductStore
              {
                  private readonly HttpClient _httpClient;
              
                  public ClientProductStore(HttpClient httpClient) => _httpClient = httpClient;
              
                  public IList<Product> GetAllProducts() => throw new NotImplementedException();
              
                  public Product GetProduct(int id) => throw new NotImplementedException();
              
                  public Task<IList<Product>?> GetRelatedProducts(int productId) =>
                      _httpClient.GetFromJsonAsync<IList<Product>>($"/api/products/related?productId={productId}");
              }
              در این بین بر اساس نیاز کامپوننت نمایش لیست محصولات مشابه، فقط به متد GetRelatedProducts نیاز داریم؛ بنابراین فقط همین مورد در اینجا پیاده سازی شده‌است. پس از این تعریف، نیاز است سرویس فوق را در فایل Program.cs برنامه‌ی کلاینت هم ثبت کرد (به همراه سرویس HttpClient ای که در سازنده‌ی آن تزریق می‌شود):
              builder.Services.AddScoped<IProductStore, ClientProductStore>();
              builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
              به این ترتیب این سرویس در کامپوننت RelatedProducts قابل دسترسی می‌شود و جایگزین سرویس HttpClient تزریقی قبلی خواهد شد. به همین جهت به فایل کامپوننت ProductStore مراجعه کرده و فقط 2 سطر آن‌را تغییر می‌دهیم:
              الف) معرفی سرویس IProductStore بجای HttpClient قبلی
              @inject IProductStore ProductStore
              ب) استفاده از متد GetRelatedProducts این سرویس:
              private async Task LoadRelatedProducts()
              {
                 _loadRelatedProducts = true;
                 _relatedProducts = await ProductStore.GetRelatedProducts(ProductId);
              }
              مابقی قسمت‌های این کامپوننت یکی است و تفاوتی با قبل ندارد.

              اکنون اگر برنامه را اجرا کنیم، پس از مشاهده‌ی جزئیات یک محصول، بارگذاری کامپوننت Blazor WASM آن در developer tools مرورگر کاملا مشخص است:



              راه‌حل سوم: استفاده از سرویس PersistentComponentState

              با استفاده از سرویس PersistentComponentState می‌توان اطلاعات دریافتی از بانک‌اطلاعاتی را در حین pre-rendering در سمت سرور، به جزایر تعاملی انتقال داد و این روشی است که مایکروسافت برای پیاده سازی مباحث اعتبارسنجی و احراز هویت در Blazor 8x در پیش‌گرفته‌است. این راه‌حل را در قسمت بعد بررسی می‌کنیم.


              کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-WebAssembly-Normal.zip
              مطالب
              Http Batch Processing در Asp.Net Web Api
              بعد از معرفی نسخه‌ی 2 از Asp.Net Web Api و  پشتیبانی رسمی آن از OData بسیاری از توسعه دهندگان سیستم نفس راحتی کشیدند؛ زیرا از آن پس می‌توانستند علاوه بر امکانات جالب و مهمی که تحت پروتکل OData میسر بود، از سایر امکانات تعبیه شده در نسخه‌ی دوم web Api نیز استفاده نمایند. یکی از این قابلیت‌ها، مبحث مهم Batching Processing است که در طی این پست با آن آشنا خواهیم شد.
              منظور از Batch Request این است که درخواست دهنده بتواند چندین درخواست (Multiple Http Request) را به صورت یک Pack جامع، در قالب فقط یک درخواست (Single Http Request) ارسال نماید و به همین روال تمام پاسخ‌های معادل درخواست ارسال شده را به صورت یک Pack دیگر دریافت کرده و آن را پردازش نماید. نوع درخواست نیز مهم نیست یعنی می‌توان در قالب یک Pack چندین درخواست از نوع Post و Get یا حتی Put و ... نیز داشته باشید.  بدیهی است که پیاده سازی این قابلیت در جای مناسب و در پروژه‌هایی با تعداد کاربران زیاد می‌تواند باعث بهبود چشمگیر کارآیی پروژه شود.

              برای شروع همانند سایر مطالب می‌توانید از این پست جهت راه اندازی هاست سرویس‌های Web Api استفاده نمایید. برای فعال سازی قابلیت batching Request نیاز به یک MessageHandler داریم تا بتوانند درخواست‌هایی از این نوع را پردازش نمایند. خوشبختانه به صورت پیش فرض این Handler پیاده سازی شده‌است و ما فقط باید آن را با استفاده از متد MapHttpBatchRoute به بخش مسیر یابی (Route Handler) پروژه معرفی نماییم.
              public class Startup
                  {
                      public void Configuration(IAppBuilder appBuilder)
                      {
                          var config = new HttpConfiguration();           
                        
                          config.Routes.MapHttpBatchRoute(
                              routeName: "Batch",
                              routeTemplate: "api/$batch",
                              batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer));
              
                          config.MapHttpAttributeRoutes();
              
                          config.Routes.MapHttpRoute(
                              name: "Default",
                              routeTemplate: "{controller}/{action}/{name}",
                              defaults: new { name = RouteParameter.Optional }
                          );
              
                          config.Formatters.Clear();
                          config.Formatters.Add(new JsonMediaTypeFormatter());
                          config.Formatters.JsonFormatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
                          config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
              
                          config.EnsureInitialized();          
                          appBuilder.UseWebApi(config);
                      }
                  }

              مهم‌ترین نکته‌ی آن استفاده از DefaultHttpBatchHandler و معرفی آن به بخش batchHandler مسیریابی است. کلاس DefaultHttpBatchHandler  برای وهله سازی نیاز به آبجکت سروری که سرویس‌های WebApi در آن هاست شده‌اند دارد که با دستور GlobalConfiguration.DefaultServer به آن دسترسی خواهید داشت. در صورتی که HttpServer خاص خود را دارید به صورت زیر عمل نمایید:
               var config = new HttpConfiguration();
               HttpServer server = new HttpServer(config);
              تنظیمات بخش سرور به اتمام رسید. حال نیاز داریم بخش کلاینت را طوری طراحی نماییم که بتواند درخواست را به صورت دسته‌ای ارسال نماید. در زیر یک مثال قرار داده شده است:
              using System.Net.Http;
              using System.Net.Http.Formatting;
              
              public class Program
                  {
                      private static void Main(string[] args)
                      {
                          string baseAddress = "http://localhost:8080";
                          var client = new HttpClient();
                          var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch")
                          {
                              Content = new MultipartContent("mixed")
                              {                   
                                  new HttpMessageContent(new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/Book/Add")
                                  {
                                      Content = new ObjectContent<string>("myBook", new JsonMediaTypeFormatter())
                                  }),                   
                                  new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/Book/GetAll"))
                              }
                          };
              
                          var batchResponse = client.SendAsync(batchRequest).Result;
              
                          MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result;
                          foreach (var content in streamProvider.Contents)
                          {
                              var response = content.ReadAsHttpResponseMessageAsync().Result;                
                          }
                      }
                  }
              همان طور که می‌دانیم برای ارسال درخواست به سرویس Web Api باید یک نمونه از کلاس HttpRequestMessage وهله سازی شود سازنده‌ی آن به نوع HttpMethod اکشن نظیر (POST یا GET) و آدرس سرویس مورد نظر نیاز دارد. نکته‌ی مهم آن این است که خاصیت Content این درخواست باید از نوع MultipartContent و subType آن نیز باید mixed باشد. در بدنه‌ی آن نیز می‌توان تمام درخواست‌ها را به ترتیب و با استفاده از وهله سازی از کلاس HttpMessageContent تعریف کرد.
              برای دریافت پاسخ این گونه درخواست‌ها نیز از متد الحاقی ReadAsMultipartAsync استفاده می‌شود که امکان پیمایش بر بدنه‌ی پیام دریافتی را می‌دهد.


              مدیریت ترتیب درخواست ها

              شاید این سوال به ذهن شما نیز خطور کرده باشد که ترتیب پردازش این گونه پیام‌ها چگونه خواهد بود؟ به صورت پیش فرض ترتیب اجرای درخواست‌ها حائز اهمیت است. بعنی تا زمانیکه پردازش درخواست اول به اتمام نرسد، کنترل اجرای برنامه، به درخواست بعدی نخواهد رسید که این مورد بیشتر زمانی رخ می‌دهد که قصد دریافت اطلاعاتی را داشته باشید که قبل از آن باید عمل Persist در پایگاه داده اتفاق بیافتد. اما در حالاتی غیر از این می‌توانید این گزینه را غیر فعال کرده تا تمام درخواست‌ها به صورت موازی پردازش شوند که به طور قطع کارایی آن نسبت به حالت قبلی بهینه‌تر است.
              برای غیر فعال کردن گزینه‌ی ترتیب اجرای درخواست‌ها، به صورت زیر عمل نمایید:
               config.Routes.MapHttpBatchRoute(
                              routeName: "WebApiBatch",
                              routeTemplate: "api/$batch",
                              batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer)
                              {
                                  ExecutionOrder = BatchExecutionOrder.NonSequential
                              });
              تفاوت آن فقط در مقدار دهی خاصیت ExecutionOrder به صورت NonSequential است.
              اشتراک‌ها
              توسعه ASP.NET Core Web Api CRUD در دات نت 7

              .NET 7 💥 - ASP.NET Core Web Api CRUD, Repository Pattern, SQLite & Automapper
              In this video we will be going to be create a full .NET WebApi  with Automapper, SQLite and utilising the Repository Pattern
               

              توسعه ASP.NET Core Web Api CRUD در دات نت 7
              اشتراک‌ها
              کتابخانه‌ی JSONAPI.NET

              JSONAPI.NET is a set of utility classes that aim to make it possible to implement JSON API spec compliant RESTful web services quickly and easily using ASP.NET MVC WebAPI. 

              کتابخانه‌ی JSONAPI.NET
              نظرات مطالب
              EF Code First #6
              در مورد ASP.NET Web API و UpShot و اینها که از EF 4.3 تو خودشون استفاده کردند چی ؟
              متاسفانه دیگه نمی‌شه با assembly binding بشون بگیم که از EF 5 استفاده کنید
              چون Runtime خطا می‌دن و مثلا می‌گن که System.ComponentMode.DataAnnotation.ForeignKey در Entity Framework 5 نیست !
              بله نیست، چون رفته به DLL مربوط به Component Model.Data Annotation
              راه حلی جز گرفتن سورس کد upshot و Build مجددش هست ؟
              و غیر از عقب گرد به EF 3
              ممنون