مطالب
آشنایی با Refactoring - قسمت 12


قبلا در مورد تبدیل switch statement به الگوی استراتژی مطلبی را در این سایت مطالعه کرده‌اید (^) و بیشتر مربوط است به حالتی که داخل هر یک از case های یک switch statement چندین و چند سطر کد و یا فراخوانی یک تابع وجود دارد. حالت ساده‌تری هم برای refactoring یک عبارت switch وجود دارد و آن هم زمانی است که هر case، تنها از یک سطر تشکیل می‌شود؛ مانند:

namespace Refactoring.Day12.RefactoringSwitchStatement.Before
{
public class Translator
{
public string ToPersian(string englishWord)
{
switch (englishWord)
{
case "zero":
return "صفر";
case "one":
return "یک";
default:
return string.Empty;
}
}
}
}

در اینجا می‌توان از امکانات ساختار داده‌های توکار دات نت استفاده کرد و این switch statement را به یک dictionary تبدیل نمود:

using System.Collections.Generic;

namespace Refactoring.Day12.RefactoringSwitchStatement.After
{
public class Translator
{
IDictionary<string, string> Words = new Dictionary<string, string>
{
{ "zero", "صفر" },
{ "one", "یک" }
};

public string ToPersian(string englishWord)
{
string persianWord;
if (Words.TryGetValue(englishWord, out persianWord))
{
return persianWord;
}

return string.Empty;
}
}
}

همانطور که ملاحظه می‌کنید هر case به یک key و هر return به یک value در Dictionary تعریف شده، تبدیل گشته‌اند. در اینجا هم بهتر است از متد TryGetValue جهت دریافت مقدار کلیدها استفاده شود؛ زیرا در صورت فراخوانی یک Dictionary با کلیدی که در آن موجود نباشد یک استثناء بروز خواهد کرد.
برای حذف این متد TryGetValue، می‌توان یک enum را بجای کلیدهای تعریف شده، معرفی کرد. به صورت زیر:

using System.Collections.Generic;

namespace Refactoring.Day12.RefactoringSwitchStatement.After
{
public enum EnglishWord
{
Zero,
One
}

public class Translator2
{
IDictionary<EnglishWord, string> Words = new Dictionary<EnglishWord, string>
{
{ EnglishWord.Zero, "صفر" },
{ EnglishWord.One, "یک" }
};

public string ToPersian(EnglishWord englishWord)
{
return Words[englishWord];
}
}
}


به این ترتیب از یک خروجی پر از if و else و switch به یک خروجی ساده و بدون وجود هیچ شرطی رسیده‌ایم.
مطالب
مقاومت اتصال و اتصالات بهبودپذیر در Entity framework 6
Timeouts، Deadlocks و قطعی‌های احتمالی و موقت اتصال به بانک اطلاعاتی در شبکه، جزئی از ساختار دنیای واقعی هستند. در EF 6 برای پیاده سازی سعی مجدد در اتصال و انجام مجدد عملیات، ویژگی خاصی تحت عنوان connection resiliency اضافه شده‌است که در ادامه مثالی از آن‌را بررسی خواهیم کرد.

پیاده سازی‌های پیش فرض موجود

برای پیاده سازی منطق سعی مجدد در اتصال، باید اینترفیس IDbExecutionStrategy پیاده سازی شود. در EF 6 حداقل 4 نوع پیاده سازی پیش فرض از آن به صورت توکار ارائه شده‌است:
الف) DefaultExecutionStrategy : حالت پیش فرض است و در صورت بروز مشکل، سعی مجددی را در اتصال، به عمل نخواهد آورد.
ب) DefaultSqlExecutionStrategy : برای کارهای درونی EF از آن استفاده می‌شود. سعی مجددی در اتصال قطع شده نخواهد کرد؛ اما جزئیات خطاهای بهتری را در اختیار مصرف کننده قرار می‌دهد.
ج) DbExecutionStrategy : هدف از آن تهیه یک کلاس پایه است برای نوشتن استراتژی‌های سعی مجدد سفارشی.
د) SqlAzureExecutionStrategy : یک نمونه DbExecutionStrategy سفارشی تهیه شده برای ویندوز اژور است. برای فعال سازی و تعریف آن نیز باید به نحو ذیل عمل کرد:
public class MyConfiguration : DbConfiguration 
{ 
    public MyConfiguration() 
    { 
        SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy()); 
    } 
}


تهیه یک DbExecutionStrategy سفارشی برای SQL Server

همانطور که عنوان شد، هدف از کلاس DbExecutionStrategy، تهیه یک کلاس پایه، جهت نوشتن منطق سعی مجدد در اتصال به بانک اطلاعاتی است و این مورد از دیتابیسی به دیتابیس دیگر می‌تواند متفاوت باشد؛ زیرا خطاهایی را که ارائه می‌دهند، یکسان و یک دست نیستند. در ادامه یک پیاده سازی سفارشی را از DbExecutionStrategy، جهت SQL Server مرور خواهیم کرد:
    public class SqlServerExecutionStrategy : DbExecutionStrategy
    {
        public SqlServerExecutionStrategy()
        { }

        public SqlServerExecutionStrategy(int maxRetryCount, TimeSpan maxDelay)
            : base(maxRetryCount, maxDelay)
        { }

        protected override bool ShouldRetryOn(Exception ex)
        {
            var sqlException = ex as SqlException;
            if (sqlException == null)
                return false; // don't retry

            foreach (var error in sqlException.Errors.Cast<SqlError>())
            {
                switch (error.Number)
                {
                    case 1205: // Deadlock
                    case -1: // Timeout
                    case -2: // Timeout
                        return true; // retry
                }
            }

            return false;
        }
    }
در اینجا کار با بازنویسی متد ShouldRetryOn شروع می‌شود. این متد اگر پس از بررسی استثنای دریافتی، مقدار true را برگرداند، به معنای نیاز به سعی مجدد در اتصال است و برعکس. سازنده پیش فرض این کلاس طوری تنظیم شده‌است که 5 بار سعی مجدد کند؛ با فواصل زمانی 7 ثانیه. اگر می‌خواهید این زمان را صریحا تعیین کنید باید متد GetNextDelay کلاس پایه را نیز بازنویسی کرد:
   protected override TimeSpan? GetNextDelay(Exception lastException)
  {
        return base.GetNextDelay(lastException);
  }
در ادامه برای استفاده از آن خواهیم داشت:
    public class MyDbConfiguration : DbConfiguration
    {
        public MyDbConfiguration()
        {
            SetExecutionStrategy("System.Data.SqlClient", () => new SqlServerExecutionStrategy());
        }
    }
این کلاس به صورت خودکار توسط EF از اسمبلی جاری استخراج شده و استفاده خواهد شد. بنابراین نیازی نیست جایی معرفی شود. فقط باید در کدها حضور داشته باشد. همچنین ذکر System.Data.SqlClient نیز ضروری است؛ از این جهت که خطاهای بازگشت داده شده مانند 1205 و امثال آن، در بانک‌های اطلاعاتی مختلف، می‌توانند کاملا متفاوت باشند.
مطالب
از سرگیری مجدد، لغو درخواست و سعی مجدد دریافت فایل‌های حجیم توسط HttpClient
پس از آشنایی با «نکات دریافت فایل‌های حجیم توسط HttpClient»، در ادامه می‌توان سه قابلیت مهم از سرگیری مجدد، لغو درخواست و سعی مجدد دریافت فایل‌های حجیم را با HttpClient، همانند برنامه‌های download manager نیز پیاده سازی کرد.


از سرگیری مجدد درخواست ارسالی توسط HttpClient

یک نمونه از سرگیری مجدد درخواست را در مطلب «اضافه کردن قابلیت از سرگیری مجدد (resume) به HttpWebRequest» پیشتر در این سایت مطالعه کرده‌اید. اصول کلی آن نیز در اینجا صادق است. HTTP 1.1 از مفهوم range headers‌، برای دریافت پاسخ‌های جزئی پشتیبانی می‌کند. به این ترتیب در صورت پیاده سازی چنین قابلیتی در برنامه‌ی سمت سرور، می‌توان دریافت بازه‌ای از بایت‌ها را بجای دریافت فایل از ابتدا، از سرور درخواست کرد. به یک چنین قابلیتی Resume و یا از سرگیری مجدد گرفته می‌شود و درحین دریافت فایل‌های حجیم بسیار حائز اهمیت است.
var fileInfo = new FileInfo(outputFilePath);
long resumeOffset = 0;
if (fileInfo.Exists)
{
    resumeOffset = fileInfo.Length;
}
if (resumeOffset > 0)
{
    _client.DefaultRequestHeaders.Range = new RangeHeaderValue(resumeOffset, null);
}
در اینجا نحوه‌ی تنظیم یک RangeHeader را مشاهده می‌کنید. ابتدا نیاز است بررسی کنیم آیا فایل دریافتی از پیش موجود است؟ آیا قسمتی از این درخواست پیشتر دریافت شده و محتوای آن هم اکنون به صورت ذخیره شده وجود دارد؟ اگر بله، درخواست دریافت این فایل را بر اساس اندازه‌ی دریافتی فعلی آن، به سرور ارائه می‌کنیم.

یک نکته: تمام وب سرورها و یا برنامه‌های وب از یک چنین قابلیتی پشتیبانی نمی‌کنند.
روش تشخیص آن نیز به صورت زیر است:
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (response.Headers.AcceptRanges == null && resumeOffset > 0)
{
    // resume not supported, starting over
}
پس از خواندن هدر درخواست، اگر خاصیت AcceptRanges آن نال بود، یعنی قابلیت از سرگیری مجدد را ندارد. در این حالت باید فایل موجود فعلی را حذف و یا از نو (FileMode.CreateNew) بازنویسی کرد (بجای حالت FileMode.Append).


لغو درخواست ارسالی توسط HttpClient

پس از شروع غیرهمزمان client.GetAsync می‌توان متد CancelPendingRequests آن‌را فراخوانی کرد تا کلیه درخواست‌های مرتبط با این client لغو شوند. اما این متد صرفا برای حالت پیش‌فرض client.GetAsync که دریافت هدر + محتوا است کار می‌کند (یعنی حالت HttpCompletionOption.ResponseContentRead). اگر همانند نکات بررسی شده‌ی در مطلب «دریافت فایل‌های حجیم توسط HttpClient» صرفا درخواست خواندن هدر را بدهیم (HttpCompletionOption.ResponseHeadersRead)، چون کنترل ادامه‌ی بحث را خودمان بر عهده گرفته‌ایم، لغو آن نیز به عهده‌ی خودمان است و متد CancelPendingRequests بر روی آن تاثیر نخواهد داشت.
این نکته در مورد تنظیم خاصیت TimeOut نیز صادق است. این خاصیت فقط زمانیکه دریافت کل هدر + محتوا توسط متد GetAsync مدیریت شوند، تاثیر گذار است.
بنابراین درحالتیکه نیاز به کنترل بیشتر است، هرچند فراخوانی متد CancelPendingRequests ضرری ندارد، اما الزاما سبب قطع کل درخواست نمی‌شود و باید این لغو را به صورت ذیل پیاده سازی کرد:
ابتدا یک منبع توکن لغو عملیات را به صورت ذیل ایجاد می‌کنیم:
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
سپس، متد لغو برنامه، تنها کافی است متد Cancel این cts را فراخوانی کند؛ تا عملیات دریافت فایل خاتمه یابد.
پس از این فراخوانی (()cts.Cancel)، نحوه‌ی واکنش به آن به صورت ذیل خواهد بود:
var result = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, _cts.Token);
using(var stream = await result.Content.ReadAsStreamAsync())
{
   byte[] buffer = new byte[80000];
   int bytesRead;
   while((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0 &&
         !_cts.IsCancellationRequested)
   {
      outputStream.Write(buffer, 0, bytesRead);
   }
}
در اینجا از cts.Token به عنوان پارامتر سوم متد GetAsync استفاده شده‌است. همچنین قسمت ثبت اطلاعات دریافتی، در استریم خروجی نیز به صورت یک حلقه درآمده‌است تا بتوان خاصیت IsCancellationRequested این توکن لغو را بررسی کرد و نسبت به آن واکنش نشان داد.


سعی مجدد درخواست ارسالی توسط HttpClient

یک روش پیاده سازی سعی مجدد درخواست شکست خورده، توسط کتابخانه‌ی Polly است. روش دیگر آن نیز به صورت ذیل است:
public async Task DownloadFileAsync(string url, string outputFilePath, int maxRequestAutoRetries)
{
            var exceptions = new List<Exception>();

            do
            {
                --maxRequestAutoRetries;
                try
                {
                    await doDownloadFileAsync(url, outputFilePath);
                }
                catch (TaskCanceledException ex)
                {
                    exceptions.Add(ex);
                }
                catch (HttpRequestException ex)
                {
                    exceptions.Add(ex);
                }
                catch (Exception ex) when (isNetworkError(ex))
                {
                    exceptions.Add(ex);
                }

                // Wait a bit and try again later
               if (exceptions.Any())  await Task.Delay(2000, _cts.Token);
            } while (maxRequestAutoRetries > 0 &&
                     !_cts.IsCancellationRequested);

            var uniqueExceptions = exceptions.Distinct().ToList();
            if (uniqueExceptions.Any())
            {
                if (uniqueExceptions.Count() == 1)
                    throw uniqueExceptions.First();
                throw new AggregateException("Could not process the request.", uniqueExceptions);
            }
}

private static bool isNetworkError(Exception ex)
{
    if (ex is SocketException || ex is WebException)
        return true;
    if (ex.InnerException != null)
        return isNetworkError(ex.InnerException);
    return false;
}
در اینجا متد doDownloadFileAsync، پیاده سازی همان متدی است که در قسمت «لغو درخواست ارسالی توسط HttpClient» در مورد آن بحث شد. این قسمت دریافت فایل را در یک حلقه که حداقل یکبار اجرا می‌شود، قرار می‌دهیم. متد GetAsync استثناءهایی مانند TaskCanceledException (در حین TimeOut و یا فراخوانی متد CancelPendingRequests که البته همانطور که توضیح داده شد، بر روی روش کنترل Response تاثیری ندارند)، HttpRequestException پس از فراخوانی متد response.EnsureSuccessStatusCode (جهت اطمینان حاصل کردن از دریافت پاسخی بدون مشکل از طرف سرور) و یا SocketException و WebException را درصورت بروز مشکلی در شبکه، صادر می‌کند. نیازی به بررسی سایر استثناءها در اینجا نیست.
اگر یکی از این استثناءهای یاد شده رخ‌دادند، اندکی صبر کرده و مجددا درخواست را از ابتدا صادر می‌کنیم.
در پایان این سعی‌های مجدد، اگر استثنایی ثبت شده بود و همچنین عملیات نیز با موفقیت به پایان نرسیده بود، آن‌را به فراخوان صادر می‌کنیم.
اشتراک‌ها
دوره 13 ساعته زبان Rust

Learn Rust Programming - Complete Course 🦀

In this comprehensive Rust course for beginners, you will learn about the core concepts of the language and underlying mechanisms in theory.

⭐️ Contents ⭐️
00:00:00 Introduction & Learning Resources
00:06:19 Variables
00:27:07 Numbers & Binary System
01:09:51 Chars, Bools & Unit Types
01:17:55 Statements & Expressions
01:24:50 Functions
01:32:53 Ownership
02:24:06 Borrowing
02:47:45 String vs. &str
03:17:59 Slices
03:31:35 Tuples
03:40:04 Structs
04:02:52 Enums
04:13:46 The "Option" Enum
04:21:32 Flow Control
04:44:43 Pattern Match
05:16:42 Methods & Associated Functions
05:31:50 Generics
06:06:32 Traits
06:47:15 Trait Objects
07:09:51 Associated Types
07:39:31 String
07:59:52 Vectors
08:29:00 HashMaps
08:52:45 Type Coercion
09:04:54 From & Into
09:36:03 panic!
09:44:56 Result
10:28:23 Cargo, Crates & Modules
11:08:28 Debug & Display
11:30:13 Lifetimes
12:14:46 Lifetime Elision
12:38:53 Closures
13:30:08 Iterators 

دوره 13 ساعته زبان Rust
نظرات مطالب
کار با اسناد در RavenDb 4، ثبت و ویرایش
یک نکته‌ی تکمیلی
تعداد درخواست هایی که می‌توانیم توسط یک Session به سمت سرور ارسال کنیم محدود است، دلیل آن هم برای طراحی بهتر ذکر شده. البته این مقدار را میتوانیم بصورت دستی تغییر دهیم:
_documentSession.Advanced.MaxNumberOfRequestsPerSession = 110;
برای ثبت تعداد زیادی سند با سرعت بسیار بالا میتوان از متد BulkInsert تعبیه شده در RavenDb استفاده کرد:
using (BulkInsertOperation bulkInsert = store.BulkInsert())
{
    for (int i = 0; i < 1000 * 1000; i++)
    {
        bulkInsert.Store(new User
        {
            PhoneNumber = randomPhone(),
        });
    }
}
بعد از Dispose شدن اتوماتیک DocumentStore(به دلیل استفاده از using) عملیات BulkInsert به بصورت چندین دسته(Batches) به سمت سرور ارسال می‌شوند و هر دسته در یک Transaction قرار میگیرد. بنابراین تمام عملیات ما طی یک Transaction انجام نخواهد شد! نکتع ی دیگر اینکه BulkInsert اطلاحا thread safe نیست و باید بعد از پایان یک BulkInsert برای اعمال BulkInsert بعدی اقدام کنیم.
مطالب
PowerShell 7.x - قسمت سوم - آشنایی با Redirection
در PowerShell به صورت پیش‌فرض، خروجی، PowerShell Host یا همان کنسول است. PowerShell از چندین استریم پشتیبانی میکند:
  • Success
  • Error
  • Warning
  • Verbose
  • Debug
  • Information 
برای هر کدام از استریم‌های فوق یک آی‌دی اختصاص داده شده‌است که به ترتیب از 1 تا ۶ میباشد. همچنین برای هرکدام یک cmdlet مجزا وجود دارد:
cmdlet  Name   Id
 Write-Output  Success  1
 Write-Error  Error  2
 Write-Warning  Warning  3
 Write-Verbose  Verbose  4
 Write-Debug  Debug  5
 Write-Information  Information  6

به جز دو مورد اول، بقیه cmdletها خروجی را به صورت پیش‌فرض درون کنسول نمایش نمیدهند. به عنوان مثال اسکریپت زیر را در نظر بگیرید:

Write-Output 'Output'                          
Write-Error 'This is an error'                 
Write-Warning 'This is a warning'              

Write-Verbose 'This is verbose'                
Write-Debug 'This is Debug'                    
Write-Information 'This is information'

با اجرای اسکریپت فوق خروجی زیر را خواهیم داشت:

Output
Write-Error: This is an error
WARNING: This is a warning

همانطور که مشاهده میکنید سه cmdlet فوق، خروجی را درون کنسول نمایش نداده‌اند. این رفتار توسط مفهومی تحت عنوان Action Preference قابل تنظیم است که در واقع یک Enum است با مقدار زیر:

  6  Break
رخداد به صورت عادی مدیریت شده و برنامه ادامه پیدا میکند  2  Continue
به طور کلی از رخداد صرفنظر خواهد شد؛ بدون اینکه چیزی در استریم نمایش داده شود 4  Ignore
سوال پرسیده خواهد شد که برنامه را ادامه دهد یا متوقف کند  3  Inquire
به طور کلی از رخداد صرفنظر خواهد شد    
0  SilentlyContinue
 دستور را متوقف خواهد کرد  Stop
 دستور به نوعی معلق خواهد شد 5  Suspend

بنابراین با تغییر Action Preference برای هر کدام از cmdletها میتوانیم رفتار اسکریپت قبلی را تغییر دهیم:

Write-Output 'Output'                          
Write-Error 'This is an error'                 
Write-Warning 'This is a warning'              

$VerbosePreference = 'Continue'
Write-Verbose 'This is verbose'

$DebugPreference = 'Continue'
Write-Debug 'This is Debug'  

$InformationPreference = 'Continue'
Write-Information 'This is information'

اکنون اگر اسکریپت فوق را اجرا کنید، سه خروجی آخر را نیز مشاهده خواهید کرد:

Output
Write-Error: This is an error
WARNING: This is a warning
VERBOSE: This is verbose
DEBUG: This is Debug
This is information

هر کدام از استریم‌های فوق قابل redirect شدن نیز هستند؛ برای اینکار میتوانیم از redirect operatorهایی که در PowerShell پشتیبانی میشود استفاده کنیم:

>
>>
>&1

به عنوان مثال میتوانیم تمام خطاها یا هشدارهای درون یک اسکریپت را به یک فایل منتقل کنیم:

./script.ps1 2>&1 > .\logs.txt

یا میتوانیم تمام Success streamها را به یک فایل هدایت کنیم:

.\script.ps1 > script.log

ارسال تمام Success, Warning, Errorها به یک فایل:

&{
   Write-Warning "hello"
   Write-Error "hello"
   Write-Output "hi"
} 3>&1 2>&1 > C:\Temp\redirection.log

ارسال تمام استریم‌ها به یک فایل:

.\script.ps1 *> script.log

همچنین میتوانیم استریمی را به اصطلاح suppress کنیم که در خروجی نمایش داده نشود:

./script.ps1 1> $null 2> $null

./script.ps1 *> $null

از تکنیک فوق برای drop کردن خروجی‌هایی که نمیخواهیم نمایش داده شوند، استفاده میشود. در کد فوق دو Idهای ۱ و ۲ را به متغیر ویژه‌ی null هدایت کرده‌ایم؛ همچنین میتوانستیم از یک رشته‌ی خالی نیز بجای null استفاده کنیم. در خط بعدی از * استفاده کرده‌ایم که به معنای تمامی استریم‌های موجود است؛ با اینکار چیزی در خروجی نمایش داده نخواهد شد. یک روش دیگر برای drop کردن، استفاده از دستور Out-Null است:

Get-ChildItem | Out-Null

لازم به ذکر است که این cmdlet تا قبل از نسخه ۶ خیلی کند بود؛ زیرا همانند دیگر cmdletهای درون pipeline میبایست یک ورودی (InputObject) را دریافت کند که باعث میشد هزینه‌ی پردازشی بالایی داشته باشد. اما در نسخه ۶ به بعد این مشکل رفع شده‌است و پارزر به محض رسیدن به این keyword به صورت کلی خروجی را discard میکند بدون اینکه Out-Null را فراخوانی کند؛ در واقع این cmdlet یک hint برای پارزر است. روش دیگر برای drop کردن خروجی، انتساب نتیجه یک دستور به متغییر null است:

New-Item -Type Directory -Path $path | Out-Null

$null = New-Item -Type Directory -Path $path

همچنین میتوانیم خروجی یک دستور را به void تبدیل کنیم؛ که نتیجه مشابه با تکنیک‌های فوق دارد:

[void](New-Item -Name test -ItemType Directory)

یک نکته در مورد Out-Null

در loopهای بزرگ ممکن است Out-Null حتی در PowerShell 7.x هم کند عمل کند:

PS > Measure-Command { for($i=0; $i -lt 1mb; $i++) { $i | Out-Null } } | Select-Object TotalSeconds

TotalSeconds
------------
   4.3056315


PS > Measure-Command { for($i=0; $i -lt 1mb; $i++) { $null = $i } } | Select-Object TotalSeconds

TotalSeconds
------------
   1.1210884

PS > Measure-Command { for($i=0; $i -lt 1mb; $i++) { [void]$i } } | Select-Object TotalSeconds

TotalSeconds
------------
    1.130507

PS > Measure-Command { for($i=0; $i -lt 1mb; $i++) { $i > $null } } | Select-Object TotalSeconds

TotalSeconds
------------
   1.3832427
مطالب
بومی سازی تاریخ و اعداد در جاوا اسکریپت در سال 2020
احتمالا تا به امروز در برنامه‌های جاوا اسکریپتی خود از کتابخانه‌های ثالث و یا توابع ویژه‌ای برای نمایش شمسی تاریخ، نمایش فارسی اعداد، افزودن جدا کننده‌ی سه رقمی اعداد (جداکننده‌ی هزارگان)، نمایش تاریخ نسبی مانند 1 روز قبل و ... استفاده کرده‌اید. خبر خوب اینکه موتور جاوا اسکریپتی تمام مرورگرهای جدید (از فایرفاکس 29 و کروم 24 به بعد) به صورت توکار یک چنین تبدیل‌هایی را پشتیبانی می‌کنند و برای مثال برای تبدیل تاریخ میلادی به شمسی و نمایش آن، در بسیاری از موارد نیازی به کتابخانه‌ی حجیم moment.js (و یا سایر روش‌های مرسوم خانگی) نیست.


معرفی API استاندارد بومی سازی JavaScript

Internationalization یا به صورت خلاصه i18n (یعنی یک i به همراه 18 حرف و یک n)، پروسه‌ای که در آن برنامه به نحوی طراحی می‌شود تا خروجی آن قابلیت استفاده‌ی برای انواع و اقسام فرهنگ‌ها را داشته باشد. برای مثال دو متد زیر را در نظر بگیرید:
function formatDate(d)
{
  var month = d.getMonth() + 1;
  var date = d.getDate();
  var year = d.getFullYear();
  return month + "/" + date + "/" + year;
}

function formatMoney(amount)
{
  return "$" + amount.toFixed(2);
}
آیا در همه جای دنیا، تاریخ به صورت ماه، روز و سال نمایش داده می‌شود؛ آن هم به صورت میلادی؟ و یا آیا خروجی فرمت شده‌ی یک مقدار پولی، همیشه با دلار شروع می‌شود و نمایش آن نیز با اعداد انگلیسی است؟
پیشتر جاوا اسکریپت برای مدیریت یک چنین مواردی (i18n-aware formatting) از متد toLocaleString استفاده می‌کرد (و هنوز هم برای پشتیبانی از برنامه‌های قدیمی، از API عمومی آن حذف نشده‌است) و خروجی آن از هر مرورگر و پیاده سازی خاصی، به مرورگر دیگری می‌تواند متفاوت باشد؛ حتی اگر جزئیات دقیقی هم درخواست شود. برای رفع این مشکل، استاندارد ECMAScript Internationalization API ارائه شد تا قابلیت‌های توکار i18n جاوا اسکریپت را بهبود بخشیده و همچنین یک‌دست کند. توسط آن امکان انتخاب یک یا چند منطقه‌ی خاص و سپس فرمت کردن تاریخ، اعداد و یا حتی مرتب سازی واژه‌ها و عبارات با معرفی collations، میسر می‌باشد. در اینجا حتی امکان سفارشی سازی این فرمت‌ها نیز پیش‌بینی شده‌است.


معرفی اینترفیس Intl

i18n API در یک شیء سراسری به نام Intl قابل دسترسی است و تعدادی از سازنده‌های آن Intl.Collator ،Intl.DateTimeFormat و Intl.NumberFormat نام دارند؛ مانند:
const result =  new Intl.NumberFormat("fa").format(123456)
برای کار با این شیء، نیازی به import هیچ ماژول و یا کتابخانه‌ای نیست و جزئی از جاوا اسکریپت استاندارد می‌باشد. به همین جهت کار با آن حجمی را به برنامه‌ی شما اضافه نخواهد کرد.

تمام این سازنده‌ها می‌توانند با یک فرهنگ و یا آرایه‌ا‌‌ی از فرهنگ‌های مدنظر شروع شوند:
const portugueseTime =  new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options);
در مثال اول فرهنگ فارسی و در مثال دوم فرهنگ پرتغالی که در برزیل و پرتغال مورد استفاده‌است، ذکر شده‌اند.
پارامتر اختیاری دوم آن‌ها نیز تنظیماتی است که جهت سفارشی سازی این بومی سازی می‌توان تعریف کرد.


نمایش شمسی تاریخ میلادی توسط i18n API

پس از معرفی i18n API، اکنون می‌خواهیم در طی مثال‌هایی، تمام کتابخانه‌های ثالث تبدیل تاریخ میلادی به شمسی را کنار گذاشته و با استفاده از جاوا اسکریپت استاندارد، این تبدیل را انجام دهیم. پارامتر دوم سازنده‌ی new Intl.DateTimeFormat که تنظیمات آن‌را مشخص می‌کند، می‌تواند به همراه ترکیبی از موارد زیر باشد که مقادیر مجاز برای آن‌ها را نیز مشاهده می‌کنید:
{
  weekday: 'narrow' | 'short' | 'long',
  era: 'narrow' | 'short' | 'long',
  year: 'numeric' | '2-digit',
  month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long',
  day: 'numeric' | '2-digit',
  hour: 'numeric' | '2-digit',
  minute: 'numeric' | '2-digit',
  second: 'numeric' | '2-digit',
  timeZoneName: 'short' | 'long',
// Time zone to express it in
  timeZone: 'Asia/Shanghai',
  // Force 12-hour or 24-hour
  hour12: true | false,
// Rarely-used options
  hourCycle: 'h11' | 'h12' | 'h23' | 'h24',
  formatMatcher: 'basic' | 'best fit'
}
برای نمونه، ذکر Intl.DateTimeFormat بدون هیچ تنظیمی و فقط با تعیین فرهنگ فارسی:
var dateFormat = new Intl.DateTimeFormat("fa");
console.log(dateFormat.format(Date.now()));
خروجی «۱۳۹۸/۱۲/۱» را نمایش می‌دهد.


نمایش تاریخ شمسی با فرمت «۱۳۹۸ اسفند ۱, پنجشنبه»

برای تبدیل تاریخ میلادی به شمسی می‌توان از سازنده‌ی new Intl.DateTimeFormat با فرهنگ fa استفاده کرد. در اینجا ذکر مقدار long برای نام روز هفته، سبب درج نام روز می‌شود. نمایش سال به صورت عددی تنظیم شده‌است، ماه را به صورت بلند و نام کامل نمایش می‌دهد و مقدار روز را به صورت عددی درج می‌کند. این اعداد نیز فارسی هستند:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const faDate = new Intl.DateTimeFormat("fa", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
}).format(date);
console.log(faDate);
که برای نمونه سبب درج خروجی «۱۳۹۸ اسفند ۱, پنجشنبه» در کنسول توسعه دهندگان مرورگر خواهد شد.

اگر فقط نیاز به نمایش «۱ اسفند ۱۳۹۸» بود، می‌توان از تنظیمات زیر که در آن ماه، روز و سال ذکر شده‌اند و در آن، ماه به صورت کامل و بلند نمایش داده می‌شود، استفاده کرد:
const isoString = new Date().toISOString();
const date = new Date(isoString);
console.log(
  new Intl.DateTimeFormat("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  }).format(date)
);

یک نکته: همین خروجی را با متد قدیمی toLocaleDateString نیز می‌توان به دست آورد؛ اما روش توصیه شده برای برنامه‌های جدید، همان استفاده از new Intl است.
console.log(
  new Date().toLocaleDateString("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  })
);

نمایش تاریخ شمسی با فرمت «۹۸/۱۲/۱،‏ ۶:۳۰»

برای اینکار پس از ذکر فرهنگ fa، تمام اجزای تاریخ را به صورت عددی مشخص می‌کنیم و سال را نیز دو رقمی نمایش خواهیم داد:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const fmt = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});
console.log(fmt.format(date));
در این حالت اگر نیاز بود حتما اعداد ماه و روز، دو رقمی باشند، می‌توان تنظیم 2-digit را صریحا ذکر کرد:
const faDateTime = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short"
}).format;
const now = Date.now();
console.log(faDateTime(now));
با خروجی «۹۸/۱۲/۰۱،‏ ۱۲:۵۹ (‎+۳:۳۰ گرینویچ)»

و یا اگر «۱ اسفند ۱۳۹۸،‏ ۰۹:۲۹ (UTC)» مدنظر بود، می‌توان ماه را به long تنظیم کرد و مقدار timeZone را صریحا ذکر نمود (که البته ذکر تنظیمات timeZone اختیاری است):
const faTime = new Intl.DateTimeFormat("fa", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short",
  timeZone: "UTC"
});
console.log(faTime.format(now));

نمایش تاریخ‌های نسبی مانند «1 روز بعد»
برای نمایش تاریخ‌های نسبی، می‌توان از شیء new Intl.RelativeTimeFormat استفاده کرد:
const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit", // other values: "lookup"
  numeric: "always", // other values: "auto"
  style: "long", // other values: "short" or "narrow"
});
console.log(rtf.format(-1, "day"));
console.log(rtf.format(1, "day"));
با خروجی‌های «۱ روز پیش» و «۱ روز بعد»


نمایش اعداد فارسی توسط i18n API

احتمالا برای تبدیل اعداد انگلیسی به فارسی و نمایش آن‌ها، متدهایی را برای replace حروف و اعداد طراحی کرده‌اید. به کمک شیء استاندارد Intl.NumberFormat دیگر نیازی به آن‌ها نخواهید داشت!
خروجی شیء Intl.NumberFormat به همراه ذکر فرهنگ فارسی و هیچ تنظیم اضافه‌تری
console.log(new Intl.NumberFormat("fa").format(123456));
به صورت «۱۲۳٬۴۵۶» است که هم اعداد آن فارسی شده‌اند و هم به همراه جداکننده‌ی هزارگان خودکار است.

اگر می‌خواهید این جداکننده‌ی هزارگان نمایش داده نشود، نیاز است تنظیمات آن‌را به همراه useGrouping: false، به صورت زیر ذکر کرد:
console.log(
   new Intl.NumberFormat("fa", { useGrouping: false }).format(123456)
);

این شیء یک مقدار غیرعددی را
console.log(new Intl.NumberFormat("fa").format("تست"));
به صورت «ناعدد» نمایش می‌دهد.

و یا برای نمایش واحد پولی، می‌توان حالت نمایش را به currency و نوع currency را به IRR که ریال است، تنظیم کرد:
const gasPrice = new Intl.NumberFormat("fa", {
  style: "currency",
  currency: "IRR",
  minimumFractionDigits: 3
});
console.log(gasPrice.format(5.2597));
با این خروجی: «‎ریال ۵٫۲۶۰» که در اینجا امکان تنظیم نمایش تعداد اعشار آن نیز میسر است.

برای نمایش درصد پس از اعداد می‌توان از تنظیم زیر استفاده کرد:
const faPercent = new Intl.NumberFormat("fa", {
  style: "percent",
  minimumFractionDigits: 2
}).format;
console.log(faPercent(0.438));
که خروجی «۴۳٫۸۰٪» را نمایش می‌دهد.

و یا برای نمایش ممیز به همراه تنظیم دقت آن داریم:
const persianDecimal = new Intl.NumberFormat("fa", {
  minimumIntegerDigits: 2,
  maximumFractionDigits: 2
});
console.log(persianDecimal.format(3.1416));
با این خروجی: «۰۳٫۱۴»
نظرات مطالب
EF Code First #7
سلام، من اگه بخوام بین دوتا فیلد از یک جدول به یک جدول دیگه رابطه برقرار کنم به چه صورت است :
pulbic class User
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public ICollection<Comment> Comments { get; set; }
}
public class Comment
{
    public int Id { get; set; }
    public string Text { get; set; }
    public int UserId { get; set; }
    public int? UserId2 { get; set; }
    
    [ForeignKey(nameof(UserId))
    public virtual User User { get; set; }
    [ForeignKey(nameof(UserId2))
    public virtual User User2 { get; set; }
}
در این حالت اولین رابطه که UserId هست ازحذف میشه و UserId2 با جدول User رابطه ش برقرار میشه چطور میشه دو فیلد از یک جدول رو با یک جدول دیگه رابطه زد ؟
نظرات مطالب
آشنایی با مفاهیم نوع داده Enum و توسعه آن - قسمت دوم
وقتی کلاس Description در   فضای نام  System.ComponentModel وجود داره دلیلی نداره  کلاس مشابه ای تعریف کنیم.
بخاطر اینکه مصرف کننده محض نباشیم یک متد الحاقی به نام ()GetEnumList اضافه کردم که لیست اعضای یک Enum  رو برای استفاده در کمبو باکس و ... بر می‌گرودنه :
ابتدا کلاس زیر به کلاس ExtensionMethodCls اضافه می‌کنیم :
 public class EnumObject
        {
            public Enum ValueMember { get; set; }
            public int intValueMember
            {
                get { return int.Parse(ValueMember.ToString("D")); }
            }
            public string stringValueMember
            {
                get { return ValueMember.ToString(""); }
            }
            public string DisplayMember
            {
                get { return ValueMember.GetDescription(); }
            }
        }
و متد الحاقی زیر رو برای گرفتن لیست تعریف می‌کنیم:
      public static List<EnumObject> GetEnumList(this Enum enu)
        {
            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enu.GetType().GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }
نحوه استفاده  :
          comboBox1.DataSource = Grade.VeryGood.GetEnumList();
          comboBox1.DisplayMember = "DisplayMember";
          comboBox1.ValueMember = "ValueMember";
همون طوری که در کد بالا می‌بینید برای گرفتن لیست مجبور شدیم یکی از اعضای enum  رو انتخاب کنیم (Grade.VeryGood.GetEnumList()) شاید انتخاب یکی از اعضا و بعد درخواست لیست اعضا رو کردن کار قشنگی نباشه به همین دلیل متد زیر رو تعریف کردیم :
        public static List<EnumObject> EnumToList<T>()
        {
            Type enumType = typeof(T);
            if (enumType.BaseType != typeof(Enum))
                throw new ArgumentException("T must be of type System.Enum");

            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enumType.GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }
نحوه استفاده :
    comboBox1.DataSource =ExtensionMethodCls.EnumToList<Grade>();
          comboBox1.DisplayMember = "DisplayMember";
          comboBox1.ValueMember = "ValueMember";
کد کامل :
 public static class ExtensionMethodCls
    {
        public class EnumObject
        {
            public Enum ValueMember { get; set; }
            public int intValueMember
            {
                get { return int.Parse(ValueMember.ToString("D")); }
            }
            public string stringValueMember
            {
                get { return ValueMember.ToString(""); }
            }
            public string DisplayMember
            {
                get { return ValueMember.GetDescription(); }
            }
        }
 
    

        public static List<EnumObject> EnumToList<T>()
        {
            Type enumType = typeof(T);
            if (enumType.BaseType != typeof(Enum))
                throw new ArgumentException("T must be of type System.Enum");

            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enumType.GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }

        public static List<EnumObject> GetEnumList(this Enum enu)
        {
            List<EnumObject> li = new List<EnumObject>();
            foreach (var item in enu.GetType().GetEnumValues())
            {
                li.Add(new EnumObject { ValueMember = (Enum)item });
            }
            return li;
        }

        public static string GetDescription(this Enum enu)
        {

            Type type = enu.GetType();

            MemberInfo[] memInfo = type.GetMember(enu.ToString());

            if (memInfo != null && memInfo.Length > 0)
            {

                object[] attrs = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);

                if (attrs != null && attrs.Length > 0)
                    return ((DescriptionAttribute)attrs[0]).Description;
            }

            return enu.ToString();

        }
    }
مطالب
امکان تعریف ساده‌تر خواص Immutable در C# 9.0 با معرفی ویژگی خواص Init-Only
نگاهی به روند تکاملی نحوه‌ی تعریف خواص از C# 1.0 تا C# 9.0

در C# 1.0 برای تعریف خواص، نیاز به نوشتن مقدار زیادی کد بود:
public class Person 
{ 
    public string _firstName; 
 
    public string FirstName 
    { 
        get 
        { 
            return _firstName; 
        } 
        set 
        { 
            _firstName = value; 
        } 
    }  
}
در اینجا تعریف backing field‌ها (مانند public string _firstName) و استفاده‌ی دستی از آن‌ها الزامی بود.

در C# 2.0 از لحاظ ساده سازی این تعاریف، اتفاق خاصی رخ‌نداد. فقط امکان تعریف سطوح دسترسی مانند private بر روی getter‌ها و setter‌ها میسر شد:
public string _firstName; 
public string FirstName 
{ 
    get 
    { 
        return _firstName; 
    } 
    private set 
    { 
        _firstName = value; 
    } 
}

در C# 3.0 بود که با ارائه‌ی auto-implemented properties، نحوه‌ی تعریف خواص، بسیار ساده شد و دیگر نیازی به تعریف backing field‌ها نبود؛ چون کامپایلر به صورت خودکار آن‌ها را در پشت صحنه ایجاد می‌کرد/می‌کند:
public class Person
{
   public string FirstName { get; set; }
}

در C# 6.0، امکان حذف private setter‌ها از تعریف یک خاصیت میسر شد. یعنی مثال زیر را
public class User
{
   public string Name { get; private set; }
}
به این نحو ساده‌تر و واضح‌تر نیز می‌توان نوشت:
public class User
{
   public string Name { get; }
}
به‌علاوه در همین زمان بود که امکان مقدار دهی اولیه‌ی خواص نیز در همان سطر تعریف آن‌ها ممکن شد:
public class Foo
{
   public string FirstName { get; set; } = "Initial Value";
}
پیش از این برای مقدار دهی اولیه‌ی خواص در همان کلاسی که آن‌ها را تعریف می‌کند، می‌بایستی از طریق مقدار دهی آن‌ها در سازنده‌ی کلاس اقدام می‌شد.

همچنین در C# 6.0 با معرفی expression bodied members که بر روی خواص نیز قابل اعمال است، امکان تعریف خواص readonly محاسبه شده‌ی بر اساس مقدار سایر خواص نیز میسر شد:
public class Foo
{  
   public DateTime DateOfBirth { get; set; }
   public int Age => DateTime.Now.Year - DateOfBirth.Year;  
}

و در C# 9.0، با معرفی واژه‌ی کلیدی init، امکان تعریف ساده‌تر خواص immutable ممکن شد‌ه‌است که در مطلب جاری به آن خواهیم پرداختیم.


روش غیرقابل مقدار دهی کردن خواص، در نگارش‌ها پیش از C# 9.0

در بسیاری از موارد می‌خواهیم که خاصیتی از یک کلاس مدل، در خارج از آن قابل تغییر نباشد (مانند خواص شیء‌ای که به محتوای فایل config ثابت برنامه اشاره می‌کند). راه حل فعلی آن تا پیش از C# 9.0 به صورت زیر است:
public class User
{
   public string Name { get; private set; }
}
که در این حالت دیگر نمی‌توان مقدار خاصیت Name را در خارج از کلاس User مقدار دهی کرد:
var user = new User
{
   Name = "User 1" // Compile Error
};
وبا اینکار خطای کامپایلر زیر را دریافت می‌کنیم:
The property or indexer 'User.Name' cannot be used in this context
because the set accessor is inaccessible [CS9Features]csharp(CS0272)
در این تعریف باتوجه به وجود private set، برای مقداردهی خاصیت Name می‌توان از یکی از دو روش زیر در داخل کلاس User استفاده کرد:
- تنظیم مقدار خاصیت Name در سازنده‌ی کلاس
- و یا تنظیم این مقدار در یک متد ثالث دیگر مانند SetName
public class User
{
  public User(string name)
  {
    this.Name = name;
  }

  public void SetName(string name)
  {
    this.Name = name;
  }

  public string Name { get; private set; }
}
در هر دو حالت، از مقدار دهی مستقیم خاصیت Name توسط Object Initializer (یا همان روش متداول new User { Name = "some name"}) محروم می‌شویم. همچنین در ادامه شاید نیاز باشد که این خاصیت پس از مقدار دهی اولیه، دیگر قابل تغییر نباشد؛ یا به عبارتی immutable شود. در مثال فوق هنوز هم امکان تغییر مقدار خاصیت Name درون کلاس User، با فراخوانی‌های بعدی متد SetName، وجود دارد.


معرفی خواص Init-Only در C# 9.0

برای رفع دو مشکل یاد شده (امکان تنظیم مقدار خاصیت‌ها با همان روش متداول object initializer و همچنین غیرقابل تغییر شدن آن‌ها)، اکنون در C# 9.0 می‌توان بجای private set از واژه‌ی کلیدی init استفاده کرد:
public class User
{
   public string Name { get; init; }
}
در اینجا تنها تغییر صورت گرفته، استفاده از واژه‌ی کلیدی init، در حین تعریف خاصیت Name است. به این ترتیب به دو مزیت زیر دسترسی پیدا می‌کنیم:
الف) امکان مقدار دهی خاصیت Name، در خارج بدنه‌ی کلاس User و توسط روش متداول کار با object initializer‌ها هنوز هم وجود دارد و در این حالت الزامی به تعریف یک سازنده و یا متد خاصی درون کلاس User برای مقدار دهی آن نیست:
var user = new User
{
   Name = "User 1"
};
ب) پس از اولین بار مقدار دهی این خاصیت init-only، دیگر نمی‌توان مقدار آن‌را تغییر داد:
// Compile Time 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 = "Test";
این نکته در مورد متدهای داخل کلاس User هم صدق می‌کند:
public class User
{
   public string Name { get; init; }

   public User(string name)
   {
     this.Name = name; // Works fine
   }

   public void SetName(string name)
   {
     this.Name = name; // Compile Time Error
   }
}
می‌توان یک خاصیت init-only را برای بار اول، در سازنده‌ی همان کلاس نیز مقدار دهی کرد؛ اما مقدار دهی ثانویه‌ی آن در سایر متدهای داخل کلاس User نیز به خطای زمان کامپایل یاد شده، ختم می‌شود و مجاز نیست.


روش تعریف immutable properties در نگارش‌های پیشین #C

با استفاده از واژه‌ی readonly در نگارش‌های قبلی #C نیز می‌توان به صورت زیر، یک خاصیت را به صورت غیرقابل تغییر یا immutable در آورد:
    public class Product
    {
        public Product(string name)
        {
            _name = name;
        }

        private readonly string _name;

        public string Name => _name;
    }
هرچند این روش کار می‌کند اما دیگر همانند init-only properties نمی‌توان از طریق object initializers خاصیت Name را مقدار دهی کرد و این مقدار دهی حتما باید از طریق سازنده‌ی کلاس باشد. همچنین ایجاد یک اصطلاحا backing filed هم برای آن، کدها را طولانی‌تر می‌کند.

یک نکته: امکان استفاده‌ی از فیلدهای readonly با خواص init-only هم وجود دارد؛ از این جهت که این نوع خواص تنها در زمان نمونه سازی اولیه‌ی شیء، اجرا و مقدار دهی می‌شوند، با مفهوم readonly، سازگاری دارند:
    public class Person
    {
        private readonly string _name;

        public string Name
        {
            get => _name;
            init => _name = value;
        }
    }