مدیریت پردازش آپلود فایلها در ASP.NET Core نسبت به ASP.NET MVC 5.x به طور کامل تغییر کردهاست و اینبار بجای ذکر نوع System.Web.HttpPostedFileBase باید از اینترفیس جدید IFormFile واقع در فضای نام Microsoft.AspNetCore.Http کمک گرفت.
مراحل فعال سازی آپلود فایلها در ASP.NET Core
مرحلهی اول فعال سازی آپلود فایلها در ASP.NET Core، شامل افزودن ویژگی "enctype="multipart/form-data به یک فرم تعریف شدهاست:
<form method="post"
asp-action="Index"
asp-controller="TestFileUpload"
enctype="multipart/form-data">
<input type="file" name="files" multiple />
<input type="submit" value="Upload" />
</form>
در اینجا همچنین ذکر ویژگی multiple در input از نوع file، امکان ارسال چندین فایل با هم را نیز میسر میکند.
در سمت سرور، امضای اکشن متد دریافت کنندهی این فایلها به صورت ذیل خواهد بود:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IList<IFormFile> files)
در اینجا نام پارامتر تعریف شده، باید دقیقا مساوی نام input از نوع file باشد. همچنین از آنجائیکه ویژگی multiple را نیز در سمت کلاینت قید کردهایم، این پارامتر سمت سرور از نوع یک لیست، تعریف شدهاست. اگر ویژگی multiple را حذف کنیم میتوان آنرا به صورت سادهی IFormFile files نیز تعریف کرد.
یافتن جایگزینی برای Server.MapPath در ASP.NET Core
زمانیکه فایل ارسالی، در سمت سرور دریافت شد، مرحلهی بعد، ذخیره سازی آن بر روی سرور است و از آنجائیکه ما دقیقا نمیدانیم ریشهی سایت در کدام پوشهی سرور واقع شدهاست، میشد از متد Server.MapPath برای یافتن دقیق آن کمک گرفت. با حذف این متد در ASP.NET Core، روش یافتن ریشهی سایت یا همان پوشهی wwwroot در اینجا شامل مراحل ذیل است:
public class TestFileUploadController : Controller
{
private readonly IHostingEnvironment _environment;
public TestFileUploadController(IHostingEnvironment environment)
{
_environment = environment;
}
ابتدا اینترفیس توکار IHostingEnvironment را در سازندهی کلاس تزریق میکنیم. سرویس HostingEnvironment
جزو سرویسهای از پیش تعریف شدهی ASP.NET Core است و نیازی به تنظیمات اضافهتری ندارد. همینقدر که ذکر شود، به صورت خودکار توسط ASP.NET Core مقدار دهی و تامین میگردد.
پس از آن خاصیت environment.WebRootPath_ به ریشهی پوشهی wwwroot برنامه، بر روی سرور اشاره میکند. به این ترتیب میتوان مسیر دقیقی را جهت ذخیره سازی فایلهای رسیده، مشخص کرد.
امکان ذخیره سازی async فایلها در ASP.NET Core
عملیات کار با فایلها، عملیاتی است که از مرزهای IO سیستم عبور میکند. به همین جهت یکی از بهترین مثالهای پیاده سازی async، جهت رها سازی تردهای برنامه و بالا بردن میزان پاسخدهی آن با بالا بردن تعداد تردهای آزاد بیشتر است. در ASP.NET Core، نوشتن async محتوای فایل رسیده در یک stream پشتیبانی میشود و این stream میتواند یک FileStream و یا MemoryStream باشد. در ذیل نحوهی کار async با یک FileStream را مشاهده میکنید:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IList<IFormFile> files)
{
var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsRootFolder))
{
Directory.CreateDirectory(uploadsRootFolder);
}
foreach (var file in files)
{
if (file == null || file.Length == 0)
{
continue;
}
var filePath = Path.Combine(uploadsRootFolder, file.FileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
return View();
}
در اینجا کدهای کامل متد دریافت فایلها را در سمت سرور مشاهده میکنید. ابتدا با استفاده از خاصیت environment.WebRootPath_، به مسیر ریشهی wwwroot دسترسی و سپس پوشهی uploads را در آن جهت ذخیره سازی فایلهای دریافتی، تعیین کردهایم.
چون برنامههای ASP.NET Core قابلیت اجرای بر روی لینوکس را نیز دارند، تا حد امکان باید از Path.Combine جهت جمع زدن اجزای مختلف یک میسر، استفاده کرد. از این جهت که در لینوکس، جداکنندهی اجزای مسیرها، / است بجای \ در ویندوز و متد Path.Combine به صورت خودکار این مسایل را لحاظ خواهد کرد.
در آخر با استفاده از متد file.CopyToAsync کار نوشتن غیرهمزمان محتوای فایل دریافتی در یک FileStream انجام میشود؛ به همین جهت در امضای متد فوق، <async Task<IActionResult را نیز ملاحظه میکنید.
پشتیبانی کامل از Model Binding آپلود فایلها در ASP.NET Core
در ASP.NET MVC 5.x اگر ویژگی Required را بر روی یک خاصیت از نوع HttpPostedFileBase قرار دهید ...
کار نمیکند و در سمت کلاینت تاثیری را به همراه نخواهد داشت؛ مگر اینکه تنظیمات سمت کلاینت آنرا به صورت دستی انجام دهیم. این مشکلات در ASP.NET Core، کاملا برطرف شدهاند:
public class UserViewModel
{
[Required(ErrorMessage = "Please select a file.")]
[DataType(DataType.Upload)]
public IFormFile Photo { get; set; }
}
در اینجا یک خاصیت از نوع IFormFile، با دو ویژگی Required و DataType خاص آن در یک ViewModel تعریف شدهاند. فرم معادل آن در ASP.NET Core به صورت ذیل خواهد بود:
@model UserViewModel
<form method="post"
asp-action="UploadPhoto"
asp-controller="TestFileUpload"
enctype="multipart/form-data">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input asp-for="Photo" />
<span asp-validation-for="Photo" class="text-danger"></span>
<input type="submit" value="Upload"/>
</form>
در اینجا ابتدا نوع مدل View تعیین شدهاست و سپس با استفاده از Tag Helpers، صرفا یک input را به خاصیت Photo مدل View جاری متصل کردهایم. همین اتصال سبب فعال سازی مباحث اعتبارسنجی سمت سرور و کاربر نیز میشود.
اینبار جهت فعال سازی و استفادهی از قابلیتهای Model Binding میتوان از ModelState نیز بهره گرفت:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel)
{
if (ModelState.IsValid)
{
var formFile = userViewModel.Photo;
if (formFile == null || formFile.Length == 0)
{
ModelState.AddModelError("", "Uploaded file is empty or null.");
return View(viewName: "Index");
}
var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsRootFolder))
{
Directory.CreateDirectory(uploadsRootFolder);
}
var filePath = Path.Combine(uploadsRootFolder, formFile.FileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await formFile.CopyToAsync(fileStream).ConfigureAwait(false);
}
RedirectToAction("Index");
}
return View(viewName: "Index");
}
اگر ModelState معتبر باشد، کار ذخیره سازی تک فایل رسیده را انجام میدهیم. سایر نکات این متد، با اکشن متد Index که پیشتر بررسی شد، یکی هستند.
بررسی پسوند فایلهای رسیدهی به سرور
ASP.NET Core دارای ویژگی است به نام FileExtensions که ... هیچ ارتباطی به خاصیتهایی از نوع IFormFile ندارد:
[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
ویژگی FileExtensions صرفا جهت درج بر روی خواصی از نوع string طراحی شدهاست. بنابراین قرار دادن این ویژگی بر روی خاصیتهایی از نوع IFormFile، سبب فعال سازی اعتبارسنجی سمت سرور پسوندهای فایلهای رسیده، نخواهد شد.
در ادامه جهت بررسی پسوندهای فایلهای رسیده، میتوان یک ویژگی اعتبارسنجی سمت سرور جدید را طراحی کرد:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class UploadFileExtensionsAttribute : ValidationAttribute
{
private readonly IList<string> _allowedExtensions;
public UploadFileExtensionsAttribute(string fileExtensions)
{
_allowedExtensions = fileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
public override bool IsValid(object value)
{
var file = value as IFormFile;
if (file != null)
{
return isValidFile(file);
}
var files = value as IList<IFormFile>;
if (files == null)
{
return false;
}
foreach (var postedFile in files)
{
if (!isValidFile(postedFile)) return false;
}
return true;
}
private bool isValidFile(IFormFile file)
{
if (file == null || file.Length == 0)
{
return false;
}
var fileExtension = Path.GetExtension(file.FileName);
return !string.IsNullOrWhiteSpace(fileExtension) &&
_allowedExtensions.Any(ext => fileExtension.Equals(ext, StringComparison.OrdinalIgnoreCase));
}
}
در اینجا با ارث بری از کلاس پایه ValidationAttribute و بازنویسی متد IsValid آن، کار اعتبارسنجی پسوند فایلها و یا فایل رسیده را انجام دادهایم. این ویژگی جدید اگر بر روی خاصیتی از نوع IFormFile قرار بگیرد، پارامتر object value متد IsValid آن حاوی اطلاعات فایل و یا فایلهای رسیده، خواهد بود. بر این اساس میتوان تصمیم گیری کرد که آیا پسوند این فایل، مجاز است یا خیر.
public class UserViewModel
{
[Required(ErrorMessage = "Please select a file.")]
//`FileExtensions` needs to be applied to a string property. It doesn't work on IFormFile properties, and definitely not on IEnumerable<IFormFile> properties.
//[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
[UploadFileExtensions(".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
[DataType(DataType.Upload)]
public IFormFile Photo { get; set; }
}
در اینجا روش استفادهی از این ویژگی اعتبارسنجی جدید را نیز با تکمیل ViewModel کاربر، مشاهده میکنید. پس از آن تنها بررسی if (ModelState.IsValid) در یک اکشن متد، نتیجهی دریافتی از اعتبارسنج جدید UploadFileExtensions را در اختیار ما قرار میدهد و بر این اساس میتوان تصمیمگیری کرد که آیا باید فایل رسیده را ذخیره کرد یا خیر.