With the 4.6.1 RC we have added support for WPF to recognize custom dictionaries registered globally. This capability is available in addition to the ability to register them per-control. Also, custom dictionaries in the previous versions of WPF had no affordance for Excluded Words and AutoCorrect lists. On Windows 8.1 and Windows 10, these scenarios are now enabled through the use of files that can be placed under %AppData%\Microsoft\Spelling\<language tag>.
پیشنیازها
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax »
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف و پوشهی خالی wwwroot را نیز به آن اضافه میکنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios react-toastify
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای نمایش پیامهای برنامه از کامپوننت react-toastify استفاده میکنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آنرا اضافه کنیم:
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
ایجاد کامپوننت React فرم ارسال فایلها به سمت سرور
پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل میکنیم:
import React, { useState } from "react"; import axios from "axios"; import { toast } from "react-toastify"; export default function UploadFileSimple() { const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState(); return ( <form> <fieldset className="form-group"> <legend>Support Form</legend> <div className="form-group row"> <label className="form-control-label" htmlFor="description"> Description </label> <input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file1"> File 1 </label> <input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} /> </div> <div className="form-group row"> <label className="form-control-label" htmlFor="file2"> File 2 </label> <input type="file" className="form-control" name="file2" onChange={event => setSelectedFile2(event.target.files[0])} /> </div> <div className="form-group row"> <button className="btn btn-primary" type="submit" > Submit </button> </div> </fieldset> </form> ); }
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شدهاند.
- مرحلهی بعد، دسترسی به فایلهای انتخابی کاربر و همچنین مقدار توضیحات وارد شدهاست. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کردهایم:
const [description, setDescription] = useState(""); const [selectedFile1, setSelectedFile1] = useState(); const [selectedFile2, setSelectedFile2] = useState();
- پس از طراحی state این فرم، مرحلهی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، میتوان آنرا به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخداد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
<input type="text" className="form-control" name="description" onChange={event => setDescription(event.target.value)} value={description} />
<input type="file" className="form-control" name="file1" onChange={event => setSelectedFile1(event.target.files[0])} />
تشکیل مدل ارسال دادهها به سمت سرور
در فرمهای معمولی، عموما دادهها به صورت یک شیء JSON به سمت سرور ارسال میشوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرمهایی که به همراه المانهای دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه میشود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا میکند:
<form enctype="multipart/form-data" action="/upload" method="post"> <input id="file-input" type="file" /> </form>
let file = document.getElementById("file-input").files[0]; let formData = new FormData(); formData.append("file", file); fetch('/upload/image', {method: "POST", body: formData});
در یک برنامهی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام دادهایم. در مرحلهی بعد، شیء FormData را تشکیل خواهیم داد:
// ... export default function UploadFileSimple() { // ... const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); toast.success("Form has been submitted successfully!"); setDescription(""); }; return ( <form onSubmit={handleSubmit}> </form> ); }
ارسال مدل دادههای فرم React به سمت سرور
پس از تشکیل شیء FormData در متد مدیریت کنندهی handleSubmit، اکنون با استفاده از کتابخانهی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
// ... export default function UploadFileSimple() { const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket"; // ... const [isUploading, setIsUploading] = useState(false); const handleSubmit = async event => { event.preventDefault(); const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2); try { setIsUploading(true); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} }); toast.success("Form has been submitted successfully!"); console.log("uploadResult", data); setIsUploading(false); setDescription(""); } catch (error) { setIsUploading(false); toast.error(error); } }; return ( // ... ); }
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده میکنید. از آن میتوان برای فعال و غیرفعال کردن دکمهی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button disabled={ isUploading } className="btn btn-primary" type="submit" > Submit </button>
اعتبارسنجی سمت کلاینت فایلهای ارسالی به سمت سرور
در اینجا شاید نیاز باشد نوع و یا اندازهی فایلهای انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه میکنیم:
const isFileValid = selectedFile => { if (!selectedFile) { // toast.error("Please select a file."); return false; } const allowedMimeTypes = [ "image/png", "image/jpeg", "image/gif", "image/svg+xml" ]; if (!allowedMimeTypes.includes(selectedFile.type)) { toast.error(`Invalid file type: ${selectedFile.type}`); return false; } const maxFileSize = 1024 * 500; const fileSize = selectedFile.size; if (fileSize > maxFileSize) { toast.error( `File size ${(fileSize / 1024).toFixed( 2 )} KB must be less than ${maxFileSize / 1024} KB` ); return false; } return true; };
اکنون برای استفادهی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کنندهی submit اطلاعات:
const handleSubmit = async event => { event.preventDefault(); if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) { return; }
ب) استفادهی از آن جهت غیرفعال کردن دکمهی submit:
<button disabled={ isUploading || !isFileValid(selectedFile1) || !isFileValid(selectedFile2) } className="btn btn-primary" type="submit" > Submit </button>
نمایش درصد پیشرفت آپلود فایلها
کتابخانهی axios، امکان دسترسی به میزان اطلاعات آپلود شدهی به سمت سرور را به صورت یک رخداد فراهم کردهاست که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایلها استفاده میکنیم:
const startTime = Date.now(); const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }, onUploadProgress: progressEvent => { const { loaded, total } = progressEvent; const timeElapsed = Date.now() - startTime; const uploadSpeed = loaded / (timeElapsed / 1000); setUploadProgress({ queueProgress: Math.round((loaded / total) * 100), uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed), uploadTimeElapsed: Math.ceil(timeElapsed / 1000), uploadSpeed: (uploadSpeed / 1024).toFixed(2) }); } });
const [uploadProgress, setUploadProgress] = useState({ queueProgress: 0, uploadTimeRemaining: 0, uploadTimeElapsed: 0, uploadSpeed: 0 });
const showUploadProgress = () => { const { queueProgress, uploadTimeRemaining, uploadTimeElapsed, uploadSpeed } = uploadProgress; if (queueProgress <= 0) { return <></>; } return ( <table className="table"> <thead> <tr> <th width="15%">Event</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td> <strong>Elapsed time</strong> </td> <td>{uploadTimeElapsed} second(s)</td> </tr> <tr> <td> <strong>Remaining time</strong> </td> <td>{uploadTimeRemaining} second(s)</td> </tr> <tr> <td> <strong>Upload speed</strong> </td> <td>{uploadSpeed} KB/s</td> </tr> <tr> <td> <strong>Queue progress</strong> </td> <td> <div className="progress-bar progress-bar-info progress-bar-striped" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow={queueProgress} style={{ width: queueProgress + "%" }} > {queueProgress}% </div> </td> </tr> </tbody> </table> ); };
{showUploadProgress()}
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست. اگر style آنرا هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.
یک نکته: اگر میخواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگهی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایلها، 100 درصد را مشاهده خواهید کرد.
دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیرهی فایلهای آن
بر اساس نحوهی تشکیل FormData سمت کلاینت:
const formData = new FormData(); formData.append("description", description); formData.append("file1", selectedFile1); formData.append("file2", selectedFile2);
using Microsoft.AspNetCore.Http; namespace UploadFilesSample.Models { public class Ticket { public int Id { set; get; } public string Description { set; get; } public IFormFile File1 { set; get; } public IFormFile File2 { set; get; } } }
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده میکنید:
using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using UploadFilesSample.Models; namespace UploadFilesSample.Controllers { [Route("api/[controller]")] [ApiController] public class SimpleUploadController : Controller { private readonly IWebHostEnvironment _environment; public SimpleUploadController(IWebHostEnvironment environment) { _environment = environment; } [HttpPost("[action]")] public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket) { var file1Path = await saveFileAsync(ticket.File1); var file2Path = await saveFileAsync(ticket.File2); //TODO: save the ticket ... get id return Created("", new { id = 1001 }); } private async Task<string> saveFileAsync(IFormFile file) { const string uploadsFolder = "uploads"; var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } //TODO: Do security checks ...! if (file == null || file.Length == 0) { return string.Empty; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream); } return $"/{uploadsFolder}/{file.Name}"; } } }
- تزریق IWebHostEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
const { data } = await axios.post(apiUrl, formData, { headers: { "Content-Type": "multipart/form-data" }} });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
چرا در C# 9.0 تا این اندازه بر روی سادگی ایجاد اشیاء Immutable تمرکز شدهاست؟
به شیءای Immutable گفته میشود که پس از وهله سازی ابتدایی آن، وضعیت آن دیگر قابل تغییر نباشد. همچنین به کلاسی Immutable گفته میشود که تمام وهلههای ساخته شدهی از آن نیز Immutable باشند. نمونهی یک چنین شیءای را از نگارش 1 دات نت در حال استفاده هستیم: رشتهها. رشتهها در دات نت غیرقابل تغییر هستند و هرگونه تغییری بر روی آنها، سبب ایجاد یک رشتهی جدید (یک شیء جدید) میشود. نوع جدید record نیز به همین صورت عمل میکند.
مزایای وجود Immutability:
- اشیاء Immutable یا غیرقابل تغییر، thread-safe هستند که در نتیجه، برنامه نویسی همزمان و موازی را بسیار ساده میکنند؛ چون چندین thread میتوانند با شیءای کار کنند که دسترسی به آن، تنها read-only است.
- اشیاء Immutable از اثرات جانبی، مانند تغییرات آنها در متدهای مختلف در امان هستند. میتوانید آنها را به هر متدی ارسال کنید و مطمئن باشید که پس از پایان کار، این شیء تغییری نکردهاست.
- کار با اشیاء Immutable، امکان بهینه سازی حافظه را میسر میکنند. برای مثال NET runtime.، هش رشتههای تعریف شدهی در برنامه را در پشت صحنه نگهداری میکند تا مطمئن شود که تخصیص حافظهی اضافی، برای رشتههای تکراری صورت نمیگیرد. نمونهی دیگر آن نمایش حرف "a" در یک ادیتور یا نمایشگر است. زمانیکه یک شیء Immutable حاوی اطلاعات حرف "a"، ایجاد شود، به سادگی میتوان این تک وهله را جهت نمایش هزاران حرف "a" مورد استفادهی مجدد قرار داد، بدون اینکه نگران مصرف حافظهی بالای برنامه باشیم.
- کار با اشیاء Immutable به باگهای کمتری ختم میشود؛ چون همواره امکان تغییر حالت درونی یک شیء، توسط قسمتهای مختلف برنامه، میتواند به باگهای ناخواستهای منتهی شوند.
- Hash listها که در جهت بهبود کارآیی برنامهها بسیار مورد استفاده قرار میگیرند، بر اساس کلیدهایی Immutable قابل تشکیل هستند.
روش تعریف نوعهای جدید record
کلاس سادهی زیر را در نظر بگیرید:
public class User { public string Name { set; get; } }
public record User { public string Name { set; get; } }
var user = new User(); user.Name = "User 1";
روش تعریف دومی نیز در اینجا میسر است (به آن positional record هم میگویند):
public record User(string Name);
برای کار با رکورد دومی که تعریف کردیم باید سازندهی این record را مقدار دهی کرد:
var user = new User("User 1"); // Error: Init-only property or indexer 'User.Name' can only be assigned // in an object initializer, or on 'this' or 'base' in an instance constructor // or an 'init' accessor. [CS9Features]csharp(CS8852) user.Name = "User 1";
نوع جدید record چه اطلاعاتی را به صورت خودکار تولید میکند؟
روش دوم تعریف recordها اگر در نظر بگیریم:
public record User(string Name);
using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; using CS9Features; public class User : IEquatable<User> { protected virtual Type EqualityContract { [System.Runtime.CompilerServices.NullableContext(1)] [CompilerGenerated] get { return typeof(User); } } public string Name { get; set/*init*/; } public User(string Name) { this.Name = Name; base..ctor(); } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("User"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("Name"); builder.Append(" = "); builder.Append((object?)Name); return true; } [System.Runtime.CompilerServices.NullableContext(2)] public static bool operator !=(User? r1, User? r2) { return !(r1 == r2); } [System.Runtime.CompilerServices.NullableContext(2)] public static bool operator ==(User? r1, User? r2) { return (object)r1 == r2 || (r1?.Equals(r2) ?? false); } public override int GetHashCode() { return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name); } public override bool Equals(object? obj) { return Equals(obj as User); } public virtual bool Equals(User? other) { return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name); } public virtual User <Clone>$() { return new User(this); } protected User(User original) { Name = original.Name; } public void Deconstruct(out string Name) { Name = this.Name; } }
- recordها هنوز هم در اصل همان classهای استاندارد #C هستند (یعنی در اصل reference type هستند).
- این کلاس به همراه یک سازنده و یک خاصیت init-only است (بر اساس تعاریف ما).
- متد ToString آن بازنویسی شدهاست تا اگر آنرا بر روی شیء حاصل، فراخوانی کردیم، به صورت خودکار نمایش زیبایی را از محتوای آن ارائه دهد.
- این کلاس از نوع <IEquatable<User است که امکان مقایسهی اشیاء record را به سادگی میسر میکند. برای این منظور متدهای GetHashCode و Equals آن به صورت خودکار بازنویسی و تکمیل شدهاند (یعنی مقایسهی آن شبیه به value-type است).
- این کلاس امکان clone کردن اطلاعات جاری را مهیا میکند.
- همچنین به همراه یک متد Deconstruct هم هست که جهت انتساب خواص تعریف شدهی در آن، به یک tuple مفید است.
بنابراین یک رکورد به همراه قابلیتهایی است که سالها در زبان #C وجود داشتهاند و شاید ما به سادگی حاضر به تشکیل و تکمیل آنها نمیشدیم؛ اما اکنون کامپایلر زحمت کدنویسی خودکار آنها را متقبل میشود!
ساخت یک وهلهی جدید از یک record با clone کردن آن
اگر به کدهای حاصل از دیکامپایل فوق دقت کنید، یک قسمت جدید clone هم با syntax خاصی در آن ظاهر شدهاست:
public virtual User <Clone>$() { return new User(this); }
public record User(string Name, int Age);
var user1 = new User("User 1", 21);
var user2 = user1 with { Age = 31 };
مقایسهی نوعهای record
در کدهای حاصل از دیکامپایل فوق، قسمت عمدهای از آن به تکمیل اینترفیس <IEquatable<User پرداخته شده بود. به همین جهت اکنون دو رکورد با مقادیر خواص یکسانی را ایجاد میکنیم:
var user1 = new User("User 1", 21); var user2 = new User("User 1", 21);
Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2)); Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
user1.Equals(user2) -> True user1 == user2 -> True
- زمانیکه عملگر == را بر روی شیء user1 و user2 اعمال میکنیم، اگر User، از نوع کلاس معمولی باشد، حاصل آن false خواهد بود؛ چون این دو، به یک مکان از حافظه اشاره نمیکنند، حتی با اینکه مقادیر خواص هر دو شیء یکی است.
- اما اگر به قطعه کد دیکامپایل شده دقت کنید، در یک رکورد که هر چند در اصل یک کلاس است، حتی عملگر == نیز بازنویسی شدهاست تا در پشت صحنه همان متد Equals را فراخوانی کند و این متد با توجه به پیاده سازی اینترفیس <IEquatable<User، اینبار دقیقا مقادیر خواص رکورد را یک به یک مقایسه کرده و نتیجهی حاصل را باز میگرداند:
public virtual bool Equals(User? other) { return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name) && EqualityComparer<int>.Default.Equals(Age, other!.Age); }
یک نکته: بازنویسی عملگر == در SDK نگارش rc2 فعلی رخدادهاست و در نگارشهای قبلی preview، اینگونه نبود.
امکان ارثبری در recordها
دو رکورد زیر را در نظر بگیرید که اولی به همراه Name است و نمونهی مشتق شدهی از آن، خاصیت init-only سن را نیز به همراه دارد:
public record User { public string Name { get; init; } public User(string name) { Name = name; } } public record UserWithAge : User { public int Age { get; init; } public UserWithAge(string name, int age) : base(name) { Age = age; } }
var user1 = new User("User 1"); var user2 = new UserWithAge("User 1", 21); Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2)); Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
user1.Equals(user2) -> False user1 == user2 -> False
امکان تعریف ارثبری رکوردها به صورت زیر نیز وجود دارد و الزاما نیازی به روش تعریف کلاس مانند آنها، مانند مثال فوق نیست:
public abstract record Food(int Calories); public record Milk(int C, double FatPercentage) : Food(C);
رکوردها متد ToString را بازنویسی میکنند
در مثال قبلی اگر یک ToString را بر روی اشیاء تشکیل شده فراخوانی کنیم:
Console.WriteLine(user1.ToString()); Console.WriteLine(user2.ToString());
User { Name = User 1 } UserWithAge { Name = User 1, Age = 21 }
امکان استفادهی از Deconstruct در رکوردها
دو روش برای تعریف رکوردها وجود دارند؛ یکی شبیه به تعریف کلاسها است و دیگری تعریف یک سطری، که positional record نیز نامیده میشود:
public record Person(string Name, int Age);
public void Deconstruct(out string Name, out int Age) { Name = this.Name; Age = this.Age; }
var (name, age) = new Person("User 1", 21);
امکان استفادهی از نوعهای record در ASP.NET Core 5x
سیستم model binding در ASP.NET Core 5x، از نوعهای record نیز پشتیبانی میکند؛ یک مثال:
public record Person([Required] string Name, [Range(0, 150)] int Age); public class PersonController { public IActionResult Index() => View(); [HttpPost] public IActionResult Index(Person person) { // ... } }
پرسش و پاسخ
آیا نوعهای record به صورت value type معرفی میشوند؟
پاسخ: خیر. رکوردها در اصل reference type هستند؛ اما از لحاظ مقایسه، شبیه به value types عمل میکنند.
آیا میتوان در یک کلاس، خاصیتی از نوع رکورد را تعریف کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان در رکوردها، از struct و یا کلاسها جهت تعریف خواص استفاده کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان از واژهی کلیدی with با کلاسها و یا structها استفاده کرد؟
پاسخ: خیر. این واژهی کلیدی در C# 9.0 مختص به رکوردها است.
آیا رکوردها به صورت پیشفرض Immutable هستند؟
پاسخ: اگر آنها را به صورت positional records تعریف کنید، بله. چون در این حالت خواص تشکیل شدهی توسط آنها از نوع init-only هستند. در غیراینصورت، میتوان خواص غیر init-only را نیز به تعریف رکوردها اضافه کرد.
پیشنیاز
- نقشه راه «آزمون واحد در دات نت»
- مطلب «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها»
در این مطلب قصد داریم تست ServiceLayer را به جای تست درون حافظهای که با ابزارهای Mocking در قالب Unit Testing انجام میگیرد، به کمک یک دیتابیس واقعی سبک وزن در قالب Integration Testing انجام دهیم.
قدم اول
یک پروژه تست را ایجاد کنید؛ بهتر است برای نظم دهی به ساختار Solution، پروژههای تست را در پوشه ای به نام Tests نگهداری کنید.
قدم دوم
بستههای نیوگت زیر را نصب کنید:
PM> install-package NUnit PM> install-package Shouldly PM> install-package EntityFramework PM> install-package FakeHttpContext
قدم سوم
نسخه دیتابیس انتخابی برای تست خودکار، LocalDB می باشد. لازم است در ابتدای اجرای تستها دیتابیس مربوط به Integration Test ایجاد شده و بعد از اتمام نیز دیتابیس مورد نظر حذف شود؛ برای این منظور از کلاس TestSetup استفاده خواهیم کرد.
[SetUpFixture] public class TestSetup { [OneTimeSetUp] public void SetUpDatabase() { DestroyDatabase(); CreateDatabase(); } [OneTimeTearDown] public void TearDownDatabase() { DestroyDatabase(); } //... }
با توجه به اینکه کلاس TestSetup با [SetUpFixture] تزئین شده است، Nunit قبل از اجرای تستها سراغ این کلاس آمده و متد SetUpDatebase را به دلیل تزئین شدن با [OneTimeSetUp]، قبل از اجرای تستها و متد TearDownDatabase را بدلیل تزئین شدن با [OneTimeTearDown] بعد از اجرای تمام تستها، اجرا خواهد کرد.
متد CreateDatabase
private static void CreateDatabase() { ExecuteSqlCommand(Master, string.Format(SqlResource.DatabaseScript, FileName)); //Use T-Sql Scripts For Create Database //ExecuteSqlCommand(MyAppTest, SqlResources.V1_0_0); var migration = new MigrateDatabaseToLatestVersion<ApplicationDbContext, DataLayer.Migrations.Configuration>(); migration.InitializeDatabase(new ApplicationDbContext()); } private static SqlConnectionStringBuilder Master => new SqlConnectionStringBuilder { DataSource = @"(LocalDB)\MSSQLLocalDB", InitialCatalog = "master", IntegratedSecurity = true }; private static string FileName => Path.Combine( Path.GetDirectoryName( Assembly.GetExecutingAssembly().Location), "MyAppTest.mdf");
برای مدیریت محل ذخیره سازی فایلهای دیتابیس، ابتدا دستورات ایجاد «دیتابیس تست» را برروی دیتابیس master اجرا میکنیم و در ادامه برای ساخت جداول از مکانیزم Migration خود EF استفاده شده است.
لازم است رشته اتصال به این دیتابیس ایجاد شده را در فایل App.config پروژه تست قرار دهید:
<connectionStrings> <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=(LocalDB)\MSSQLLocalDb;Initial Catalog=MyAppTest;Integrated Security=True;" /> </connectionStrings>
متد DestroyDatabase
private static void DestroyDatabase() { var fileNames = ExecuteSqlQuery(Master, SqlResource.SelecDatabaseFileNames, row => (string)row["physical_name"]); if (!fileNames.Any()) return; ExecuteSqlCommand(Master, SqlResource.DetachDatabase); fileNames.ForEach(File.Delete); }
در این متد ابتدا آدرس فایلهای مرتبط با «دیتابیس تست» واکشی شده و در ادامه دستورات Detach دیتابیس انجام شده و فایلهای مرتبط حذف خواهند شد. فایلهای دیتابیس در مسیری شبیه به آدرس نشان داده شدهی در شکل زیر ذخیره خواهند شد.
قدم چهارم
برای جلوگیری از تداخل بین تستها لازم است تک تک تستها از هم ایزوله باشند؛ یکی از راه حلهای موجود، استفاده از تراکنشها میباشد. برای این منظور امکان AutoRollback را به صورت خودکار به متدهای تست با استفاده از Attribute زیر اعمال خواهیم کرد:
public class AutoRollbackAttribute : Attribute, ITestAction { private TransactionScope _scope; public void BeforeTest(ITest test) { _scope = new TransactionScope(TransactionScopeOption.RequiresNew,new TransactionOptions {IsolationLevel = IsolationLevel.Snapshot}); } public void AfterTest(ITest test) { _scope?.Dispose(); _scope = null; } public ActionTargets Targets => ActionTargets.Test; }
متدهای BeforTest و AfterTest به ترتیب قبل و بعد از اجرای متدهای تست تزئین شده با این Attribute اجرا خواهند شد.
در مواقعی هم که به HttpConext نیاز دارید، میتوانید از کتابخانه FakeHttpContext بهره ببرید. برای این مورد هم میتوان Attributeای را به مانند AutoRollback در نظر گرفت.
public class HttpContextAttribute:Attribute,ITestAction { private FakeHttpContext.FakeHttpContext _httpContext; public void BeforeTest(ITest test) { _httpContext = new FakeHttpContext.FakeHttpContext(); } public void AfterTest(ITest test) { _httpContext?.Dispose(); _httpContext = null; } public ActionTargets Targets => ActionTargets.Test; }
کاری که FakeHttpContext انجام میدهد، مقدار دهی HttpContext.Current با یک پیاده سازی ساختگی میباشد.
قدم پنجم
به عنوان مثال اگر بخواهیم برای سرویس «گروه کاربری»، Integration Test بنویسیم، به شکل زیر عمل خواهیم کرد:
namespace MyApp.IntegrationTests.ServiceLayer { [TestFixture] [AutoRollback] [HttpContext] public class RoleServiceTests { private IRoleApplicationService _roleService; [SetUp] public void Init() { } [TearDown] public void Clean() { } [OneTimeSetUp] public void SetUp() { _roleService = IoC.Resolve<IRoleApplicationService>(); using (var uow = IoC.Resolve<IUnitOfWork>()) { RoleInitialDataBuilder.Build(uow); } } [OneTimeTearDown] public void TearDown() { } [Test] [TestCase("Role1")] public void Should_Create_New_Role(string role) { var viewModel = new RoleCreateViewModel { Name = role }; _roleService.Create(viewModel); using (var context = IoC.Resolve<IUnitOfWork>()) { var user = context.Set<Role>().FirstOrDefault(a => a.Name == role); user.ShouldNotBeNull(); } } [Test] public void Should_Not_Create_New_Role_With_Admin_Name() { var viewModel = new RoleCreateViewModel { Name = "Admin" }; Assert.Throws<DbUpdateException>(() => _roleService.Create(viewModel)); } [Test] public void Should_AdminRole_Exists() { using (var context = IoC.Resolve<IUnitOfWork>()) { var user = context.Set<Role>().FirstOrDefault(a => a.Name == "Admin"); user.ShouldNotBeNull(); } } [Test] public void Should_Not_Create_New_Role_Without_Name() { Assert.Throws<ValidationException>(() => _roleService.Create(new RoleCreateViewModel { Name = null })); } } }
با این خروجی:
- Accessibility improvements in narration, high contrast and focus control areas
- .NET Framework support for .NET Standard 2.0 and compiler features
- More secure SHA-2 support in ASP.NET and System.Messaging
- Configuration builders
- ASP.NET Execution step feature
- ASP.NET HttpCookie parsing
- Enhancements in Visual Tree for WPF applications
- Performance and reliability improvements
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body) public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body) public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body) public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body) public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body) public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
یک مثال: در اینجا میخواهیم به صورت موازی، مشخصات کاربرانی از Github را توسط HttpClient دریافت کنیم. هر بار هم فقط میخواهیم سه وظیفه اجرا شوند و نه بیشتر
using System.Net.Http.Headers; using System.Net.Http.Json; var userHandlers = new [] { "users/VahidN", "users/shanselman", "users/jaredpar", "users/davidfowl" }; using HttpClient client = new() { BaseAddress = new Uri("https://api.github.com"), }; client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("DotNet", "6")); ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = 3 }; await Parallel.ForEachAsync(userHandlers, parallelOptions, async (uri, token) => { var user = await client.GetFromJsonAsync<GitHubUser>(uri, token); Console.WriteLine($"Name: {user.Name}\nBio: {user.Bio}\n"); }); public class GitHubUser { public string Name { get; set; } public string Bio { get; set; } }
متد Parallel.ForEachAsync، آرایهای را که باید بر روی آن کار کند، دریافت میکند. سپس تنظیمات اجرای موازی آنها را هم مشخص میکنیم. در ادامه آنها را در دستههای مشخصی، به صورت موازی بر اساس منطقی که مشخص میکنیم، اجرا خواهد کرد.
وضعیت امکان اجرای موازی متدهای async همزمان، تا پیش از دات نت 6
<List<T به همراه متد الحاقی ForEach است که میتواند یک <Action<T را بر روی المانهای این لیست، اجرا کند و ... عموما زمانیکه به وظایف async میرسیم، به اشتباه مورد استفاده قرار میگیرد:
customers.ForEach(c => SendEmailAsync(c));
foreach(var c in customers) { SendEmailAsync(c); // the return task is ignored }
customers.ForEach(async c => await SendEmailAsync(c));
تنها راه حل پذیرفتهی شدهی چنین عمل async ای، فراخوانی آنها به صورت متداول زیر و بدون استفاده از متد ForEach است:
foreach(var c in customers) { await SendEmailAsync(c); }
foreach(var o in orders) { await ProcessOrderAsync(o); }
var tasks = orders.Select(o => ProcessOrderAsync(o)).ToList(); await Task.WhenAll(tasks);
دات نت 6، هم کنترل MaxDegreeOfParallelism را میسر کردهاست و هم اینکه اینبار نگارش async واقعی Parallel.ForEachAsync را ارائه دادهاست تا دیگر همانند حالت قبلی Parallel.ForEach، به async voidها و مشکلات مرتبط با آنها نرسیم.
امکان استفاده از گروپپالیسیها و...
Ubuntu 21.04 desktops can now join an AD domain at installation for central configuration. In turn, AD administrators can now manage Ubuntu workstations, which simplifies compliance with company policies.
The new Ubuntu also adds the ability to configure system settings from an AD domain controller. Using Ubuntu's AD Group Policy Client or other Group Policy Clients, system administrators can specify security policies on all connected clients, such as password policies and user access control, and desktop environment settings, such as login screen, background, and favorite programs.
Public Class User { public string Id { get; set; } public string PhoneNumber { get; set; } public Dictionary<string, App> Apps { get; set; } } public class App { public string FirstName { get; set; } public string LastName { get; set; } public string UserName { get; set; } public List<string> Roles { get; set; } public List<String> Messages { get; set; } public String AdressId { get; set; } public bool IsActive { get; set; } = true; [JsonIgnore] public string DisplayName => $"{FirstName} {LastName}"; }
services.AddSingleton<IDocumentStore>(serviceProvider => { var store = new DocumentStore() { Urls = new[] { "http://192.168.1.10:8080" }, Database = "AccountingSystem", }.Initialize(); return store; }); services.AddScoped<IAsyncDocumentSession>(serviceProvider => { var store = serviceProvider.GetRequiredService<IDocumentStore>(); return store.OpenAsyncSession(); });
var user = new User { PhoneNumber = user.PhoneNumber }; user.Apps.Add(appCode, new ActiveApp { FirstName = "عبدالصالح", LastName = "کاشانی", UserName = abdossaleh, IsActive = true, RolesId = new List<string>{"Admin"} }); await _documentSession.StoreAsync(user); await _documentSession.SaveChangesAsync()
var user = await _documentSession.LoadAsync<User>("Users/131-A");
_documentSession.Advanced.Patch<User, string>("Users/131-A", u => u.PhoneNumber , "09131110000");
_documentSession.Advanced.Patch<User, string>("Users/131-A", u => u.Apps["59"].RolesId , r => r.Add("Admin"));
_documentSession.Advanced.Increment<User, int>("Users/131-A", x => x.TestProp, 10);
_documentSession.Advanced.Defer(new PatchCommandData("Users/131-A", null, new PatchRequest() { Script = $@"this.Apps[args.appCode] = args.app", Values = { {"appCode", appCode}, {"app", new ActiveApp { FirstName = "عبدالصالح", LastName = "کاشانی", UserName = abdossaleh, RolesId = new List<string>{"Admin"} } } } }, null));
Script = "this.Apps[args.app].Roles.splice(args.index,0,args.role)", Values = { { "index": 1 // مکانی که میخواهیم عملیات انجام شود "app", 59 "role", "User" } }
splice(args.index,1,args.role)
Script = @"this.Roles= this.Apps[args.app].Roles.filter(role=> role != args.role);", Values = { {"app", 59} {"role", "User"} }
در مورد معرفی WPF Extended toolkit چندی قبل مطلبی منتشر شد. در ادامه این بی مهریها (!) میتوان به عدم به روز رسانی قالبهای ارائه شده برای WPF اشاره کرد. در WPF4 ، کنترل DataGrid از WPF toolkit به مجموعهی کنترلهای اصلی WPF منتقل شده است، اما قالبهای منتشر شدهی آن جهت لحاظ کردن این مورد به روز نشدهاند. یعنی اگر برای مثال یکی از قالبهای موجود را به برنامه خود اعمال کنید و سپس DataGrid را بر روی فرم قرار دهید، وصلهی ناهماهنگی را مشاهده خواهید نمود. این مشکلات در Silverlight وجود ندارند و قالبهای ارائه شدهی برای آن به روز بوده و همچنین روز به روز هم تعدادشان بیشتر میشوند.
اما باز هم نمیتوان ایراد گرفت چون کار ارائه شده سورس باز است. به عبارتی اگر مایکروسافت این قالبها را به روز نکرده، خوب، لطفا خود شما وقت بگذارید و این کار را انجام داده و سپس یک patch ارائه دهید. ایرادی دارد؟!
برای این منظور پروژهای در سایت CodePlex ایجاد شده است و تنها به پوشش دات نت سه و نیم و دیتاگرید متعلق به WPF Toolkit پرداخته است :
اگر علاقمند باشید که از دیتاگرید بومی دات نت 4 استفاده کنید میتوانید از این patch استفاده کنید.
public static class MyAttributes { [Obsolete] public static void MyMethod1() { } public static void MyMetho2() { } }
حال در این بین این سؤال پیش میآید که چگونه ما هم میتوانیم متادیتاهایی را با سلیقهی خود ایجاد کنیم.
برای تهیهی یک متادیتا از کلاس system.attribute استفاده میکنیم:
public class MyMaxLength:Attribute { }
[MyMaxLength] public class GetCustomProperties { //... }
public class MyMaxLength:Attribute { private int max; public string ErrorText = ""; public MyMaxLength(int max) { this.max = max; ErrorText = string.Format("max Length is {0} chars", max); } }
[MyMaxLength(30)] public class GetCustomProperties { //... } //or [MyMaxLength(30,ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")] public class GetCustomProperties { //... }
اجباری کردن Type
هر متادیتا میتواند مختص یک نوع Type باشد که این نوع میتواند یک کلاس، متد، پراپرتی یا ساختار و ... باشد. نحوهی محدود سازی آن توسط یک متادیتا مشخص میشود:
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)] public class MyMaxLength:Attribute { private int max; public string ErrorText = ""; public MyMaxLength(int max) { this.max = max; ErrorText = string.Format("max Length is {0} chars", max); } }
[AttributeUsage(AttributeTargets.Property)]
public class User { [MyMaxLength(30, ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")] public string Name { get; set; } }
[AttributeUsage(AttributeTargets.Property,AllowMultiple = true)] public class MyMaxLength:Attribute { //.... }
[MyMaxLength(40, ErrorText = "شما اجازه ندارید بیش از 40 کاراکتر وارد نمایید")] [MyMaxLength(50, ErrorText = "شما اجازه ندارید بیش از 50 کاراکتر وارد نمایید")] [MyMaxLength(30, ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")] public string Name { get; set; }
آخرین ویژگی که این متادیتا در دسترس ما قرار میدهد، استفاده از خصوصیت ارث بری است که به طور پیش فرض با True مقداردهی شده است. موقعی که شما یک متادیتا را به ویژگی ارث بری مزین کنید، در صورتی که آن کلاس که برایش متادیتا تعریف میکنید به عنوان والد مورد استفاده قرار بگیرد، فرزند آن هم به طور خودکار این متادیتا برایش منظور میگردد. به مثالهای زیر دقت کنید:
دو عدد متادیتا تعریف شده که یکی از آنها ارث بری در آن فعال شده و دیگری خیر.
public class MyAttribute : Attribute { //... } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public class YourAttribute : Attribute { //... }
هر دو متادیتا بر سر یک متد در یک کلاسی که بعدا از آن ارث بری میشود تعریف شده اند.
public class MyClass { [MyAttribute] [YourAttribute] public virtual void MyMethod() { //... } }
public class YourClass : MyClass { public override void MyMethod() { //... } }
Type type = typeof (User); foreach (PropertyInfo property in type.GetProperties()) { foreach (Attribute attribute in property.GetCustomAttributes(true)) { MyMaxLength max = attribute as MyMaxLength; if (max != null) { string Max = max.ErrorText; //انجام عملیات } } }
[MyMaxLength(30, typeof(User))]