اشتراکها
نظرات مطالب
دستیابی به HttpContext در Blazor Server
بحث Hosted WASM متفاوت است و رفتار سمت سرور آن دقیقا مانند یک برنامهی کامل ASP.NET Core Web API است.
یک نکتهی تکمیلی
به همراه NET Core 2.1.، یک HttpClientFactory توکار توسط مایکروسافت ارائه شدهاست:
به این ترتیب برای مثال جهت کار با یک آدرس مشخص، میتوان تنظیمات آنرا یکبار در آغاز برنامه ثبت کرد:
و بعد برای استفادهی سراسری از آن توسط سیستم ترزیق وابستگیها، میتوان به صورت زیر عمل کرد:
به همراه NET Core 2.1.، یک HttpClientFactory توکار توسط مایکروسافت ارائه شدهاست:
HttpClientFactory in ASP.NET Core 2.1 (Part 1) An Introduction to HttpClientFactory
HttpClientFactory in ASP.NET Core 2.1 (Part 2) Defining Named and Typed Clients
HttpClientFactory in ASP.NET Core 2.1 (Part 2) Defining Named and Typed Clients
به این ترتیب برای مثال جهت کار با یک آدرس مشخص، میتوان تنظیمات آنرا یکبار در آغاز برنامه ثبت کرد:
public void ConfigureServices(IServiceCollection services) { services.AddHttpClient("github", c => { c.BaseAddress = new Uri("https://api.github.com/"); c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent }); services.AddHttpClient(); }
IHttpClientFactory _httpClientFactory; public MyController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public IActionResult Index() { //This client doesn’t have any special configuration applied var defaultClient = _httpClientFactory.CreateClient(); //This client has the header and base address configured for the “github” client above. var gitHubClient = _httpClientFactory.CreateClient("github"); return View(); }
از زمان Blazor 5x، امکان آپلود فایل به صورت استاندارد به Blazor اضافه شدهاست که نمونهی Blazor Server آنرا پیشتر در مطلب «Blazor 5x - قسمت 17 - کار با فرمها - بخش 5 - آپلود تصاویر» مطالعه کردید. در تکمیل آن، روش آپلود فایلها در برنامههای WASM را نیز بررسی خواهیم کرد. این برنامه از نوع hosted است؛ یعنی توسط دستور dotnet new blazorwasm --hosted ایجاد شدهاست و به صورت خودکار دارای سه بخش Client، Server و Shared است.
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کردهایم:
ساختار کنترلر Web API دریافت کنندهی مدل برنامه
در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
یعنی در سمت Web API، قرار است اطلاعات مدل User و همچنین لیستی از فایلهای آپلودی (احتمالی و اختیاری) را یکجا و در طی یک عملیات Post، دریافت کنیم. در اینجا نام پارامترهایی را هم که انتظار داریم، دقیقا userModel و inputFiles هستند. همچنین فایلهای آپلودی باید بتوانند ساختار IFormFile استاندارد ASP.NET Core را تشکیل داده و به صورت خودکار به پارامترهای تعریف شده، bind شوند. به علاوه content-type مورد انتظار هم FromForm است.
ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایلهای انتخابی کاربر
کدهای کامل سرویسی که میتواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
توضیحات:
- کامپوننت استاندارد 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 برنامهی کلاینت است:
تکمیل فرم ارسال اطلاعات مدل و فایلهای همراه آن در برنامهی Blazor WASM
در ادامه پس از تشکیل IFilesManagerService، نوبت به استفادهی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر میدهیم:
در اینجا فیلدهای مورد استفادهی در فرم برنامه مشخص شدهاند:
- SelectedFiles همان لیست فایلهای انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص میکنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایلها مانند «فقط تصاویر را انتخاب کنید» را تعریف میکند.
بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل میدهیم:
توضیحات:
- UserModel که وهلهی از شیء اشتراکی User است، به EditForm متصل شدهاست.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت میکنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر میکند. پس از انتخاب فایلها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
- در اینجا امضای متد رویداد گردان OnChange را مشاهده میکنید. توسط متد GetMultipleFiles میتوان لیست فایلهای انتخابی توسط کاربر را دریافت کرد. نیاز است پارامتر maximumFileCount آنرا نیز تنظیم کنیم تا دقیقا مشخص شود چه تعداد فایلی مدنظر است؛ بیش از آن، یک استثناء را صادر میکند.
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش میدهیم.
- در پایان این متد، لیست فایلهای دریافتی را به فیلد SelectedFiles انتساب میدهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.
مرحلهی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
- در اینجا زمانیکه isProcessing به true تنظیم میشود، دکمهی ارسال اطلاعات، غیرفعال خواهد شد؛ تا از کلیک چندبارهی بر روی آن جلوگیری شود.
- سپس روش استفادهی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده میکنید که اطلاعات فایلها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال میکند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آمادهی دریافت اطلاعات بعدی میکنیم.
تکمیل کنترلر Web API دریافت کنندهی مدل برنامه
در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کنندهی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آنرا مشاهده میکنید:
نکات تکمیلی این کنترلر را در مطلب «بررسی روش آپلود فایلها در ASP.NET Core» میتوانید مطالعه کنید و از این لحاظ هیچ نکتهی جدیدی را به همراه ندارد؛ بجز پارامتر userModel آن:
همانطور که عنوان شد، userModel ارسالی به سمت سرور چون به همراه تعدادی فایل است، به صورت خودکار به شیء User نگاشت نخواهد شد. به همین جهت نیاز است model-binder سفارشی زیر را برای آن تهیه کرد:
در اینجا مقدار رشتهای پارامتر مزین شدهی توسط JsonModelBinder فوق، توسط متد استاندارد JsonSerializer.Deserialize تبدیل به یک شیء شده و به آن پارامتر انتساب داده میشود. اگر نخواهیم از این model-binder سفارشی استفاده کنیم، ابتدا باید پارامتر دریافتی را رشتهای تعریف کنیم و سپس خودمان کار فراخوانی متد JsonSerializer.Deserialize را انجام دهیم:
یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمیشود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه میشود و این API استاندارد، هنوز از streaming پشتیبانی نمیکند . حتی ممکن است با کمی جستجو به راهحلهایی که سعی کردهاند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راهحلها تنها کاری را که انجام میدهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده میشود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmUpload.zip
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت 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)
ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایلهای انتخابی کاربر
کدهای کامل سرویسی که میتواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
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; }
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی 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; } }
- سپس روش استفادهی از متد 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(); } } }
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
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; } } }
[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
اشتراکها
دوره کامل NET MAUI.
.NET MAUI Full Course : Cross Platform Development
.NET MAUI allows you to create multi-platform apps using one code base. That means you can develop for Android, iOS, macOS and of course Windows at the same time!
In this course I provide numerous examples on how to use all the tools MAUI provides. Check out the long table of contents below.
اشتراکها
سری ASP.NET Core Identity
از زمان ASP.NET Core 2.1، قابلیت جدیدی به نام Generic Host، به آن اضافه شدهاست که از آن میتوان برای انجام کارهای متداول پس زمینه، مانند ارسال ایمیلهای خبرنامهی یک برنامه، تهیه فایلهای پشتیبان و غیره استفاده کرد.
Generic Host چیست؟
Generic Host یکی از ویژگیهای جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای اینکار، امکان استفادهی از آن نه فقط در پروژههای وب، بلکه در پروژههای کنسول نیز میباشد. به این ترتیب میتوان کارهای غیر HTTP را از برنامهی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پسزمینهی خارج از اعمال web host جاری، ارائه دادهاند:
بنابراین برای ایجاد یک HostedService، نیاز است سرویس کارهای پسزمینهی ما، اینترفیس IHostedService را پیاده سازی کند. متد StartAsync آن جائیاست که تنها یکبار پس از آغاز برنامه اجرا میشود و هدف آن اجرای کار پسزمینهی مدنظر است. متد StopAsync نیز دقیقا پیش از خاتمهی برنامه فراخوانی خواهد شد تا اگر نیاز به پاکسازی منابعی وجود داشته باشد، بتوان از این فرصت استفاده کرد. به این ترتیب اگر نیاز به اجرای متناوب کار پسزمینهای وجود دارد، پیاده سازی آن به خود ما واگذار شدهاست.
یک مثال: معرفی کار پسزمینهای که هر دو ثانیه یکبار انجام میشود
در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ میشود و اگر برنامه را توسط دستور dotnet run اجرا کنید، میتوانید خروجی آنرا در کنسول، مشاهده کنید:
در ادامه برای معرفی این کار پسزمینه به سیستم به صورت یک سرویس با طول عمر Singleton خواهیم داشت:
روش دیگر انجام اینکار استفاده از متد الحاقی AddHostedService است:
مزیت اینکار این است که متد Configure واقع در کلاس Startup یک چنین امضایی را دارد:
و IHostingEnvironment هم در فضای نام Microsoft.AspNetCore.Hosting واقع شدهاست و هم در فضای نام Microsoft.Extensions.Hosting که IHostedService در آن قرار دارد. به همین جهت چون متد AddHostedService، تعریف IHostedService را مخفی میکند، خطای زمان کامپایلی را جهت مشخص سازی صریح فضای نام IHostingEnvironment دریافت نخواهید کرد:
مشکلات پیاده سازی کار پسزمینهی SampleHostedService فوق
هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
پس از اجرای دستور dotnet run، سرویس پس زمینه شروع به کار کردهاست. پس از مدتی کلیدهای Ctrl+C را فشردهایم تا این حلقهی بینهایت و برنامه خاتمه یابد. اینجا است که مشاهده میکنید تازه قسمت هاست برنامهی وب ما شروع به کار کردهاست؛ یعنی دقیقا زمانیکه پروسهی برنامه در حال خاتمه یافتن است. چرا اینگونه رفتار کردهاست؟
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب میشود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته میشود و چون در پیاده سازی فوق این نکته رعایت نشدهاست، این Task همواره در حال اجرا و خاتمه نیافته محسوب میشود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شدهی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آنرا فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمهی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شدهاست:
برای استفادهی از آن تنها کافی است متد ExecuteAsync آنرا پیاده سازی کنیم. به این ترتیب اینبار پیاده سازی SampleHostedService به صورت زیر تغییر میکند:
اینبار اگر این کار پسزمینه را به سیستم معرفی:
و سپس برنامه را اجرا کنیم:
مشاهده میکنیم که ابتدا هاست وب برنامه شروع به کار کردهاست و سپس سرویس انجام کارهای پسزمینه در حال اجرا است و به این ترتیب اجرای این سرویس پسزمینه، تداخلی را در کار برنامهی وب ایجاد نکردهاست. بنابراین از این پس بجای استفادهی از IHostedService خام، از نمونهی بهبود یافتهی BackgroundService آن استفاده کنید.
یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پسزمینه
IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگیها معرفی میشوند. در این حالت اگر سرویسهایی با طول عمر transient و یا scoped را به آنها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آنها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگیهای NET Core. با صدور استثنائی، از این مساله جلوگیری میکند (در این مورد در مطالب «مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامههای وب» و همچنین «قسمت سوم - رهاسازی منابع سرویسهای IDisposable» بیشتر بحث شدهاست). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope میتوان مدیریت کرد:
از این پس برای تعریف کارهای پسزمینهای که نیاز به تزریق سرویسهایی با طول عمر Scoped یا Transient دارند، میتوان کلاس سرویس وظیفه را از ScopedBackgroundService مشتق کرد و سپس متد ExecuteInScope آنرا پیاده سازی نمود. serviceProvider ای که در اینجا در اختیار مصرف کننده قرار میگیرد، داخل Scope قرار دارد و توسط آن میتوان سرویسهای مدنظر را توسط متدهایی مانند serviceProvider.GetRequiredService، دریافت کرد.
طراحی سرویس کارهای پسزمینهی زمانبندی شده
ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمیکند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیلهای خبرنامهی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شدهاست. برای پیاده سازی بهتر این تایمر میتوان از کتابخانهی NCrontab که توسط نویسندهی کتابخانهی معروف ELMAH تهیه شدهاست، استفاده کرد که با برنامههای NET Core. نیز سازگاری دارد:
عبارات Cron، روش بسیار متداولی برای تعریف و انجام کارهای زمانبندی شده در سیستمهای لینوکسی هستند. برای مثال عبارت * * * 0 1 سبب اجرای یک وظیفه، هر روز یک دقیقه پس از نیمهشب، میشود و فرمت کلی 5 قسمتی آن، به صورت زیر است:
و یا عبارت 6 قسمتی آن چنین مفهومی را دارد:
اگر ScopedBackgroundService فوق را با CrontabSchedule یاد شده ترکیب کنیم، میتوانیم به یک کلاس abstract دیگر برسیم که طراحی کلاس پایهی اجرای کارهای زمانبندی شده را ارائه میدهد:
این کلاس پایه، توسط متد CrontabSchedule.Parse، مقدار رشتهای Schedule را با فرمت Cron (فرمت 6 قسمتی که دارای ثانیه هم هست) دریافت و پردازش میکند. سپس متد GetNextOccurrence، زمان بعدی اجرای این وظیفه را مشخص میکند.
روش استفادهی از آن برای تعریف یک وظیفهی جدید نیز به صورت زیر است:
در اینجا ابتدا کار با پیاده سازی کلاس پایه ScheduledScopedBackgroundService شروع میشود. سپس باید مقدار Schedule را با فرمت 6 قسمتی مشخص کرد. برای مثال در سرویس فوق، این تنظیم سبب اجرای هر 10 ثانیه یکبار این وظیفه میگردد. در آخر، خود وظیفه داخل متد ScheduledExecuteInScope تعریف خواهد شد که serviceProvider دریافتی آن، داخل یک Scope قرار دارد.
روش معرفی آن به سیستم نیز مانند قبل است:
در این حالت اگر برنامه را اجرا کنید، یک چنین خروجی را که بیانگر اجرای هر 10 ثانیه یکبار وظیفهی تعریف شدهاست، مشاهده میکنید:
Generic Host چیست؟
Generic Host یکی از ویژگیهای جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای اینکار، امکان استفادهی از آن نه فقط در پروژههای وب، بلکه در پروژههای کنسول نیز میباشد. به این ترتیب میتوان کارهای غیر HTTP را از برنامهی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پسزمینهی خارج از اعمال web host جاری، ارائه دادهاند:
namespace Microsoft.Extensions.Hosting { public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); } }
یک مثال: معرفی کار پسزمینهای که هر دو ثانیه یکبار انجام میشود
در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ میشود و اگر برنامه را توسط دستور dotnet run اجرا کنید، میتوانید خروجی آنرا در کنسول، مشاهده کنید:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MvcTest { public class SampleHostedService : IHostedService { private readonly ILogger<SampleHostedService> _logger; public SampleHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting Hosted service"); while (!cancellationToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping Hosted service"); return Task.CompletedTask; } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHostedService, SampleHostedService>();
services.AddHostedService<SampleHostedService>();
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
Startup.cs(82,56): error CS0104: 'IHostingEnvironment' is an ambiguous reference between 'Microsoft.AspNetCore.Hosting.IHostingEnvironment' and 'Microsoft.Extensions.Hosting.IHostingEnvironment'
مشکلات پیاده سازی کار پسزمینهی SampleHostedService فوق
هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
D:\MvcTest>dotnet run info: MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:10 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:12 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:14 Ctrl+C Application is shutting down... Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down.
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب میشود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته میشود و چون در پیاده سازی فوق این نکته رعایت نشدهاست، این Task همواره در حال اجرا و خاتمه نیافته محسوب میشود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شدهی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آنرا فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمهی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شدهاست:
namespace Microsoft.Extensions.Hosting { public abstract class BackgroundService : IHostedService, IDisposable { protected BackgroundService(); public virtual void Dispose(); public virtual Task StartAsync(CancellationToken cancellationToken); public virtual Task StopAsync(CancellationToken cancellationToken); protected abstract Task ExecuteAsync(CancellationToken stoppingToken); } }
namespace MvcTest { public class PrinterHostedService : BackgroundService { private readonly ILogger<SampleHostedService> _logger; public PrinterHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Starting Hosted service"); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<PrinterHostedService>();
D:\MvcTest>dotnet run Hosting environment: Development infoContent root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. : MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:23 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:25 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:27 Application is shutting down... ^C
یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پسزمینه
IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگیها معرفی میشوند. در این حالت اگر سرویسهایی با طول عمر transient و یا scoped را به آنها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آنها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگیهای NET Core. با صدور استثنائی، از این مساله جلوگیری میکند (در این مورد در مطالب «مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامههای وب» و همچنین «قسمت سوم - رهاسازی منابع سرویسهای IDisposable» بیشتر بحث شدهاست). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope میتوان مدیریت کرد:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; public abstract class ScopedBackgroundService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; public ScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using (var scope = _serviceScopeFactory.CreateScope()) { await ExecuteInScope(scope.ServiceProvider, stoppingToken); } } public abstract Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
طراحی سرویس کارهای پسزمینهی زمانبندی شده
ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمیکند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیلهای خبرنامهی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شدهاست. برای پیاده سازی بهتر این تایمر میتوان از کتابخانهی NCrontab که توسط نویسندهی کتابخانهی معروف ELMAH تهیه شدهاست، استفاده کرد که با برنامههای NET Core. نیز سازگاری دارد:
dotnet add package ncrontab
┌───────────── minute (0 - 59) │ ┌───────────── hour (0 - 23) │ │ ┌───────────── day of month (1 - 31) │ │ │ ┌───────────── month (1 - 12) │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; │ │ │ │ │ 7 is also Sunday on some systems) │ │ │ │ │ │ │ │ │ │ * * * * *
* * * * * * - - - - - - | | | | | | | | | | | +--- day of week (0 - 6) (Sunday=0) | | | | +----- month (1 - 12) | | | +------- day of month (1 - 31) | | +--------- hour (0 - 23) | +----------- min (0 - 59) +------------- sec (0 - 59)
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NCrontab; using static NCrontab.CrontabSchedule; public abstract class ScheduledScopedBackgroundService : ScopedBackgroundService { private CrontabSchedule _schedule; private DateTime _nextRun; protected abstract string Schedule { get; } public ScheduledScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) { _schedule = CrontabSchedule.Parse(Schedule, new ParseOptions { IncludingSeconds = true }); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } public override async Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { do { var now = DateTime.Now; if (now > _nextRun) { await ScheduledExecuteInScope(serviceProvider, stoppingToken); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } await Task.Delay(1000, stoppingToken); //1 second delay } while (!stoppingToken.IsCancellationRequested); } public abstract Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
روش استفادهی از آن برای تعریف یک وظیفهی جدید نیز به صورت زیر است:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; public class MyScheduledTask : ScheduledScopedBackgroundService { private readonly ILogger<MyScheduledTask> _logger; public MyScheduledTask( IServiceScopeFactory serviceScopeFactory, ILogger<MyScheduledTask> logger) : base(serviceScopeFactory) { _logger = logger; } protected override string Schedule => "*/10 * * * * *"; //Runs every 10 seconds public override Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { _logger.LogInformation("MyScheduledTask executing - {0}", DateTime.Now); return Task.CompletedTask; } }
روش معرفی آن به سیستم نیز مانند قبل است:
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<MyScheduledTask>();
D:\MvcTest>dotnet run Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:18:50 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:00 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:10 Application is shutting down... ^C
اشتراکها