اگر قصد اجرای برخی کارها به صورت زمانبندی شده و در فواصل زمانی مشخص را دارید، این مقاله به شما کمک خواهد کرد تا به بهترین شکل ممکن آن را انجام دهید. کارهایی مانند ارسال خبرنامه، فرستادن SMS تبریک تولد یا هماهنگ سازی دادهها بین دو منبع داده از جمله اَعمالی هستند که باید به صورت زمانبندی شده انجام شوند.
کتابخانهی Quartz.NET، از کتابخانه ای با نام Quartz و از زبان Java به NET. منتقل شده است. Quartz.NET، رایگان و باز متن است و از طریق آدرس http://quartznet.sourceforge.net در دسترس است. از طریق NuGet نیز میتوانید با تایپ عبارت quartz در فرم مربوطه، این کتابخانه را نصب کنید. این کتابخانه را در برنامههای Desktop و Web (حتی یک Shared Server) تست کردم و به خوبی انجام وظیفه میکند.
شروع کار با Quartz.NET
ضمن در اختیار قرار دادن امکانات فوق العاده و انعطاف پذیری بسیار، کار با این کتابخانه آسان و از فرایندی منطقی تبعیت میکند. فرایند اجرای یک روال زمانبندی شده از طریق Quartz.NET، از چهار مرحلهی اصلی تشکیل شده است.
1) پیاده سازی اینترفیس IJob
2) مشخص کردن جزئیات روال با اینترفیس IJobDetail
3) مشخص کردن تنظیمات زمان با استفاده از اینترفیس ITrigger
4) مدیریت اجرا با استفاده از اینترفیس IScheduler
مثالی را بررسی میکنیم. در این مثال قصد داریم تا عبارتی را همراه با تاریخ و زمان جاری در یک فایل ذخیره کنیم. این پیغام باید 3 بار و در فواصل زمانی 10 ثانیه به فایل اضافه شود. در پایان، فایلی خواهیم داشت که در سه خط، یک عبارت، همراه با تاریخ و زمانهای مختلف را که 10 ثانیه با یکدیگر اختلاف دارند در خود ذخیره کرده است. ابتدا کار زمانبندی شده را با ارائهی پیاده سازی برای متد Execute اینترفیس IJob این کتابخانه ایجاد میکنیم. وارد کردن فضای نام Quartz را فراموش نکنید.
namespace SchedulerDemo.Jobs { using System; using System.IO; using Quartz; public class HelloJob : IJob { public void Execute(IJobExecutionContext context) { // for web apps // string path = System.Web.Hosting.HostingEnvironment.MapPath("~/Data/Log.txt"); // for desktop apps string path = @"C:\Log.txt"; using (StreamWriter sw = new StreamWriter(path, true)) { sw.WriteLine("Message from HelloJob " + DateTime.Now.ToString()); } } } }
حال، زمان انجام تنظیمات مختلف برای اجرای روال مربوطه است. بهتر است تا interfaceیی ایجاد و متدی با نام Run در آن داشته باشیم.
namespace SchedulerDemo.Interfaces { public interface ISchedule { void Run(); } }
حال، پیاده سازی خود را برای این interface ارائه میدهیم.
namespace SchedulerDemo.Jobs { using System; using Quartz; using Quartz.Impl; using SchedulerDemo.Interfaces; using SchedulerDemo.Jobs; public class HelloSchedule : ISchedule { public void Run() { //DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 2); DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second); IJobDetail job = JobBuilder.Create<HelloJob>() .WithIdentity("job1") .Build(); ITrigger trigger = TriggerBuilder.Create() .WithIdentity("trigger1") .StartAt(startTime) .WithSimpleSchedule(x => x.WithIntervalInSeconds(10).WithRepeatCount(2)) .Build(); ISchedulerFactory sf = new StdSchedulerFactory(); IScheduler sc = sf.GetScheduler(); sc.ScheduleJob(job, trigger); sc.Start(); } } }
معرفی فضاهای نام Quartz و Quartz.Impl را فراموش نکنید.
از حالا، به روالی که قرار است به صورت زمانبندی شده اجرا شود، "وظیفه" میگوییم.
ابتدا باید مشخص کنیم که وظیفه در چه زمانی پس از اجرای برنامه شروع به اجرا کند. از آنجا که پایه و اساس زمانبندی، بر تاریخ و ساعت استوار است، کتابخانهی Quartz.NET، روشها و امکانات بسیاری را برای تعیین زمان در اختیار قرار میدهد. با بررسی تمامی آنها، سادهترین و منعطفترین را به شما معرفی میکنم. کلاس DateBuilder که همراه با Quartz.NET وجود دارد، امکان تعیین زمان را به اَشکال مختلف میدهد. در خط 14، از متد FutureDate این کلاس استفاده شده است که خوانایی بهتری نسبت به بقیهی متدها دارد. پارامتر اول این متد، عدد، و پارامتر دوم، واحد زمانی را میپذیرد.
DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second);
در اینجا، زمان آغاز وظیفه را 2 ثانیه پس از آغاز برنامه تعریف کرده ایم. واحدهای زمانی دیگر شامل میلی ثانیه، دقیقه، ساعت، روز، ماه، هفته و سال هستند. کلاس DateBuilder، متدهای مختلفی برای تعیین زمان را در اختیار قرار میدهد. تعیین زمان آغاز به روش دیگر را به صورت کامنت شده در خط 13 مشاهده میکنید.
وظیفهی ایجاد شده در خط 16 تا 18 معرفی شده است.
IJobDetail job = JobBuilder.Create<HelloJob>() .WithIdentity("job1") .Build();
پشتیبانی Quartz.NET از سینتکس fluent، کدنویسی را ساده و لذت بخش میکند. با استفاده از متد Create کلاس JobBuilder، وظیفه را معرفی میکنیم. متد Create، یک متد Generic است که نام کلاسی که اینترفیس IJob را پیاده سازی کرده است میپذیرد. یک نام را با استفاده از متد WithIdentity به وظیفه نسبت میدهیم (البته این کار، اختیاری است) و در انتها، متد Build را فراخوانی میکنیم. خروجی متد Build، از نوع IJobDetail است.
و حالا نوبت به تنظیمات زمان رسیده است. در Quartz.NET، این مرحله، "ایجاد trigger" نام دارد. خطوط 20 تا 24 به این کار اختصاص دارند.
ITrigger trigger = TriggerBuilder.Create() .WithIdentity("trigger1") .StartAt(startTime) .WithSimpleSchedule(x => x.WithIntervalInSeconds(10).WithRepeatCount(2)) .Build();
ابتدا متد Create کلاس TriggerBuilder را فراخوانی میکنیم، سپس با استفاده از متد WithIdentity، یک نام به trigger اختصاص میدهیم (البته این کار، اختیاری است). با متد StartAt، زمان شروع وظیفه را که در ابتدا با استفاده از کلاس DateBuilder ایجاد کردیم تعیین میکنیم. مهمترین قسمت، تعیین دفعات و فواصل زمانی اجرای وظیفه است. همان طور که احتمالاً حدس زده اید، Quartz.NET مجموعه ای غنی از روشهای مختلف برای تعیین بازهی زمانی اجرا را در اختیار قرار میدهد. آسانترین راه، استفاده از متد WithSimpleSchedule است. با استفاده از یک عبارت Lambda که ورودی آن از نوع کلاس SimpleScheduleBuilder است، دفعات و فواصل زمانی اجرا را تعیین میکنیم. متد WithIntervalInSeconds، برای تعیین فواصل زمانی در بازهی ثانیه استفاده میشود. متد WithRepeatCount نیز برای تعیین دفعات اجرا است. وظیفهی ما، 3 مرتبه و در فواصل زمانی 10 ثانیه اجرا میشود. مطمئن باشید اشتباه نکردم! بله، سه مرتبه. تعداد دفعات اجرا برابر است با عددی که برای متد WithRepeatCount تعیین میکنید، به علاوهی یک. منطقی است، چون مرتبهی اول اجرا زمانی است که با استفاده از متد StartAt تعیین کرده اید. در پایان، متد Build را فراخوانی میکنیم. خروجی متد Build، از نوع ITrigger است.
آخرین کار (خطوط 26 تا 30)، ایجاد شی از اینترفیس IScheduler، فراخوانی متد ScheduleJob آن، و پاس دادن اشیای job و trigger که در قسمت قبل ایجاد شده اند به این متد است. در انتها، متد ()Start را برای آغاز وظیفه فراخوانی میکنیم.
ISchedulerFactory sf = new StdSchedulerFactory(); IScheduler sc = sf.GetScheduler(); sc.ScheduleJob(job, trigger); sc.Start();
حال شما یک وظیفه تعریف کرده اید که در هر جای برنامه به صورت زیر، قابل فراخوانی است.
ISchedule myTask = new HelloSchedule(); myTask.Run();
کتابخانه ای که با آن سر و کار داریم بسیار غنی است و امکانات بسیاری دارد. در قسمت بعد، با برخی امکانات دیگر این کتابخانه آشنا میشوید.
اکثر کنترلهای تعیین اعتبار ASP.Net بر اساس جاوا اسکریپت کار میکنند (مانند RangeValidator و امثال آن). حال اگر کاربری افزونه no script فایرفاکس را نصب کرده بود چه باید کرد؟
با استفاده از این افزونه، این نوع کنترلها از کار خواهند افتاد (چون دیگر کدهای جاوا اسکریپتی آنها اجرا نخواهند شد).
خوشبختانه برای بررسی صحت عملکرد این کنترلها در ASP.Net امکان بررسی خاصیت Page.IsValid نیز وجود دارد که در ادامه به آن خواهیم پرداخت.
صفحهی بسیار ساده ASP.Net زیر را در نظر بگیرید:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm3.aspx.cs" Inherits="testWebForms87.WebForm3" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="txtData" runat="server"></asp:TextBox>
<asp:RangeValidator ID="RangeValidator1" runat="server" ControlToValidate="txtData"
ErrorMessage="لطفا یک عدد وارد کنید" MaximumValue="100000" MinimumValue="0" SetFocusOnError="True"
Type="Integer"></asp:RangeValidator>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="txtData"
ErrorMessage="لطفا مقداری را وارد نمائید" SetFocusOnError="True"></asp:RequiredFieldValidator>
<br />
<asp:Button ID="btnSubmit" runat="server" OnClick="btnSubmit_Click" Text="Submit" />
<br />
<asp:Label ID="lblValue" runat="server"></asp:Label>
</div>
</form>
</body>
</html>
سپس برای بررسی سمت سرور عملکرد کنترلهای تعیین اعتبار در ASP.Net میتوان به صورت زیر عملکرد:
protected void btnSubmit_Click(object sender, EventArgs e)
{
if (btnSubmit.CausesValidation)
{
// Validate the Page
Page.Validate();
// Ensure Page is Valid
if (!Page.IsValid)
{
lblValue.Text = "لطفا جاوا اسکریپت را در مرورگر خود فعال نمائید";
return;
}
}
lblValue.Text = txtData.Text;
}
راه دیگر بررسی غیرفعال بودن جاوا اسکریپت در یک صفحه استفاده از روش سنتی تگ noscript است:
<noscript>
<meta http-equiv="refresh" content="0;url=http://www.google.com">
</noscript>
در پایان باید خاطر نشان کرد که هیچگاه به کنترلهای تعیین اعتبار سمت کاربر اطمینان نکنید و حتما یا از روش فوق استفاده نمائید، یا در روال submit صفحه به سرور، یکبار دیگر دادهها را به صورت دستی نیز بررسی کنید. برای مثال اگر کاربر قرار است آدرس ایمیلی را وارد کند، حتما یکبار دیگر با استفاده از regular expressions ، در سمت سرور نیز عبارت ورودی را بررسی کنید.
public ActionResult Style() { Response.ContentType = "text/css"; var model = new Style { Color = "red", Background = "blue" }; return View(model); }
@model ExternalJavaScript.Models.Style @{ Layout = null; } body { color : @Model.Color; background-color : @Model.Background; }
<link rel="stylesheet" href="@Url.Action("Style","Home")" />
public class ContentType : ActionFilterAttribute { private string _contentType; public ContentType(string ct) { this._contentType = ct; } public override void OnActionExecuted(ActionExecutedContext context) { /* nada */ } public override void OnActionExecuting(ActionExecutingContext context) { context.HttpContext.Response.ContentType = this._contentType; } }
[ContentType("text/css")] public ActionResult Style() { var model = new Style { Color = "red", Background = "blue" }; return View(model); }
برای فایلهای JS نیز میتوانیم از یک View به عنوان محل قرارگیری کدهای جاوا اسکریپت استفاده کنیم:
public class JavaScriptSettingsController : Controller { public ActionResult Index() { return PartialView(); } }
$(function(){ $.post('@Url.Action("GetData", "Home")', function (data) { $('.notificationList').html(data); if ($(data).filter("li").length != 0) { $('#notificationCounter').html($(data).filter("li").length); } }); });
<script src="/JavaScriptSettings"></script>
<script> $(function () { $.post('@Url.Action("Index", "Home")', function (data) { $('.notificationList').html(data); if ($(data).filter("li").length != 0) { $('#notificationCounter').html($(data).filter("li").length); } }); }); </script>
public class ExternalFileAttribute : ActionFilterAttribute { private readonly string _contentType; private readonly string _tag; public ExternalFileAttribute(string ct, string tag) { this._contentType = ct; _tag = tag; } public override void OnResultExecuted(ResultExecutedContext filterContext) { var response = filterContext.HttpContext.Response; response.Filter = new StripEnclosingTagsFilter(response.Filter, _tag); response.ContentType = _contentType; } private class StripEnclosingTagsFilter : MemoryStream { private static Regex _leadingOpeningScriptTag; private static Regex _trailingClosingScriptTag; //private static string Tag; private readonly StringBuilder _output; private readonly Stream _responseStream; /*static StripEnclosingTagsFilter() { LeadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", Tag), RegexOptions.Compiled); TrailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", Tag), RegexOptions.Compiled); }*/ public StripEnclosingTagsFilter(Stream responseStream, string tag) { _leadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", tag), RegexOptions.Compiled); _trailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", tag), RegexOptions.Compiled); _responseStream = responseStream; _output = new StringBuilder(); } public override void Write(byte[] buffer, int offset, int count) { string response = GetStringResponse(buffer, offset, count); _output.Append(response); } public override void Flush() { string response = _output.ToString(); if (_leadingOpeningScriptTag.IsMatch(response) && _trailingClosingScriptTag.IsMatch(response)) { response = _leadingOpeningScriptTag.Replace(response, string.Empty); response = _trailingClosingScriptTag.Replace(response, string.Empty); } WriteStringResponse(response); _output.Clear(); } private static string GetStringResponse(byte[] buffer, int offset, int count) { byte[] responseData = new byte[count]; Buffer.BlockCopy(buffer, offset, responseData, 0, count); return Encoding.Default.GetString(responseData); } private void WriteStringResponse(string response) { byte[] outdata = Encoding.Default.GetBytes(response); _responseStream.Write(outdata, 0, outdata.GetLength(0)); } } }
[ExternalFile("text/javascript", "script")] public ActionResult Index() { return PartialView(); }
[ExternalFile("text/css", "style")] public ActionResult Style() { var model = new Style { Color = "red", Background = "blue" }; return View(model); }
ایجاد PDF در Blazor با iTextSharp
- اگر از attribute routing استفاده میکنید، متد MapControllers را فراخوانی کنید که البته به صورت ضمنی هم فراخوانی میشود.
- متد MapRoute پیشین به MapControllerRoute تغییر نام یافتهاست.
- متد MapAreaRoute پیشین به MapAreaControllerRoute تغییر نام یافتهاست.
علت این نوع نامگذاریهای صریح، این است که اکنون سیستم مسیریابی، فراتر از سیستم MVC را پشتیبانی میکند.
- همانند سابق متدهای MapControllerRoute/MapAreaControllerRoute/MapDefaultControllerRoute به ترتیبی که تعریف میشوند، تاثیرگذار خواهند بود.
در مثال زیر:
public void Configure(IApplicationBuilder app) { ... app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapAreaControllerRoute( "admin", "admin", "Admin/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( "default", "{controller=Home}/{action=Index}/{id?}"); }); }
- متد MapAreaControllerRoute، مسیریابی سنتی کنترلرهای درون یک Area را تعریف میکند.
- متد MapControllerRoute، مسیریابی سنتی کنترلرها را تعریف میکند.
اگر از Razor pages استفاده میکنید، در اینجا باید متد MapRazorPages را نیز فراخوانی کنید:
public void Configure(IApplicationBuilder app) { ... app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); }
Media Type یا MIME Type نشان دهنده فرمت یک مجموعه داده است. در HTTP، مدیا تایپ بیان کننده فرمت message body یک درخواست / پاسخ است و به دریافت کننده اعلام میکند که چطور باید پیام را بخواند. محل استاندارد تعیین Mime Type در هدر Content-Type است. درخواست کننده میتواند با استفاده از هدر Accept لیستی از MimeTypeهای قابل قبول را به عنوان پاسخ، به سرور اعلام کند.
Asp.net Web API از MimeType برای تعیین نحوه serialize یا deserialize کردن محتوای دریافتی / ارسالی استفاده میکند
MediaTypeFormatter
Web API برای خواندن/درج پیام در بدنه درخواست/پاسخ از MediaTypeFormmaterها استفاده میکند. اینها کلاسهایی هستند که نحوهی Serialize کردن و deserialize کردن اطلاعات به فرمتهای خاص را تعیین میکنند. Web API به صورت توکار دارای formatter هایی برای نوعهای XML ، JSON، BSON و Form-UrlEncoded میباشد. همه اینها کلاس پایه MediaTypeFormatter را پیاده سازی میکنند.
مسئله
یک پروژه Web API بسازید و view model زیر را در آن تعریف کنید:
public class NewProduct { [Required] public string Name { get; set; } public double Price { get; set; } public byte[] Pic { get; set; } }
همانطور که میبینید یک فیلد از نوع byte[] برای تصویر محصول در نظر گرفته شده است.
حالا یک کنترلر API ساخته و اکشنی برای دریافت اطلاعات محصول جدید از کاربر مینویسیم :public class ProductsController : ApiController { [HttpPost] public HttpResponseMessage PostProduct(NewProduct model) { if (ModelState.IsValid) { // ثبت محصول return new HttpResponseMessage(HttpStatusCode.Created); } return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } }
و یک صفحه html به نام index.html که حاوی یک فرم برای ارسال اطلاعات باشد :
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <h1>ساخت MediaTypeFormatter برای Multipart/form-data</h1> <h2>محصول جدید</h2> <form id="newProduct" method="post" action="/api/products" enctype="multipart/form-data"> <div> <label for="name">نام محصول : </label> <input type="text" id="name" name="name" /> </div> <div> <label for="price">قیمت : </label> <input type="number" id="price" name="price" /> </div> <div> <label for="pic">تصویر : </label> <input type="file" id="pic" name="pic" /> </div> <div> <button type="submit">ثبت</button> </div> </form> </body> </html>
زمانی که فرم حاوی فایلی برای آپلود باشد مشخصه encType باید برابر با Multipart/form-data مقداردهی شود تا اطلاعات فایل به درستی کد شوند. در زمان ارسال فرم Content-type درخواست برابر با Multipart/form-data و فرمت اطلاعات درخواست ارسالی به شکل زیر خواهد بود :
همانطور که میبینید هر فیلد در فرم، در یک بخش جداگانه قرار گرفته است که با خط چین هایی از هم جدا شده اند. هر بخش، headerهای جداگانه خود را دارد.
- Content-Disposition که نام فیلد و نام فایل را شامل میشود .
- content-type که mime type مخصوص آن بخش از دادهها را مشخص میکند.
پس از اینکه فرم را تکمیل کرده و ارسال کنید ، با پیام خطای زیر مواجه میشوید :
خطای روی داده اعلام میکند که Web API فاقد MediaTypeFormatter برای خواندن اطلاعات ارسال شده با فرمتMultiPart/Form-data است. Web API برای خواندن و بایند کردن پارامترهای complex Type از درون بدنه پیام یک درخواست از MediaTypeFormatter استفاده میکند و همانطور که گفته شد Web API فاقد Formatter توکار برای deserialize کردن دادههای با فرمت Multipart/form-data است.
راه حلها :روشی که در سایت asp.net برای آپلود فایل در web api استفاده شده، عدم استفاده از پارامترها و خواندن محتوای Request در درون کنترلر است. که به طبع در صورتی که بخواهیم کنترلرهای تمیز و کوچکی داشته باشیم روش مناسبی نیست. از طرفی امتیاز parameter binding و modelstate را هم از دست خواهیم داد.
روش دیگری که میخواهیم در اینجا پیاده سازی کنیم ساختن یک MediaTypeFormatter برای خواندن فرمت Multipart/form-data است. با این روش کد موردنیاز کپسوله شده و امکان استفاده از binding و modelstate را خواهیم داشت.
برای ساختن یک MediaTypeFormatter یکی از 2 کلاس MediaTypeFormatter یا BufferedMediaTypeFormatter را باید پیاده سازی کنیم . تفاوت این دو در این است که BufferedMediaTypeFormatter برخلاف MediaTypeFormatter از متدهای synchronous استفاده میکند.
یک کلاس به نام MultiPartMediaTypeFormatter میسازیم و کلاس MediaTypeFormatter را به عنوان کلاس پایه آن قرار میدهیم .
public class MultiPartMediaTypeFormatter : MediaTypeFormatter { ... }
ابتدا در تابع سازنده کلاس فرمت هایی که میخواهیم توسط این کلاس خوانده شوند را تعریف میکنیم :
public MultiPartMediaTypeFormatter() { SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); }
سپس با پیاده سازی توابع CanReadType و CanWriteType مربوط به کلاس MediaTypeFormatter مشخص میکنیم که چه مدلهایی را میتوان توسط این کلاس serialize / deserialize کرد. در اینجا چون میخواهیم این کلاس محدود به یک مدل خاص نباشد، از یک اینترفیس برای شناسایی کلاسهای مجاز استفاده میکنیم .
public interface INeedMultiPartMediaTypeFormatter { }
و آنرا به کلاس newProduct اضافه میکنیم :
public class NewProduct : INeedMultiPartMediaTypeFormatter { ... }
public override bool CanReadType(Type type) { return typeof(INeedMultiPartMediaTypeFormatter).IsAssignableFrom(type); } public override bool CanWriteType(Type type) { return false; }
و اما تابع ReadFromStreamAsync که کار خواندن محتوای ارسال شده و بایند کردن آنها به پارامترها را برعهده دارد
public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
ابتدا محتوای ارسال شده را خوانده و اطلاعات فرم را استخراج میکنیم و از طرف دیگر با استفاده از کلاس Activator یک نمونه از مدل جاری را ساخته و لیست propertyهای آنرا استخراج میکنیم.
MultipartMemoryStreamProvider provider = await content.ReadAsMultipartAsync(); IEnumerable<HttpContent> formData = provider.Contents.AsEnumerable(); var modelInstance = Activator.CreateInstance(type); IEnumerable<PropertyInfo> properties = type.GetProperties();
سپس در یک حلقه به ترتیب برای هر property متعلق به مدل، در میان اطلاعات فرم جستجو میکنیم. برای پیدا کردن اطلاعات متناظر با هر property در هدر Content-Disposition که در بالا توضیح داده شد، به دنبال فیلد همنام با property میگردیم .
foreach (PropertyInfo prop in properties) { var propName = prop.Name.ToLower(); var propType = prop.PropertyType; var data = formData.FirstOrDefault(d => d.Headers.ContentDisposition.Name.ToLower().Contains(propName));
گفتیم که هر فیلد یک هدر، Content-Type هم میتواند داشته باشد. این هدر به صورت پیش فرض معادل text/plain است و برای فیلدهای عادی قرار داده نمیشود . در این مثال چون فقط یک
فیلد غیر رشته ای داریم فرض را بر این گرفته ایم که در صورت وجود Content-Type، فیلد مربوط به تصویر است. در صورتیکهContentType وجود داشته باشد، محتوای فیلد را به شکل Stream
خوانده به byte[] تبدیل و با استفاده از متد SetValue در property مربوطه قرار میدهیم.
if (data != null) { if (data.Headers.ContentType != null) { using (var fileStream = await data.ReadAsStreamAsync()) { using (MemoryStream ms = new MemoryStream()) { fileStream.CopyTo(ms); prop.SetValue(modelInstance, ms.ToArray()); } } }
در صورتی که Content-Type غایب باشد بدین معنی است که محتوای فیلد از نوع رشته است ( عدد ، تاریخ ، guid ، رشته ) و باید به نوع مناسب تبدیل شود. ابتدا آن را به صورت یک رشته میخوانیم و با استفاده از Convert.ChangeType آنرا به نوع مناسب تبدیل میکنیم و در property متناظر قرار میدهیم .
if (data != null) { if (data.Headers.ContentType != null) { //... } else { string rawVal = await data.ReadAsStringAsync(); object val = Convert.ChangeType(rawVal, propType); prop.SetValue(modelInstance, val); } }
return modelInstance;
config.Formatters.Add(new MultiPartMediaTypeFormatter());
چرا TypeScript؟
- TypeScript زبان توصیه شدهی توسعهی برنامههای AngularJS 2 است و همچنین با سایر کتابخانههای معروف جاوا اسکریپتی مانند ReactJS و jQuery نیز سازگاری دارد. بنابراین اگر قصد دارید به AngularJS 2 مهاجرت کنید، اکنون فرصت خوبی است تا زبان TypeScript را نیز بیاموزید. همچنین WinJS نیز با TypeScript نوشته شدهاست.
- superset زبان JavaScript بودن به این معنا است که تمام کدهای جاوا اسکریپتی موجود، به عنوان کد معتبر TypeScript نیز شناخته میشوند و همین مساله مهاجرت به آنرا سادهتر میکند. زبانهای دیگری مانند Dart و یا CoffeeScript ، نسبت به JavaScript بسیار متفاوت به نظر میرسند؛ اما Syntax زبان TypeScript شباهت بسیار زیادی به جاوا اسکریپت و خصوصا ES 6 دارد. در اینجا تنها کافی است پسوند فایلهای js را به ts تغییر دهید و از آنها به عنوان کدهای معتبر TypeScript استفاده کنید.
- strong typing و معرفی نوعها، کدهای نهایی نوشته شده را امنتر میکنند. به این ترتیب کامپایلر، پیش از اینکه کدهای شما در زمان اجرا به خطا بر بخورند، در زمان کامپایل، مشکلات موجود را گوشزد میکند. همچنین وجود نوعها، سرعت توسعه را با بهبود ابزارهای مرتبط با برنامه نویسی، افزایش میدهند؛ از این جهت که مفهوم مهمی مانند Intellisense، با وجود نوعها، پیشنهادهای بهتر و دقیقتری را ارائه میدهد. همچنین ابزارهای Refactoring نیز در صورت وجود نوعها بهتر و دقیقتر عمل میکنند. این موارد مهمترین دلایل طراحی TypeScript جهت توسعه و نگهداری برنامههای بزرگ نوشته شدهی با JavaScript هستند.
- Syntax زبان TypeScript به شدت الهام گرفته شده از زبان سیشارپ است. به همین جهت اگر با این زبان آشنایی دارید، درک مفاهیم TypeScript برایتان بسیار ساده خواهد بود.
- بهترین قسمت TypeScript، کامپایل شدن آن به ES 5 است (به این عملیات Transpile هم میگویند). در زبان TypeScript به تمام امکانات پیشرفتهی ES 6 مانند کلاسها و ماژولها دسترسی دارید، اما کد نهایی را که تولید میکند، میتواند ES 5 ایی باشد که هم اکنون تمام مرورگرهای عمده آنرا پشتیبانی میکنند. با تنظیمات کامپایلر TypeScript، امکان تولید کدهای ES 3 تا ES 5 و همچنین ES 6 نیز وجود دارد. نمونهی آنلاین این ترجمه را در TypeScript playground میتوانید مشاهده کنید.
- TypeScript چندسکویی است. امکانات و کامپایلر این زبان، برای ویندوز، مک و لینوکس طراحی شدهاند.
- TypeScript سورس باز است. طراحان اصلی آن، همان طراحان زبان سیشارپ در مایکروسافت هستند و هم اکنون این زبان به صورت سورس باز توسط این شرکت توسعه داده شده و در GitHub نگهداری میشود.
آماده سازی محیطهای کار با TypeScript
برای کار با TypeScript، یک ادیتور متنی ساده، به همراه کامپایلر آن کفایت میکند. اما همانطور که عنوان شد، یکی از مهمترین دلایل وجودی TypeScript، بهبود ابزارهای برنامه نویسی مرتبط با JavaScript است و اگر قرار باشد صرفا از یک ادیتور متنی ساده استفاده شود، فلسفهی وجودی آن زیر سؤال میرود.
نصب TypeScript در ویژوال استودیو
در نگارشهای جدید ویژوال استودیو، از VS 2013 Update 2 به بعد، قسمت ویژهی TypeScript نیز قابل مشاهدهاست. البته این قسمت با به روز رسانیهای TypeScript، نیاز به به روز رسانی دارد. به همین جهت به سایت رسمی آن مراجعه کرده و بستههای جدید مخصوص VS 2013 و یا 2015 آنرا دریافت و نصب کنید.
همچنین افزونهی Web Essentials نیز امکانات بیشتری را جهت کار با TypeScript به همراه دارد و امکان مشاهدهی خروجی جاوا اسکریپت تولیدی را در حین کار با فایل TypeScript فعلی میسر میکند. در سمت چپ صفحه TypeScript را خواهید نوشت و در سمت راست، خروجی JavaScript نهایی را بلافاصله مشاهده میکنید.
تصویر فوق مربوط به VS 2015 است. همچنین گزینهی افزودن یک فایل و آیتم جدید نیز امکان افزودن فایلهای TS را به همراه دارد.
نصب و تنظیم TypeScript در ویژوال استودیو کد
ویژوال استودیو کد، نگارش رایگان، سورس باز و چندسکویی ویژوال استودیو است که بر روی ویندوز، مک و لینوکس قابل اجرا است. ویژوال استودیو کد نیز به همراه پشتیبانی بسیار خوبی از TypeScript است، تا حدی که تمام ارائههای معرفی Anugular 2 توسط تیم مربوطهی آن از گوگل، توسط ویژوال استودیو کد و یکپارچگی آن با TypeScript انجام شدند.
ویژوال استودیو کد بر مبنای فولدرها کار میکند و با گشودن یک پوشه در آن (با کلیک بر روی دکمهی open folder آن)، امکان کار کردن با آن پوشه و فایلهای موجود در آن را خواهیم یافت.
نکتهی مهم اینجا است که پس از نصب VS Code، برای فایلهای با پسوند ts بلافاصله Intellisense مرتبط نیز مهیا است و نیاز به هیچگونه تنظیم اضافهتری ندارد. همچنین قابلیتهای type safety این زبان نیز در این ادیتور به نحو واضحی مشخص هستند:
در ادامه ابتدا یک پوشهی جدید خالی را ایجاد کنید و سپس این پوشه را در VS Code باز نمائید (از طریق منوی فایل، گزینهی گشودن پوشه). سپس ماوس را بر روی نام این پوشه حرکت دهید:
همانطور که مشاهده میکنید، دکمهی new file ظاهر میشود. در اینجا میتوانید فایل جدیدی را به نام test.ts اضافه کنید.
در ادامه با فشردن دکمههای ctrl+shift+p، امکان انتخاب یک task runner را جهت کامپایل فایلهای ts خواهیم داشت:
در اینجا ابتدا عبارت task< را وارد کنید و سپس از منوی باز شده، گزینهی rub build task را انتخاب کنید:
پس از آن، در بالای صفحه مشاهده خواهید کرد که عنوان شده: «هنوز هیچ task runner ایی برای اینکار تنظیم نشدهاست»
برای این منظور بر روی دکمهی configure task runner تصویر فوق که با رنگ آبی مشخص شدهاست، کلیک کنید. به این ترتیب یک فایل جدید به نام task.json ایجاد میشود که در پوشهای به نام vscode. در ریشهی پروژه (یا همان پوشهی جاری) قرار میگیرد:
فایل task.json دارای تعاریفی است که کامپایلر TypeScript یا همان tsc را فعال میکند:
{ "version": "0.1.0", // The command is tsc. Assumes that tsc has been installed using npm install -g typescript "command": "tsc", // The command is a shell script "isShellCommand": true, // Show the output window only if unrecognized errors occur. "showOutput": "silent", // args is the HelloWorld program to compile. "args": ["HelloWorld.ts"], // use the standard tsc problem matcher to find compile problems // in the output. "problemMatcher": "$tsc" }
در اینجا قسمتی که نیاز به تنظیم دارد، خاصیت args است. مقادیر آن، پارامترهایی هستند که به کامپایلر typescript ارسال میشوند. برای نمونه آنرا به صورت ذیل تغییر دهید:
"args": [ "--target", "ES5", "--outdir", "js", "--sourceMap", "--watch", "test.ts" ],
برای اجرای کامپایلر، ابتدا از منوی view گزینهی toggle output را انتخاب کنید تا بتوان خروجی نهایی کامپایلر را مشاهده کرد. سپس گزینهی view->command pallet و اجرا tasks< را انتخاب کنید. در ادامه همانند مرحلهی قبل، یعنی گزینهی run build task را اجرا کنید (که خلاصهی این عملیات ctrl+shift+B است).
به این ترتیب پوشهی js که در خاصیت args مشخص کردیم، تولید میشود:
البته این خطا هم در قسمت output نمایش داده میشود:
error TS5023: Unknown option 'watch' Use the '--help' flag to see options.
علت اینجا است که در تنظیمات فوق، خاصیت command به tsc تنظیم شدهاست و همانطور که در کامنت آن عنوان شدهاست، کامپایلر typescript را از طریق دستور npm install -g typescript دریافت میکند و نیازی به ذکر مسیر آن در اینجا نیست. بنابراین لازم است تا با npm و نصب typescript از طریق آن آشنا شد و به این ترتیب کامپایلر آنرا به روز کرد تا دستور watch را شناسایی کند.
نصب TypeScript از طریق npm
همانطور که عنوان شد، TypeScript چندسکویی است و این مورد را از طریق npm یا NodeJS package manager انجام میدهد. برای این منظور به آدرس https://nodejs.org/en مراجعه کرده و فایل نصاب آنرا مخصوص سیستم عامل خود دریافت و سپس نصب کنید. Node.js یک runtime سمت سرور اجرای برنامههای جاوا اسکریپتی است. از آنجائیکه TypeScript در نهایت به JavaScript تبدیل میشود، استفاده از node.js انتخاب مناسبی جهت اجرا و توزیع آن در تمام سیستم عاملها بودهاست.
پس از نصب node.js، از package manager آن که npm نام دارد، جهت نصب TypeScript استفاده میشود. چون node.js به Path و مسیرهای اصلی ویندوز اضافه میشود، تنها کافی است دستور npm install -g typescript را در خط فرمان صادر کنید. در اینجا سوئیچ g به معنای global و دسترسی عمومی است.
همانطور که در این تصویر مشخص است، پس از صدور دستور نصب TypeScript، نگارش 1.8.9 آن نصب شدهاست. اما زمانیکه کامپایلر tsc را با پارامتر version اجرا میکنیم، شماره نگارش قدیمی 1.0.3.0 را نمایش میدهد. برای رفع این مشکل به مسیر C:\Program Files (x86)\Microsoft SDKs\TypeScript مراجعه کرده و پوشهی 1.0 را به 1.0-old تغییر نام دهید.
اکنون اگر مجددا بررسی کنیم، نگارش صحیح قابل مشاهده است:
پس از این تغییرات اگر مجددا به VS Code باز گردیم و ctrl+shift+B را صادر کنیم (جهت اجرای مجدد task runner و اجرای tsc تنظیم شده) ، پیام ذیل مشاهده میشود:
15:33:52 - Compilation complete. Watching for file changes.
در اینجا چون پارامتر watch فعال شدهاست، هر تغییری که در فایل ts داده شود، بلافاصله کامپایل شده و در فایل js منعکس خواهد شد.
تنظیم VS Code جهت دیباگ کدهای TypeScript
در نوار ابزار کنار صفحهی VS Code، بر روی دکمهی دیباگ کلیک کنید:
سپس بر روی دکمهی چرخدندهی موجود که کار انجام تنظیمات را توسط آن میتوان ادامه داد، کلیک کنید. بلافاصله منویی ظاهر میشود که درخواست انتخاب محیط دیباگ را دارد:
در اینجا node.js را انتخاب کنید. با اینکار فایل جدیدی دیگری به نام launch.json به پوشهی vscode. اضافه میشود. اگر به این فایل دقت کنید دو خاصیت name به نامهای Launch و Attach در آن موجود هستند. این نامها در یک دراپ داون، در کنار دکمهی start دیباگ نیز ظاهر میشوند:
- در فایل launch.json، باید خاصیت "program": "${workspaceRoot}/app.js" را ویرایش کرد و app.js آنرا به test.ts مثال جاری تغییر داد.
- سپس خاصیت "sourceMaps" آن نیز باید تغییر کرده و جهت استفادهی از source mapهای تولیدی به true تنظیم شود.
- در آخر باید مسیر پوشهی خروجی js را نیز تنظیم کرد: "outDir": "${workspaceRoot}/js"
همچنین باید دقت داشت چون externalConsole به false تنظیم شدهاست، خروجی این کنسول به output ویژوال استودیوکد منتقل میشود.
اکنون اگر بر روی دکمهی سبز رنگ start کلیک کنید (دکمهی F5)، امکان دیباگ سطر به سطر کد TypeScript را خواهید یافت:
فایلهای نهایی json یاد شدهی در متن را از اینجا میتوانید دریافت کنید:
VSCodeTypeScript.zip
- Workflow: یکی از مفاهیمی که باید با آن آشنا باشیم workflowها هستند. یک workflow مجموعهایی از jobهایی هستند که در رخدادهای خاصی اجرا میشوند. در واقع یک workflow یک CI pipeline است که با کمک YAML آنها را تعریف میکنیم.
- Runner: اینها به اصطلاح compute machineهایی هستند که workflowها را اجرا میکنند. این runnerها هم میتوانند به صورت سفارشی باشند و هم سرویسهای ارائه شده توسط GitHub باشند.
- Job: مجموعهایی از مراحلی که درون یک runner workspace اجرا میشوند.
- Step: در نهایت stepها هستند که کوچکترین بخش GitHub Actions هستند. stepها میتوانند فایل اسکریپت، Dockerfile یا یک community action باشند.
name: Build Application Code on: [push] jobs: build: runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v2 - name: Install Libraries uses: pip install -r requirements.txt -t . test: runs-on: ubuntu-latest needs: build steps: ...
├── .github │ ├── scripts │ └── workflows ├── README.md ├── assets └── deps
name: Update Recent Blog Posts on: schedule: - cron: "0 0 * * 0" # Run once a week at 00:00 (midnight) on Sunday workflow_dispatch: jobs: update_posts: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v3 - name: Run the script for fetching latest blog posts shell: pwsh run: | . ./.github/scripts/Get-Posts.ps1 - name: Commit and Push the changes uses: mikeal/publish-to-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Function Get-Posts { Param ( [Parameter(Mandatory = $false)] [string]$rssUrl ) $posts = @() $feed = [xml](Invoke-WebRequest -Uri $rssUrl).Content $feed.rss.channel.item | Select-Object -First 3 | ForEach-Object { $post = [PSCustomObject]@{ Title = $_.title."#cdata-section" ?? $_.title Link = $_.link Description = $_.description."#cdata-section" ?? $_.description PubDate = $_.pubDate } $posts += $post } $posts } Function Get-DntipsPosts { $assemblyPath = "$(Get-Location)/deps/CodeHollow.FeedReader.dll" [Reflection.Assembly]::LoadFile($assemblyPath) $feed = [CodeHollow.FeedReader.FeedReader]::ReadAsync("https://www.dntips.ir/feed/author/%d8%b3%db%8c%d8%b1%d9%88%d8%a7%d9%86%20%d8%b9%d9%81%db%8c%d9%81%db%8c").Result $posts = @() $feed.Items | Select-Object -First 3 | ForEach-Object { $post = [PSCustomObject]@{ Title = $_.Title Link = $_.Link Description = $_.Description PubDate = $_.PublishingDate } $posts += $post } $posts } Function Set-Posts { [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSCustomObject[]]$posts, [Parameter(Mandatory = $false)] [string]$marker = "## Recent Blog Posts - English" ) Begin { $readMePath = "./README.md" $readmeContents = Get-Content -Path $readMePath -Raw $markdownTable = "| Link | Published At |`n" $markdownTable += "| --- | --- |`n" } Process { if ($null -eq $_.Title) { return } $date = Get-Date -Date $_.PubDate $link = "[$($_.Title)]($($_.Link))" $markdownTable += "| $($link) | $($date.ToString("dd/MM/yy")) |`n" } End { $updatedContent = $readmeContents -replace "$marker\n([\s\S]*?)(?=#| $)", "$marker`n$($markdownTable)`n" $updatedContent | Set-Content -Path $readMePath } } Function Set-Blogs { $recentBlogPostsStr = "## Recent blog posts -" Get-Posts("https://dev.to/feed/sirwanafifi") | Set-Posts -marker "$recentBlogPostsStr dev.to" Get-Posts("https://sirwan.infohttps://www.dntips.ir/rss.xml") | Set-Posts -marker "$recentBlogPostsStr sirwan.info" Get-DntipsPosts | Set-Posts -marker "$recentBlogPostsStr dntips.ir" } Set-Blogs
name: Update Step Component on: schedule: - cron: "0 18 * * *" workflow_dispatch: jobs: update_steps: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v3 - name: Run the script for fetching my latest steps shell: pwsh env: STEPS_URI: ${{ secrets.STEPS_URI }} run: | . ./.github/scripts/Get-Steps.ps1 - name: Commit and Push the changes uses: mikeal/publish-to-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Function Set-Steps { Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSObject]$json ) Write-Host ($json | ConvertTo-Json) $SvgPath = "$(Get-Location)/assets/step.svg" $SvgContent = Get-Content -Path $SvgPath -Raw $TextTags = @" <tspan id="step-count" font-weight="bold">$([System.String]::Format("{0:n0}", [int]$json.steps))</tspan> "@ $DatetimeTags = "<text id=""datetime"" x=""800"" y=""72"" font-size=""39"" fill="#99989E"">$($json.date)</text>" $SvgContent = $SvgContent -Replace '<tspan id="step-count" font-weight="bold">.*?</tspan>', $TextTags $SvgContent = $SvgContent -Replace '<text id="datetime" x="800" y="72" font-size="39" fill="#99989E">.*?</text>', $DatetimeTags $SvgContent | Set-Content -Path $SvgPath } Function Get-LatestSteps { Try { $Uri = $env:STEPS_URI Write-Host "Uri: $Uri" $JsonResult = (Invoke-WebRequest -Uri $Uri).Content | ConvertFrom-Json Write-Host "Steps: $($JsonResult.steps)" Return $JsonResult } Catch { Return @{ steps = 0 date = Get-Date -Format "yyyy-MM-dd" } } } Write-Host "Getting latest steps..." Get-LatestSteps | Set-Steps Write-Host "Done!"