مطالب
نکات کار با استثناءها در دات نت
استثناء چیست؟
واژه‌ی استثناء یا exception کوتاه شده‌ی عبارت exceptional event است. در واقع exception یک نوع رویداد است که در طول اجرای برنامه رخ می‌دهد و در نتیجه، جریان عادی برنامه را مختل می‌کند. زمانیکه خطایی درون یک متد رخ دهد، یک شیء (exception object) حاوی اطلاعاتی درباره‌ی خطا ایجاد خواهد شد. به فرآیند ایجاد یک exception object و تحویل دادن آن به سیستم runtime، اصطلاحاً throwing an exception یا صدور استثناء گفته می‌شود که در ادامه به آن خواهیم پرداخت.
بعد از اینکه یک متد استثناءایی را صادر می‌کند، سیستم runtime سعی در یافتن روشی برای مدیریت آن خواهد کرد.
خوب اکنون که با مفهوم استثناء آشنا شدید اجازه دهید دو سناریو را با هم بررسی کنیم.
- سناریوی اول:
فرض کنید یک فایل XML از پیش تعریف شده (برای مثال یک لیست از محصولات) قرار است در کنار برنامه‌ی شما باشد و باید این لیست را درون برنامه‌ی خود نمایش دهید. در این حالت برای خواندن این فایل انتظار دارید که فایل وجود داشته باشد. اگر این فایل وجود نداشته باشد برنامه‌ی شما با اشکال روبرو خواهد شد.
- سناریوی دوم:
فرض کنید یک فایل XML از آخرین محصولات مشاهده شده توسط کاربران را به صورت cache در برنامه‌تان دارید. در این حالت در اولین بار اجرای برنامه توسط کاربر انتظار داریم که این فایل موجود نباشد و اگر فایل وجود نداشته باشد به سادگی می‌توانیم فایل مربوط را ایجاده کرده و محصولاتی را که توسط کاربر مشاهده شده، درون این فایل اضافه کنیم.
در واقع استثناء‌ها بستگی به حالت‌های مختلفی دارد. در مثال اول وجود فایل حیاتی است ولی در حالت دوم بدون وجود فایل نیز برنامه می‌تواند به کار خود ادامه داده و فایل مورد نظر را از نو ایجاد کند.
 استثناها مربوط به زمانی هستند که این احتمال وجود داشته باشد که برنامه طبق انتظار پیش نرود.
برای حالت اول کد زیر را داریم:
public IEnumerable<Product> GetProducts()
{
    using (var stream = File.Read(Path.Combine(Environment.CurrentDirectory, "products.xml")))
    {
        var serializer = new XmlSerializer();
        return (IEnumerable<Product>)serializer.Deserialize(stream);
    }
}
همانطور که عنوان شد در حالت اول انتظار داریم که فایلی بر روی دیسک موجود باشد. در نتیجه نیازی نیست هیچ استثناءایی را مدیریت کنیم (زیرا در واقع اگر فایل موجود نباشد هیچ روشی برای ایجاد آن نداریم).
در مثال دوم می‌دانیم که ممکن است فایل از قبل موجود نباشد. بنابراین می‌توانیم موجود بودن فایل را با یک شرط بررسی کنیم:
public IEnumerable<Product> GetCachedProducts()
{
    var fullPath = Path.Combine(Environment.CurrentDirectory, "ProductCache.xml");
    if (!File.Exists(fullPath))
        return new Product[0];
         
    using (var stream = File.Read(fullPath))
    {
        var serializer = new XmlSerializer();
        return (IEnumerable<Product>)serializer.Deserialize(stream);
    }
}

چه زمانی باید استثناءها را مدیریت کنیم؟
زمانیکه بتوان متدهایی که خروجی مورد انتظار را بر می‌گردانند ایجاد کرد.
اجازه دهید دوباره از مثال‌های فوق استفاده کنیم:
IEnumerable<Product> GetProducts()
همانطور که از نام آن پیداست این متد باید همیشه لیستی از محصولات را برگرداند. اگر می‌توانید اینکار را با استفاده از catch کردن یک استثنا انجام دهید در غیر اینصورت نباید درون متد اینکار را انجام داد.
IEnumerable<Product> GetCachedProducts()
در متد فوق می‌توانستیم از FileNotFoundException برای فایل موردنظر استفاده کنیم؛ اما مطمئن بودیم که فایل در ابتدا وجود ندارد.
در واقع استثنا‌ها حالت‌هایی هستند که غیرقابل پیش‌بینی هستند. این حالت‌ها می‌توانند یک خطای منطقی از طرف برنامه‌نویس و یا چیزی خارج کنترل برنامه‌نویس باشند (مانند خطاهای سیستم‌عامل، شبکه، دیسک). یعنی در بیشتر مواقع این نوع خطاها را نمی‌توان مدیریت کرد.

اگر می‌خواهید استثناء‌ها را catch کرده و آنها را لاگ کنید در بالاترین لایه اینکار را انجام دهید.


چه استثناءهایی باید مدیریت شوند و کدام‌ها خیر؟ 
مدیریت صحیح استثناء‌ها می‌تواند خیلی مفید باشد. همانطور که عنوان شد یک استثناء زمانی رخ می‌دهد که یک حالت استثناء در برنامه اتفاق بیفتد. این مورد را بخاطر داشته باشید، زیرا به شما یادآوری می‌کند که در همه جا نیازی به استفاده از try/catch نیست. در اینجا ذکر این نکته خیلی مهم است:
تنها استثناء‌هایی را catch کنید که بتوانید برای آن راه‌حلی ارائه دهید.
به عنوان مثال اگر در لایه‌ی دسترسی به داده، خطایی رخ دهد و استثناءی SqlException صادر شود، می‌توانیم آن را catch کرده و درون یک استثناء عمومی‌تر قرار دهیم:
public class UserRepository : IUserRepository
{
    public IList<User> Search(string value)
    {
        try
        {
              return CreateConnectionAndACommandAndReturnAList("WHERE value=@value", Parameter.New("value", value));
        }
        catch (SqlException err)
        {
             var msg = String.Format("Ohh no!  Failed to search after users with '{0}' as search string", value);
             throw new DataSourceException(msg, err);
        }
    }
}
همانطور که در کد فوق مشاهده می‌کنید به محض صدور استثنای SqlException آن را درون قسمت catch به صورت یک استثنای عمومی‌تر همراه با افزودن یک سری اطلاعات جدید صادر می‌کنیم. اما همانطور که عنوان شد کار لاگ کردن استثناءها را بهتر است در لایه‌های بالاتر انجام دهیم.
اگر مطمئن نیستید که تمام استثناء‌ها توسط شما مدیریت شده‌اند، می‌توانید در حالت‌های زیر، دیگر استثناءها را مدیریت کنید:
ASP.NET: می‌توانید Aplication_Error را پیاده‌سازی کنید. در اینجا فرصت خواهید داشت تا تمامی خطاهای مدیریت نشده را هندل کنید.
WinForms: استفاده از رویدادهای Application.ThreadException و AppDomain.CurrentDomain.UnhandledException 
WCF: پیاده‌سازی اینترفیس IErrorHandler 
ASMX: ایجاد یک Soap Extension سفارشی
ASP.NET WebAPI


چه زمان‌هایی باید یک استثناء صادر شود؟ 
صادر کردن یک استثناء به تنهایی کار ساده‌ایی است. تنها کافی است throw را همراه شیء exception (exception object) فراخوانی کنیم. اما سوال اینجاست که چه زمانی باید یک استثناء را صادر کنیم؟ چه داده‌هایی را باید به استثناء اضافه کنیم؟ در ادامه به این سوالات خواهیم پرداخت.
همانطور که عنوان گردید استثناءها زمانی باید صادر شوند که یک استثناء اتفاق بیفتد.

اعتبارسنجی آرگومان‌ها
ساده‌ترین مثال، آرگومان‌های مورد انتظار یک متد است:
public void PrintName(string name)
{
     Console.WriteLine(name);
}
در حالت فوق انتظار داریم مقداری برای پارامتر name تعیین شود. متد فوق با آرگومان null نیز به خوبی کار خواهد کرد؛ یعنی مقدار خروجی یک خط خالی خواهد بود. از لحاظ کدنویسی متد فوق به خوبی کار خود را انجام می‌دهد اما خروجی مورد انتظار کاربر نمایش داده نمی‌شود. در این حالت نمی‌توانیم تشخیص دهیم مشکل از کجا ناشی می‌شود.
مشکل فوق را می‌توانیم با صدور استثنای ArgumentNullException رفع کنیم:
public void PrintName(string name)
{
    if (name == null) throw new ArgumentNullException("name");
     
     Console.WriteLine(name);
}
خوب، name باید دارای طول ثابت و همچنین ممکن است حاوی عدد و حروف باشد:
public void PrintName(string name)
{
    if (name == null) throw new ArgumentNullException("name");
    if (name.Length < 5 || name.Length > 10) throw new ArgumentOutOfRangeException("name", name, "Name must be between 5 or 10 characters long");
    if (name.Any(x => !char.IsAlphaNumeric(x)) throw new ArgumentOutOfRangeException("name", name, "May only contain alpha numerics");
     
     Console.WriteLine(name);
}
برای حالت فوق و همچنین جلوگیری از تکرار کدهای داخل متد PrintName می‌توانید یک متد Validator برای کلاسی با نام Person ایجاد کنید.
حالت دیگر صدور استثناء، زمانی است که متدی خروجی مورد انتظارمان را نتواند تحویل دهد. یک مثال بحث‌برانگیز متدی با امضای زیر است:
public User GetUser(int id)
{
}
کاملاً مشخص است که متدی همانند متد فوق زمانیکه کاربری را پیدا نکند، مقدار null را برمی‌گرداند. اما این روش درستی است؟ خیر؛ زیرا همانطور که از نام این متد پیداست باید یک کاربر به عنوان خروجی برگردانده شود.
با استفاده از بررسی null کدهایی شبیه به این را در همه جا خواهیم داشت:
var user = datasource.GetUser(userId);
if (user == null)
    throw new InvalidOperationException("Failed to find user: " + userId);
// actual logic here
به این چنین کدهایی معمولاً The null cancer گفته می‌شود (سرطان نال!) زیرا اجازه داده‌ایم متد، خروجی null را بازگشت دهد. به جای کد فوق می‌توانیم از این روش استفاده کنیم:
public User GetUser(int id)
{
    if (id <= 0) throw new ArgumentOutOfRangeException("id", id, "Valid ids are from 1 and above. Do you have a parsing error somewhere?");
    
    var user = db.Execute<User>("WHERE Id = ?", id);
    if (user == null)
        throw new EntityNotFoundException("Failed to find user with id " + id);
        
    return user;
}
نکته‌ایی که باید به آن توجه کنید این است که در هنگام صدور یک استثناء اطلاعات کافی را نیز به آن پاس دهید. به عنوان مثال در EntityNotFoundException مثال فوق پاس دادن "Failed to find user with id " + id کار دیباگ را برای مصرف کننده، راحتر خواهد کرد.


خطاهای متداول حین کار با استثناءها  


  • صدور مجدد استثناء و از بین بردن stacktrace

کد زیر را در نظر بگیرید:

try
{
    FutileAttemptToResist();
}
catch (BorgException err)
{
     _myDearLog.Error("I'm in da cube! Ohh no!", err);
    throw err;
}
مشکل کد فوق قسمت throw err است. این خط کد، محتویات stacktrace را از بین برده و استثناء را مجدداً برای شما ایجاد خواهد کرد. در این حالت هرگز نمی‌توانیم تشخیص دهیم که منبع خطا از کجا آمده است. در این حالت پیشنهاد می‌شود که تنها از throw استفاده شود. در این حالت استثناء اصلی مجدداً صادر گردیده و مانع حذف شدن محتویات stacktrace خواهد شد(+).
  • اضافه نکردن اطلاعات استثناء اصلی به استثناء جدید

یکی دیگر از خطاهای رایج اضافه نکردن استثناء اصلی حین صدور استثناء جدید است:

try
{
    GreaseTinMan();
}
catch (InvalidOperationException err)
{
    throw new TooScaredLion("The Lion was not in the m00d", err); //<---- استثناء اصلی بهتر است به استثناء جدید پاس داده شود
}
  • ارائه ندادن context information

در هنگام صدور یک استثناء بهتر است اطلاعات دقیقی را به آن ارسال کنیم تا دیباگ کردن آن به راحتی انجام شود. به عنوان مثال کد زیر را در نظر داشته باشید:

try
{
   socket.Connect("somethingawful.com", 80);
}
catch (SocketException err)
{
    throw new InvalidOperationException("Socket failed", err);  
}
هنگامی که کد فوق با خطا مواجه شود نمی‌توان تنها با متن Socket failed تشخیص داد که مشکل از چه چیزی است. بنابراین پیشنهاد می‌شود اطلاعات کامل و در صورت امکان به صورت دقیق را به استثناء ارسال کنید. به عنوان مثال در کد زیر سعی شده است تا حد امکان context information کاملی برای استثناء ارائه شود:
void IncreaseStatusForUser(int userId, int newStatus)
{
    try
    {
         var user  = _repository.Get(userId);
         if (user == null)
             throw new UpdateException(string.Format("Failed to find user #{0} when trying to increase status to {1}", userId, newStatus));
    
         user.Status = newStatus;
         _repository.Save(user);
    }
   catch (DataSourceException err)
   {
       var errMsg = string.Format("Failed to find modify user #{0} when trying to increase status to {1}", userId, newStatus);
        throw new UpdateException(errMsg, err);
   }

نحوه‌ی طراحی استثناءها 
برای ایجاد یک استثناء سفارشی می‌توانید از کلاس Exception ارث‌بری کنید و چهار سازنده‌ی آن را اضافه کنید:
public NewException()
public NewException(string description )
public NewException(string description, Exception inner)
protected or private NewException(SerializationInfo info, StreamingContext context)
سازنده اول به عنوان default constructor شناخته می‌شود. اما پیشنهاد می‌شود که از آن استفاده نکنید، زیرا یک استثناء بدون context information از ارزش کمی برخوردار خواهد بود.
سازنده‌ی دوم برای تعیین description بوده و همانطور که عنوان شد ارائه دادن context information از اهمیت بالایی برخوردار است. به عنوان مثال فرض کنید استثناء KeyNotFoundException که توسط کلاس Dictionary صادر شده است را دریافت کرده‌اید. این استثناء زمانی صادر خواهد شد که بخواهید به عنصری که درون دیکشنری پیدا نشده است دسترسی داشته باشید. در این حالت پیام زیر را دریافت خواهید کرد:
“The given key was not present in the dictionary.”
حالا فرض کنید اگر پیام به صورت زیر باشد چقدر باعث خوانایی و عیب‌یابی ساده‌تر خطا خواهد شد:
“The key ‘abrakadabra’ was not present in the dictionary.”
در نتیجه تا حد امکان سعی کنید که context information شما کاملتر باشد.
سازنده‌ی سوم شبیه به سازنده‌ی قبلی عمل می‌کند با این تفاوت که توسط پارامتر دوم می‌توانیم یک استثناء دیگر را catch کرده یک استثناء جدید صادر کنیم.
سازنده‌ی سوم زمانی مورد استفاده قرار می‌گیرد که بخواهید از Serialization پشتیبانی کنید (به عنوان مثال ذخیره‌ی استثناءها درون فایل و...)

خوب، برای یک استثناء سفارشی حداقل باید کدهای زیر را داشته باشیم:
public class SampleException : Exception
{
    public SampleException(string description)
        : base(description)
    {
        if (description == null) throw new ArgumentNullException("description");
    }
 
    public SampleException(string description, Exception inner)
        : base(description, inner)
    {
        if (description == null) throw new ArgumentNullException("description");
        if (inner == null) throw new ArgumentNullException("inner");
    }
 
    public SampleException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
}

اجباری کردن ارائه‌ی Context information:
برای اجباری کردن context information کافی است یک فیلد اجباری درون سازنده تعریف کنیم. برای مثال اگر بخواهیم کاربر HTTP status code را برای استثناء ارائه دهد باید سازنده‌ها را اینگونه تعریف کنیم:
public class HttpException : Exception
{
    System.Net.HttpStatusCode _statusCode;
     
    public HttpException(System.Net.HttpStatusCode statusCode, string description)
        : base(description)
    {
        if (description == null) throw new ArgumentNullException("description");
        _statusCode = statusCode;
    }
 
    public HttpException(System.Net.HttpStatusCode statusCode, string description, Exception inner)
        : base(description, inner)
    {
        if (description == null) throw new ArgumentNullException("description");
        if (inner == null) throw new ArgumentNullException("inner");
        _statusCode = statusCode;
    }
 
    public HttpException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
     
    public System.Net.HttpStatusCode StatusCode { get; private set; }
 
}
همچنین بهتر است پراپرتی Message را برای نمایش پیام مناسب بازنویسی کنید:
public override string Message
{
        get { return base.Message + "\r\nStatus code: " + StatusCode; }
}
مورد دیگری که باید در کد فوق مد نظر داشت این است که status code قابلیت سریالایز شدن را ندارد. بنابراین باید متد GetObjectData را برای سریالایز کردن بازنویسی کنیم:
public class HttpException : Exception
{
    // [...]
 
    public HttpException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        // this is new
        StatusCode = (HttpStatusCode) info.GetInt32("HttpStatusCode");
    }
 
    public HttpStatusCode StatusCode { get; private set; }
 
    public override string Message
    {
        get { return base.Message + "\r\nStatus code: " + StatusCode; }
    }
 
    // this is new
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue("HttpStatusCode", (int) StatusCode);
    }
}
در اینحالت فیلدهای اضافی در طول فرآیند Serialization به خوبی سریالایز خواهند شد.

در حین صدور استثناءها همیشه باید در نظر داشته باشیم که چه نوع context information را می‌توان ارائه داد، این مورد در یافتن راه‌حل خیلی کمک خواهد کرد.


طراحی پیام‌های مناسب 
پیام‌های exception مختص به توسعه‌دهندگان است نه کاربران نهایی.
نوشتن این نوع پیام‌ها برای برنامه‌نویس کار خسته‌کننده‌ایی است. برای مثال دو مورد زیر را در نظر داشته باشید:
throw new Exception("Unknown FaileType");
throw new Exception("Unecpected workingDirectory");
این نوع پیام‌ها حتی اگر از لحاظ نوشتاری مشکلی نداشته باشند یافتن راه‌حل را خیلی سخت خواهند کرد. اگر در زمان برنامه‌نویسی با این نوع خطاها روبرو شوید ممکن است با استفاده از debugger ورودی نامعتبر را پیدا کنید. اما در یک برنامه و خارج از محیط برنامه‌نویسی، یافتن علت بروز خطا خیلی سخت خواهد بود.
توسعه‌دهندگانی که exception message را در اولویت قرار می‌دهند، معتقد هستند که از لحاظ تجربه‌ی کاربری پیام‌ها تا حد امکان باید فاقد اطلاعات فنی باشد. همچنین همانطور که پیش‌تر عنوان گردید این نوع پیام‌ها همیشه باید در بالاترین سطح نمایش داده شوند نه در لایه‌های زیرین. همچنین پیام‌هایی مانند Unknown FaileType نه برای کاربر نهایی، بلکه برای برنامه‌نویس نیز ارزش چندانی ندارد زیرا فاقد اطلاعات کافی برای یافتن مشکل است.
در طراحی پیام‌ها باید موارد زیر را در نظر داشته باشیم:
- امنیت:
یکی از مواردی که از اهمیت بالایی برخوردار است مسئله امنیت است از این جهت که پیام‌ها باید فاقد مقادیر runtime باشند. زیرا ممکن است اطلاعاتی را در خصوص نحوه‌ی عملکرد سیستم آشکار سازند.
- زبان:
همانطور که عنوان گردید پیام‌های استثناء برای کاربران نهایی نیستند، زیرا کاربران نهایی ممکن است اشخاص فنی نباشند، یا ممکن است زبان آنها انگلیسی نباشد. اگر مخاطبین شما آلمانی باشند چطور؟ آیا تمامی پیام‌ها را با زبان آلمانی خواهید نوشت؟ اگر هم اینکار را انجام دهید تکلیف استثناء‌هایی که توسط Base Class Library و دیگر کتابخانه‌های thirt-party صادر می‌شوند چیست؟ اینها انگلیسی هستند.

در تمامی حالت‌هایی که عنوان شد فرض بر این است که شما در حال نوشتن این نوع پیام‌ها برای یک سیستم خاص هستید. اما اگر هدف نوشتن یک کتابخانه باشد چطور؟ در این حالت نمی‌دانید که کتابخانه‌ی شما در کجا استفاده می‌شود.
اگر هدف نوشتن یک کتابخانه نباشد این نوع پیام‌هایی که برای کاربران نهایی باشند، وابستگی‌ها را در سیستم افزایش خواهند داد، زیرا در این حالت پیام‌ها به یک رابط کاربری خاص گره خواهند خورد.

خب اگر پیام‌ها برای کاربران نهایی نیستند، پس برای کسانی مورد استفاده قرار خواهند گرفت؟ در واقع این نوع پیام می‌تواند به عنوان یک documentation برای سیستم شما باشند.
فرض کنید در حال استفاده از یک کتابخانه جدید هستید به نظر شما کدام یک از پیام‌های زیر مناسب هستند:
"Unecpected workingDirectory"
یا:
"You tried to provide a working directory string that doesn't represent a working directory. It's not your fault, because it wasn't possible to design the FileStore class in such a way that this is a statically typed pre-condition, but please supply a valid path to an existing directory.

"The invalid value was: "fllobdedy"."
یافتن مشکل در پیام اول خیلی سخت خواهد بود زیرا فاقد اطلاعات کافی برای یافتن مشکل است. اما پیام دوم مشکل را به صورت کامل توضیح داده است. در حالت اول شما قطعاً نیاز خواهید داشت تا از دیباگر برای یافتن مشکل استفاده کنید. اما در حالت دوم پیام به خوبی شما را برای یافتن راه‌حل راهنمایی می‌کند.
همیشه برای نوشتن پیام‌های مناسب سعی کنید از لحاظ نوشتاری متن شما مشکلی نداشته باشد، اطلاعات کافی را درون پیام اضافه کنید و تا حد امکان نحوه‌ی رفع مشکل را توضیح دهید
نظرات اشتراک‌ها
سری 11 قسمتی معماری تمیز با Angular 10 و Web API
این پروژه از آخرین نسخه LTS دات نت یعنی .net core 3.1 استفاده می‌کنه
ولی یک پروژه خوب راجع به همین موضوع هست که انگیولار ۱۰ به همراه دات نت نسخه ۵ و معماری تمیز (Clean Architecture) استفاده می‌کنه و به صورت بسته نیوگت هم عرضه می‌شه که آدرس گیت هاب:
هست.
نظرات اشتراک‌ها
معرفی کتابخانه‌ی DNTPersianUtils.Core
در خصوص کلاس اعتبار سنجی شماره شبا:
حلقه‌ای که برای بررسی نوشته شده  (به نظر من) خیلی هوشمندانه است. ابتدا از رقم چهارم سمت چپ شروع می‌کند و سپس بعد از اتمام، ۴ رقم اول را هم لحاظ می‌کند.
در تعیین بخش‌پذیری عدد ۲۶ رقمی شماره شبا بر ۹۷، متوجه الگوریتم به کار رفته نشدم. ابتدا باقیمانده‌ی ۱۰ برابر هر عدد با مقدار عددی کاراکتر محاسبه می‌شود. سپس برای عدد بعدی همین روال طی می‌شود و ...
اگر معادل IR ابتدا را حساب کنیم مثلا ۲۸۱۷ و ارقام سوم و چهارم را پشت آن گذاشته، از ابتدای شماره شبا حذف کرده و به انتهای آن اضافه کنیم، می‌شود باقیمانده را بر ۹۷ محاسبه کرد.
پاسخ با الگوریتم شما یکسان است (شاید روش‌های دیگری هم وجود داشته باشد!) چنانچه ممکن است، در خصوص آن الگوریتم بخش‌پذیری بر ۹۷ که استفاده کرده‌اید، قدری توضیح می‌دهید؟ متشکرم
نظرات اشتراک‌ها
راهنمای بروز بودن یک برنامه‌نویس NET. در سال 1398
یکی از راه‌های بروز بودن به نظر من رفتن از شرکت کنونی و به یک شرکت جدید‌تر هست! پروژه‌‌های زیادی هستن که مثلا ۱۰ سال پیش زده شدن و مشتری خودشون رو دارن و شرکت‌ها هیچ تمایلی به ارتقا و بهبودش ندارن و همیشه یک شخص junior پیدا میکنن که نگهداریش کنن. همچنین یکی از عواملی که باعث میشه آدم عقب بیفته از تکنولوژی و مطالب روز همکارایی هستن که هیچ علاقه ای به بهتر کردن خودشون ندارن و به تنها چیزی که فکر میکنن اینه که ساعت ۶ بشه و فلنگو ببندن! توی یک همچین محیطی شما باشی، ناخواسته مثل خودشون میشی و یهو میبینی که همه چی دورت عوض شده و شما هنوز WebForm رو هم درست یاد نگرفتی ....
اشتراک‌ها
ده عادت کوچکی که در بلندمدت مدت اثر مثبت دارد

۱) هفته‌ای سه بار ورزش سنگین (مثل وزنه‌زدن) بکنید.

۲) هر روز برای فردا ۳-۴ اولویت کاری/فکری مشخص کنید.

۳) روزانه ۶۰ دقیقه مطالعه کنید.

۴) روزانه ۳۰ دقیقه بنویسید. 

۵) هر روز ۷-۸ ساعت بخوابید.

۶) هر روز ۳۰ دقیقه پیاده‌روی کنید. 

۷) برنامه روزه گاه-به-گاه (Intermittent) داشته باشید و برای زمان طولانی (مثلا ۱۵-۱۶ ساعت) هیچ چیزی نخورید.

۸) در حال زندگی کنید و تمرین حضور ذهن یا Mindfulness داشته باشید. 

۹) عشق و محبت به دیگران را تمرین و تجربه کنید. 

۱۰) سی‌درصد درآمد‌تان را پس‌انداز کنید.  

ده عادت کوچکی که در بلندمدت مدت اثر مثبت دارد
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دوم - ایجاد ساختار اولیه‌ی مثال این سری
سلام؛ من مشغول انجام یک پروژه هستم که هم بخش موبایل داره (با اندروید استودیو) و هم بخش وب داره (با Asp Core). Web Api هم با Asp Core کار کردم فقط مساله ای که تقریبا ۱۰ روزه ذهنمو درگیر کرده سیستم Authorize هست تمام مقاله‌ها ی شما رو مطالعه کردم الان بین IdentityServer و Jwt وCookie موندم کدوم رو انتخاب کنم ممنون میشم راهنمایی کنید. پروژه تقریبا مثل یک شبکه اجتماعی هستش که هم از طریق اپ موبایل و هم از طریق سایت در دسترسه. این سری مقالات identityServer فوق العاده بود. قبل از مطالعه این سری از مقالات شما داشتم سیستم رو بر اساس Jwt جلو میبردم ولی با مطالعه این مقالات به نظرم سیستم identityServer بهتره لطفا در این مورد نظرتون رو بفرمایید.
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت سوم - نرمال سازها و اعتبارسنج‌ها
با سلام.
در پروژه ای ما باید یک ثبت نام اولیه برای کاربر انجام دهیم به این شکل که کاربر را با حداقل اطلاعات عضو سیستم می‌کنیم و پس از اولین لاگین اطلاعات تکمیلی ثبت نام رو می‌گیریم.
            // Extending the built-in validator
            validateEmail(user, errors);

بنده این تابع رو بکلی غیر فعال کردم ولی بازهم ایمیل رو اعتبار سنجی می‌کنه.
همین مشکل رو ما در مورد پسورد داریم و باید در مرحله اول یک پسورد ۴ کاراکتری بدیم ولی در مرحله دوم باید یک پسورد ۱۰ کاراکتری داشته باشیم.
در مورد پسورد یک راه پیدا کردیم و اونم اینه که پسورد هش شده رو در دیتابیس ذخیره کنیم ولی دنبال راه حل بهتری هستم.
آیا کلا راهی وجود داره تا بدون نیاز به Migration و update datatbase سیستم اعتبار سنجی رو برای برخی توابع فعال و غیر فعال کرد؟
با تشکر
مطالب
React 16x - قسمت 7 - ترکیب کامپوننت‌ها - بخش 1 - ارسال داده‌ها، مدیریت رخ‌دادها
تا اینجا، تنها با یک تک کامپوننت کار کردیم؛ اما یک برنامه‌ی واقعی ترکیبی است از چندین کامپوننت که در نهایت درخت کامپوننت‌ها را در React تشکیل می‌دهند. به همین جهت در طی چند قسمت، نکات ترکیب کامپوننت‌ها را بررسی می‌کنیم.


ترکیب کامپوننت‌ها

در ادامه، همان برنامه‌ی تا قسمت 5 را که کار نمایش یک counter را انجام می‌دهد، تکمیل می‌کنیم. در این برنامه اگر به فایل index.js دقت کنید، کار رندر تک کامپوننت Counter را انجام می‌دهیم:
ReactDOM.render(<Counter />, document.getElementById("root"));
اما یک برنامه‌ی واقعی React، متشکل از درختی از کامپوننت‌ها است. به این ترتیب با ترکیب و در کنار هم قرار دادن کامپوننت‌های مختلف، می‌توان به UI ای کارآمد و پیچیده رسید.
برای نمایش این مفهوم، کامپوننت جدید src\components\counters.jsx را ایجاد می‌کنیم. قصد داریم در این کامپوننت، لیستی از کامپوننت‌های Counter را رندر کنیم. سپس در index.js، بجای رندر کامپوننت Counter، کامپوننت جدید Counters را رندر می‌کنیم. به این ترتیب درخت کامپوننت‌های برنامه، در سطح بالایی خودش از کامپوننت Counters شروع می‌شود و سپس فرزندان آن‌را کامپوننت‌های Counter تشکیل می‌دهند. به همین جهت فایل index.js را به صورت زیر ویرایش می‌کنیم تا به کامپوننت Counters اشاره کند:
import Counters from "./components/counters";

ReactDOM.render(<Counters />, document.getElementById("root"));
سپس به فایل جدید src\components\counters.jsx مراجعه کرده و با استفاده از قطعه کدهای کمکی imrc و cc که در قسمت‌های قبل با آن‌ها آشنا شدیم، ساختار بدنه‌ی کامپوننت جدید Counters را ایجاد می‌کنیم. اکنون در متد render آن، یک div را ایجاد کرده و داخل آن، چندین کامپوننت Counter را رندر می‌کنیم:
import React, { Component } from "react";

import Counter from "./counter";

class Counters extends Component {
  state = {};

  render() {
    return (
      <div>
        <Counter />
        <Counter />
        <Counter />
        <Counter />
      </div>
    );
  }
}

export default Counters;
در این حالت اگر به مرورگر مراجعه کنیم، مشاهده خواهیم کرد که هر کامپوننت، state خاص خودش را دارد و از سایر کامپوننت‌ها ایزوله است:


در مرحله‌ی بعد، بجای رندر و درج دستی این کامپوننت‌ها، آرایه‌ای از اشیاء counter را ایجاد کرده و سپس آن‌ها را توسط متد Array.map رندر می‌کنیم:
import React, { Component } from "react";
import Counter from "./counter";

class Counters extends Component {
  state = {
    counters: [
      { id: 1, value: 0 },
      { id: 2, value: 0 },
      { id: 3, value: 0 },
      { id: 4, value: 0 }
    ]
  };

  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <Counter key={counter.id} />
        ))}
      </div>
    );
  }
}

export default Counters;
در اینجا یک خاصیت جدید را به شیء منتسب به خاصیت state به نام counters اضافه کرده‌ایم. این خاصیت حاوی آرایه‌ای از اشیاء counter است که هر کدام دارای یک id (که در قسمت key ذکر خواهد شد) و مقداری اولیه است. سپس آرایه‌ی this.state.counters را توسط متد map، رندر کرده‌ایم. تا اینجا پس از ذخیره‌ی فایل و بارگذاری مجدد برنامه، همان خروجی قبلی را مشاهده خواهیم کرد.


ارسال داده‌ها به کامپوننت‌ها

مشکل! مقدار value هر شیء شمارشگر تعریف شده، به کامپوننت‌های مرتبط رندر شده اعمال نشده‌است. برای مثال اگر value اولین شیء را به 4 تغییر دهیم، هنوز هم این کامپوننت با همان مقدار صفر شروع به کار می‌کند. برای رفع این مشکل، به همان روشی که ویژگی key کامپوننت Counter را مقدار دهی کردیم، می‌توان ویژگی‌های سفارشی دیگری را تعریف و مقدار دهی کرد:
  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <Counter key={counter.id} value={counter.value} selected={true} />
        ))}
      </div>
    );
پس از تعریف ویژگی‌های دلخواه value و selected که یکی از آن‌ها به مقدار value شیء counter مرتبط متصل است، به خود کامپوننت Counter مراجعه کرده و سپس در ابتدای متد render آن، خاصیت props به ارث رسیده شده‌ی از کلاس پایه‌ی Component را جهت بررسی بیشتر لاگ می‌کنیم:
class Counter extends Component {
  state = {
    count: 0
  };

  render() {
    console.log("props", this.props);
    //...
پس از ذخیره‌ی فایل counter.jsx و بارگذاری مجدد برنامه، یک چنین خروجی در کنسول توسعه دهندگان مرورگر قابل مشاهده است:


خاصیت this.props، یک شیء ساده‌ی جاوا اسکریپتی است و شامل تمام ویژگی‌هایی می‌باشد که ما در کامپوننت Counters برای هر کدام از کامپوننت‌های Counter رندر شده‌ی توسط آن، تعریف کردیم. برای نمونه دو ویژگی جدید value و selected را که به تعاریف المان‌های Counter در کامپوننت Counters اضافه کردیم، در اینجا به همراه مقادیر منتسب به آن‌ها، قابل مشاهده هستند. البته در این خروجی، key را ملاحظه نمی‌کنید؛ چون هدف اصلی آن، معرفی یکتای المان‌ها در DOM مجازی React است.
بنابراین اکنون می‌توان به value تنظیم شده‌ی در کامپوننت Counters به صورت this.props.value در کامپوننت Counter دسترسی یافت و سپس از آن جهت مقدار دهی اولیه‌ی counter استفاده کرد.
class Counter extends Component {
  state = {
    count: this.props.value
  };
اکنون اگر تغییرات کامپوننت Counter را ذخیره کرده و به مرورگر مراجعه کنیم، در اولین بار نمایش برنامه و بدون اعمال هیچگونه تغییری، یک چنین خروجی حاصل می‌شود:


یک نکته: در اینجا selected={true} را داریم. اگر مقدار آن‌را حذف کنیم، یعنی selected تنها درج شود، مقدار آن، همان true دریافت خواهد شد.


تعریف فرزند برای المان‌های کامپوننت‌ها

ویژگی‌های اضافه شده‌ی به تعاریف المان‌های کامپوننت‌ها، توسط خاصیت this.props، به هر کدام از آن کامپوننت‌ها منتقل می‌شوند. این خاصیت props، یک خاصیت ویژه را به نام children، نیز دارا است و از آن برای دسترسی به المان‌های تعریف شده‌ی بین تگ‌های یک المان اصلی استفاده می‌شود:
  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <Counter key={counter.id} value={counter.value} selected={true}>
            <h4>‍Counter #{counter.id}</h4>
          </Counter>
        ))}
      </div>
    );
  }
در اینجا بین تگ‌های ابتدا و انتهای تعریف المان Counter، یک محتوا نیز تعریف شده‌است. اکنون اگر به خروجی کنسول توسعه دهندگان مرورگر دقت کنیم، خاصیت جدید اضافه شده‌ی children را نیز می‌توان مشاهده کرد:


یک نمونه مثال واقعی این قابلیت، امکان تعریف محتوای دیالوگ باکس‌ها، توسط استفاده کنند‌ه‌ی از آن است.


روش دیباگ برنامه‌های React

افزونه‌ی مفید React developer tools را می‌توانید برای مرورگرهای کروم و فایرفاکس، دریافت و نصب کنید. برای نمونه پس از نصب آن در مرورگر کروم، یک برگه‌ی جدید به لیست برگه‌های کنسول توسعه دهندگان آن اضافه می‌شود:


همانطور که مشاهده کنید، درخت کامپوننت‌های برنامه را در برگه‌ی جدید Components، می‌توان مشاهده کرد. در اینجا با انتخاب هر کدام از فرزندان این درخت، مشخصات آن نیز مانند props و state، در کنار صفحه ظاهر می‌شوند. همچنین در بالای همین قسمت، 4 آیکن مشاهده‌ی سورس، مشاهده‌ی DOM و یا لاگ کردن جزئیات شیء کامپوننت انتخابی در کنسول هم درج شده‌اند:


که برای نمونه چنین خروجی را لاگ می‌کند:



بررسی تفاوت‌های خواص props و state

در کامپوننت Counter، از props برای مقدار دهی اولیه‌ی state استفاده می‌کنیم:
class Counter extends Component {
  state = {
    count: this.props.value
  };
اکنون این سؤال مطرح می‌شود که چه تفاوتی بین props و state وجود دارد؟
- props حاوی اطلاعاتی است که به یک کامپوننت ارسال می‌کنیم؛ اما state حاوی اطلاعاتی است که مختص به آن کامپوننت بوده و private است. یعنی سایر کامپوننت‌ها نمی‌توانند به state کامپوننت دیگری دسترسی پیدا کنند. برای مثال در کامپوننت Counters، تمام attributes سفارشی تنظیم شده‌ی بر روی تعاریف المان‌های کامپوننت Counter، جزئی از اطلاعات props خواهند بود. در اینجا نمی‌توان به state کامپوننت مدنظری دسترسی یافت و آن‌را مقدار دهی کرد. به همین ترتیب state کامپوننت Counters نیز در سایر کامپوننت‌ها قابل دسترسی نیست.
- همچنین باید درنظر داشت که props، در مقایسه با state، فقط خواندنی است. به عبارتی مقدار ورودی به یک کامپوننت را داخل آن کامپوننت نمی‌توان تغییر داد. برای مثال سعی کنید در داخل متد رویدادگردان کلیک موجود در کامپوننت Counter، مقدار this.props.value را به صفر تنظیم کنید. در این حالت با کلیک بر روی دکمه‌ی Increment، بلافاصله خطای readonly بودن خواص شیء منتسب به props را دریافت می‌کنیم. در اینجا اگر نیاز است این مقدار را داخل کامپوننت تغییر دهیم، باید ابتدا این مقدار را دریافت کرده و سپس آن‌را داخل state قرار دهیم. پس از آن امکان ویرایش اطلاعات منتسب به state، داخل یک کامپوننت وجود خواهد داشت.


صدور و مدیریت رخ‌دادها

در ادامه می‌خواهیم در کنار هر دکمه‌ی Increment کامپوننت شمارشگر، یک دکمه‌ی Delete هم قرار دهیم:


مشکل! اگر کد مدیریتی handleDelete را در کامپوننت Counter قرار دهیم، چگونه باید به لیست آرایه‌ی اشیاء counters والد آن، یعنی کامپوننت Counters که سبب رندر شدن کامپوننت‌های شمارشگر شده (state = { counters: [ ] })، دسترسی یافت و شیء‌ای را از آن حذف کرد؟ در React، کامپوننتی که state ای را تعریف می‌کند، باید کامپوننتی باشد که قرار است آن‌را تغییر دهد و اطلاعات state هر کامپوننت، صرفا متعلق به آن کامپوننت بوده و جزو اطلاعات خصوصی آن است. بنابراین مدیریت حذف و یا افزودن کامپوننت‌ها در لیست نمایش داده شده، باید جزو وظایف کامپوننت Counters باشد و نه Counter.
برای حل این مشکل، کامپوننت Counter تعریف شده (کامپوننت فرزند) باید سبب بروز رخ‌داد onDelete شود تا کامپوننت Counters (کامپوننت والد)، آن‌را توسط متد handleDelete مدیریت کند. بنابراین ابتدا به کامپوننت Counters (کامپوننت والد) مراجعه کرده و متد رویدادگردان handleDelete را به آن اضافه می‌کنیم:
  handleDelete = () => {
    console.log("handleDelete called.");
  };
سپس ارجاعی از این متد را به صورت خاصیتی از props به کامپوننت Counter (کامپوننت فرزند) ارسال خواهیم کرد؛ برای این منظور در کامپوننت Counters (کامپوننت والد)، ویژگی onDelete را به تعریف المان Counter اضافه کرده و آن‌را با ارجاعی به متدhandleDelete  مقدار دهی می‌کنیم:
<Counter
     key={counter.id}
     value={counter.value}
     selected={true}
     onDelete={this.handleDelete}
/>
پس از آن به کامپوننت Counter مراجعه کرده و دکمه‌ی جدید Delete را به صورت زیر در کنار دکمه‌ی Increment تعریف می‌کنیم:
<button
  onClick={this.props.onDelete}
  className="btn btn-danger btn-sm m-2"
>
  Delete
</button>
در اینجا onClick، به خاصیت onDelete شیء props ارسالی به کامپوننت متصل شده‌است.
اکنون اگر برنامه را ذخیره کرده و پس از بارگذاری مجدد برنامه در مرورگر بر روی دکمه‌ی Delete کلیک کنیم، پیام «handleDelete called» در کنسول توسعه دهندگان مرورگر لاگ می‌شود. به این ترتیب کامپوننت فرزند سبب بروز رخ‌دادی شده و والد آن، این رخ‌داد را مدیریت می‌کند.


به روز رسانی state

تا اینجا دکمه‌ی Delete فرزند، به متد handleDelete والد متصل شده‌است. مرحله‌ی بعد، پیاده سازی واقعی حذف یک المان از DOM مجازی و به روز رسانی state است. برای اینکار ابتدا به رخ‌دادگردان onClick، در کامپوننت شمارشگر، مراجعه کرده و id دریافتی را به سمت والد ارسال می‌کنیم:
onClick={() => this.props.onDelete(this.props.id)}
البته در سمت والد نیز باید این id را به صورت یک خاصیت جدید به props اضافه کنیم (تا this.props.id فوق کار کند)؛ چون ویژگی key، مختص DOM مجازی بوده و به props اضافه نمی‌شود:
<Counter
  key={counter.id}
  value={counter.value}
  selected={true}
  onDelete={this.handleDelete}
  id={counter.id}
/>
اکنون این id را در کامپوننت والد دریافت و به آن واکنش نشان می‌دهیم:
  handleDelete = counterId => {
    console.log("handleDelete called.", counterId);
    const counters = this.state.counters.filter(
      counter => counter.id !== counterId
    );
    this.setState({ counters }); // = this.setState({ counters: counters });
  };
همانطور که پیشتر نیز در این سری عنوان شده، در React، مقدار state را به صورت مستقیم تغییر نمی‌دهیم و اینکار باید از طریق متد setState آن صورت گیرد. به عبارت دیگر مستقیما خاصیت counters شیء منتسب به خاصیت state را تغییر نمی‌دهیم. ابتدا یک آرایه‌ی جدید از المان‌ها را تولید کرده و به متد setState ارسال می‌کنیم. سپس React، هم خاصیت counters و هم UI را بر این اساس به روز رسانی خواهد کرد. در اینجا، لیست جدید counters، بر اساس id دریافتی از کامپوننت فرزند، تولید شده و به متد this.setState ارسال می‌شود. در این حالت اگر برنامه را ذخیره کرده و پس از بارگذاری مجدد آن در مرورگر، بر روی دکمه‌ی Delete هر ردیف کلیک کنیم، آن ردیف از UI حذف خواهد شد.

البته پیاده سازی ما تا به اینجا بدون مشکل کار می‌کند، اما به ازای هر خاصیت counter، یک ویژگی جدید را به تعریف المان مرتبط اضافه کرده‌ایم که در طول زمان بیش از اندازه طولانی خواهد شد. برای رفع این مشکل، خود شیء counter را به صورت یک ویژگی جدید به کامپوننت مرتبط با آن ارسال می‌کنیم. به این ترتیب اگر در آینده خاصیتی را به این شیء اضافه کردیم، دیگر نیازی نیست تا آن‌را به صورت دستی و مجزا تعریف کنیم. به همین جهت ابتدا تعریف المان Counter را به صورت زیر خلاصه می‌کنیم که در آن ویژگی جدید counter، حاوی کل شیء counter است:
<Counter
  key={counter.id}
  counter={counter}
  onDelete={this.handleDelete}
/>
سپس در سمت کامپوننت فرزند شمارشگر، دو تغییر this.props.counter.value و this.props.counter.id باید صورت گیرند تا مقادیر شیء counter به درستی خوانده شوند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-07.zip
مطالب
بررسی ویجت Kendo UI File Upload
Kendo UI به همراه یک ویجت وب مخصوص ارسال فایل‌ها به سرور نیز هست. این ویجت قابلیت ارسال چندین فایل با هم را به صورت Ajax ایی دارا است و همچنین کاربران می‌توانند فایل‌ها را با کشیدن و رها کردن بر روی آن، به لیست فایل‌های قابل ارسال اضافه کنند.
ارسال فایل Ajax ایی آن توسط HTML5 File API صورت می‌گیرد که در تمام مرورگرهای جدید پشتیبانی خوبی از آن وجود دارد. در مرورگرهای قدیمی‌تر، به صورت خودکار همان حالت متداول ارسال همزمان فایل‌ها را فعال می‌کند (یا همان post back معمولی).

فعال سازی مقدماتی kendoUpload

ابتدایی‌ترین حالت کار با kendoUpload، فعال سازی حالت post back معمولی است؛ به شرح زیر:
<form method="post" action="submit" enctype="multipart/form-data">
  <div>
    <input name="files" id="files" type="file" />    
    <input type="submit" value="Submit" class="k-button" />
  </div>
</form>
<script>
  $(document).ready(function() {
     $("#files").kendoUpload();
  });
</script>
در این حالت صرفا input با نوع file، با ظاهری سازگار با سایر کنترل‌های Kendo UI به نظر می‌رسد و عملیات ارسال فایل، همانند قبل به همراه یک post back است. این روش برای حالتی مفید است که بخواهید یک فایل را به همراه سایر عناصر فرم در طی یک مرحله به سمت سرور ارسال کنید.


فعال سازی حالت ارسال فایل Ajax ایی kendoUpload

برای فعال سازی ارسال Ajax ایی فایل‌ها در Kendo UI نیاز است خاصیت async آن‌را به نحو ذیل مقدار دهی کرد:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: { // async configuration
                    saveUrl: "@Url.Action("Save", "Home")", // the url to save a file is '/save'
                    removeUrl: "@Url.Action("Remove", "Home")", // the url to remove a file is '/remove'
                    autoUpload: false, // automatically upload files once selected
                    removeVerb: 'POST'
                },
                multiple: true,
                showFileList: true
            }); 
        });
    </script>
در اینجا دو آدرس ذخیره سازی فایل‌ها و همچنین حذف آن‌ها را مشاهده می‌کنید. امضای این دو اکشن متد در ASP.NET MVC به صورت ذیل هستند:
        [HttpPost]
        public ActionResult Save(IEnumerable<HttpPostedFileBase> files)
        {
            if (files != null)
            {
                // ...
                // Process the files and save them
                // ...
            }

            // Return an empty string to signify success
            return Content("");
        }

        [HttpPost]
        public ContentResult Remove(string[] fileNames)
        {
            if (fileNames != null)
            {
                foreach (var fullName in fileNames)
                {
                    // ...
                    // delete the files
                    // ...
                }
            }

            // Return an empty string to signify success
            return Content("");
        }
در هر دو حالت، لیستی از فایل‌ها توسط kendoUpload به سمت سرور ارسال می‌شوند. در حالت Save، محتوای این فایل‌ها جهت ذخیره سازی بر روی سرور در دسترس خواهد بود. در حالت Remove، صرفا نام این فایل‌ها برای حذف از سرور، توسط کاربر ارسال می‌شوند.
دو دکمه‌ی حذف با کارکردهای متفاوت در ویجت kendoUpload وجود دارند. در ابتدای کار، پیش از ارسال فایل‌ها به سرور:


کلیک بر روی دکمه‌ی حذف در این حالت، صرفا فایلی را از لیست سمت کاربر حذف می‌کند.

پس از ارسال فایل‌ها به سرور:


اما پس از پایان عملیات ارسال، اگر کاربر بر روی دکمه‌ی حذف کلیک کند، توسط آدرس مشخص شده توسط خاصیت removeUrl، نام فایل‌های مورد نظر، برای حذف از سرور ارسال می‌شوند.


چند نکته‌ی تکمیلی
- تنظیم خاصیت autoUpload به true سبب می‌شود تا پس از انتخاب فایل‌ها توسط کاربر، بلافاصله و به صورت خودکار عملیات ارسال فایل‌ها به سرور آغاز شوند. اگر به false تنظیم شود، دکمه‌ی ارسال فایل‌ها در پایین لیست نمایش داده خواهد شد.
- شاید علاقمند باشید تا removeVerb را به DELETE تغییر دهید؛ بجای POST. به همین منظور می‌توان خاصیت removeVerb در اینجا مقدار دهی کرد.
- با تنظیم خاصیت multiple به true، کاربر قادر خواهد شد تا توسط صفحه‌ی دیالوگ انتخاب فایل‌ها، قابلیت انتخاب بیش از یک فایل را داشته باشد.
- showFileList نمایش لیست فایل‌ها را سبب می‌شود.


تعیین پسوند فایل‌‌های صفحه‌ی انتخاب فایل‌ها

هنگامیکه کاربر بر روی دکمه‌ی انتخاب فایل‌ها برای ارسال کلیک می‌کند، در صفحه‌ی دیالوگ باز شده می‌توان پسوندهای پیش فرض مجاز را نیز تعیین کرد.
برای این منظور تنها کافی است ویژگی accept را به input از نوع فایل اضافه کرد. چند مثال در این مورد:
<!-- Content Type with wildcard.  All Images -->
<input type="file" id="demoFile" title="Select file" accept="image/*" />
 
<!-- List of file extensions -->
<input type="file" id="demoFile" title="Select file" accept=".jpg,.png,.gif" />
 
<!-- Any combination of the above -->
<input type="file" id="demoFile" title="Select file" accept="audio/*,application/pdf,.png" />


نمایش متن کشیدن و رها کردن، بومی سازی برچسب‌ها و نمایش راست به چپ

همانطور که در تصاویر فوق ملاحظه می‌کنید، نمایش این ویجت راست به چپ و پیام‌های آن نیز ترجمه شده‌اند.
برای راست به چپ سازی آن مانند قبل تنها کافی است input مرتبط، در یک div با کلاس k-rtl محصور شود:
        <div class="k-rtl k-header">
            <input name="files" id="files" type="file"  />
        </div>
برای بومی سازی پیام‌های آن می‌توان مانند مثال ذیل، خاصیت localization را مقدار دهی کرد:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                 //...
                },
                //...
                localization: {
                    select: 'انتخاب فایل‌ها برای ارسال',
                    remove: 'حذف فایل',
                    retry: 'سعی مجدد',
                    headerStatusUploading: 'در حال ارسال فایل‌ها',
                    headerStatusUploaded: 'پایان ارسال',
                    cancel: "لغو",
                    uploadSelectedFiles: "ارسال فایل‌ها",
                    dropFilesHere: "فایل‌ها را برای ارسال، کشیده و در اینجا رها کنید",
                    statusUploading: "در حال ارسال",
                    statusUploaded: "ارسال شد",
                    statusWarning: "اخطار",
                    statusFailed: "خطا در ارسال"
                }
            });
        });
    </script>
به علاوه متن dropFilesHere به صورت پیش فرض نامرئی است. برای نمایش آن نیاز است CSS موجود را بازنویسی کرد تا em مرتبط مرئی شود:
<style type="text/css">
div.k-dropzone {
    border: 1px solid #c5c5c5; /* For Default; Different for each theme */
}

div.k-dropzone em {
    visibility: visible;
}
</style>


تغییر قالب نمایش لیست فایل‌ها

لیست فایل‌ها در ویجت kendoUpload دارای یک قالب پیش فرض است که امکان بازنویسی کامل آن وجود دارد. ابتدا نیاز است یک kendo-template را بر این منظور تدارک دید:
    <script id="fileListTemplate" type="text/x-kendo-template">
        <li class='k-file'>
            <span class='k-progress'></span>
            <span class='k-icon'></span>
            <span class='k-filename' title='#=name#'>#=name# (#=size# bytes)</span>
            <strong class='k-upload-status'></strong>
        </li>
    </script>
و سپس برای استفاده از آن خواهیم داشت:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                // ...
                },
                // ...
                template: kendo.template($('#fileListTemplate').html()),
                // ...
            });
        });
    </script>
در این قالب، مقدار size هر فایل نیز در کنار نام آن نمایش داده می‌شود.


رخدادهای ارسال فایل‌ها

افزونه‌ی kendoUpload در حالت ارسال Ajax ایی فایل‌ها، رخدادهایی مانند شروع به ارسال، موفقیت، پایان، درصد ارسال فایل‌ها و امثال آن‌را نیز به همراه دارد که لیست کامل آن‌ها را در ذیل مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: { // async configuration
                //...
                },
                //...
                localization: {
                },
                cancel: function () {
                    console.log('Cancel Event.');
                },
                complete: function () {
                    console.log('Complete Event.');
                },
                error: function () {
                    console.log('Error uploading file.');
                },
                progress: function (e) {
                    console.log('Uploading file ' + e.percentComplete);
                },
                remove: function () {
                    console.log('File removed.');
                },
                select: function () {
                    console.log('File selected.');
                },
                success: function () {
                    console.log('Upload successful.');
                },
                upload: function (e) {
                    console.log('Upload started.');
                }
            }); 
        });
    </script>


ارسال متادیتای اضافی به همراه فایل‌های ارسالی

فرض کنید می‌خواهید به همراه فایل‌های ارسالی به سرور، پارامتر codeId را نیز ارسال کنید. برای این منظور باید خاصیت e.data رویداد upload را به نحو ذیل مقدار دهی کرد:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                //...
                },
                //...
                localization: {
                },
                upload: function (e) {
                    console.log('Upload started.');
                    // Sending metadata to the save action
                    e.data = {
                        codeId: "1234567",
                        param2: 12
                        //, ...
                    };
                }
            });
        });
    </script>
سپس در سمت سرور، امضای متد Save بر اساس پارامترهای تعریف شده در سمت کاربر، به نحو ذیل تغییر می‌کند:
   [HttpPost]
  public ActionResult Save(IEnumerable<HttpPostedFileBase> files, string codeId)


فعال سازی ارسال batch

اگر در متد Save سمت سرور یک break point قرار دهید، مشاهده خواهید کرد که به ازای هر فایل موجود در لیست در سمت کاربر، یکبار متد Save فراخوانی می‌شود و عملا متد Save، لیستی از فایل‌ها را در طی یک فراخوانی دریافت نمی‌کند. برای فعال سازی این قابلیت تنها کافی است خاصیت batch را به true تنظیم کنیم:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                    // ....
                    batch: true
                },
            });
        });
    </script>
به این ترتیب دیگر لیست فایل‌ها به صورت مجزا در سمت کاربر نمایش داده نمی‌شود و تمام آن‌ها با یک کاما از هم جدا خواهند شد. همچنین دیگر شاهد نمایش درصد پیشرفت تکی فایل‌ها نیز نخواهیم بود و اینبار درصد پیشرفت کل batch گزارش می‌شود.
در یک چنین حالتی باید دقت داشت که تنظیم maxRequestLength در web.config برنامه الزامی است؛ زیرا به صورت پیش فرض محدودیت 4 مگابایتی ارسال فایل‌ها توسط ASP.NET اعمال می‌شود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <!-- The request length is in kilobytes, execution timeout is in seconds  -->
    <httpRuntime maxRequestLength="10240" executionTimeout="120" />
  </system.web>

  <system.webServer>
    <security>
      <requestFiltering>
        <!-- The content length is in bytes  -->
        <requestLimits maxAllowedContentLength="10485760"/>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>
مطالب
Cookie - قسمت اول

مقدمه 

مدت زیادی است که کوکی‌ها در عرصه وب نقش مهمی ایفا میکنند، اما متاسفانه مفهوم روشن و واضحی از آن و نحوه کار آن در اختیار اکثر کاربران و توسعه دهندگان وب نیست. شاید اولین مشکل ناشی از سوءتفاهم‌های بسیاری باشد که درباره کوکی وجود دارد. مثلا برخی آن را ابزاری صرفا برای جاسوسی از کاربران اینترنتی میدانند. برخی دیگر، از آنها و نحوه کارکردشان کلا صرفنظر میکنند. مشکل دیگری که در رابطه با کوکی‌ها میتوان برشمرد، عدم وجود رابط کاربری مناسب برای بررسی و مدیریت کوکی هاست. اما با وجود این مشکلات و برخی دیگر امروزه کوکی‌ها جزئی بسیار مهم در وب هستند که درصورت حذفشان، بسیاری از وب سایتها و برنامه‌های مبتنی بر وب از کار خواهند افتاد.
یک کوکی (cookie به معنی شیرینی یا کلوچه! که با عناوین دیگری چون Http Cookie و Web Coockie و Browser Cookie نیز شناخته میشود)، به داده‌های ارسالی از یک وب سرور (که معمولا بصورت داده‌های متنی کدگذاری شده هستند) اطلاق میشود که در مخزنی مخصوص در مرورگر کاربر به هنگام بازدید از یک سایت ذخیره میشود. وقتیکه کاربر سایت مذبور را در آینده دوباره مرور کند، این داده‌های ذخیره شده توسط مروگر به وب سرور ارسال میشود تا مثلا فعالیتهای قبلی کاربر مورد بررسی قرار گیرد. کوکی‌ها برای فراهم کردن مکانیزمی قابل اعتماد جهت ذخیره فعالیتهای قبلی یا آخرین وضعیت کاربر در یک وب‌سایت طراحی شده‌اند. با اینکه کوکی‌ها دسترسی بسیار محدودی در سمت کلاینت دارند (تقریبا هیچ دسترسی‌ای به هیچیک از منابع سیستم کاربر ندارند) اما با پیگیری هوشمند و هدفمند برخی از آنها میتوان به داده‌هایی از تاریخچه فعالیتهای کاربر در یک مرورگر و سایت خاص دست یافت که به نوعی نقض حریم شخصی کاربران به حساب می‌آید.

نکته: درواقع میتوان گفت که از کوکی به نوعی برای فراهم کردن "حافظه" موقت برای مرورگرها در ارتباط با وب سرورها استفاده میشود.

پروتوکل HTTP که برای تبادل داده‌ها میان مرورگر و وب سرور در بارگذاری صفحات وب استفاده میشود، پروتوکلی بدون حالت یا وضعیت (state-less) است. بدین معنی که به محض ارسال داده‌های یک صفحه وب به سمت مرورگری که آنرا درخواست کرده، وب سرور هیچ چیزی از این تبادل داده را ذخیره و نگهداری نمیکند. بنابراین در درخواست‌های دوباره و سه باره و ... بعدی، وب سرور با آنها همچون اولین درخواست برخورد کرده و رفتاری کاملا یکسان در برخورد با این درخواستها نشان خواهد و دقیقا همان داده‌ها را به سمت مرورگر ارسال خواهد کرد.
این رفتار در موارد زیادی میتواند دردسرساز باشد. مثلا وب سرور نمیتواند بفهمد که یک کاربر لاگ‌آن (LogOn یا همان SignIn) کرده و یا اینکه یکسری تنظیمات شخصی اعمال کرده است، چون جایی برای ذخیره  و نگهداری این حالات یا وضعیتها در پروتوکل HTTP وجود ندارد. خوشبختانه وجود کوکی‌ها یکی از بهترین راه حل‌ها برای رفع مشکلات اشاره شده است.

بنابراین همانطور که اشاره شده یکی از مهمترین انواع کاربردهای کوکی‌ها در زمینه اعتبار سنجی کاربران است. با استفاده از این نوع کوکی وب سایتها میتوانند از وضعیت ورود یا خروج کاربران و نیز انواع دسترسی‌ها و تنظیمات آنها باخبر شوند. البته با توجه به حساسیت این موضوع، درباره نحوه ذخیره داده‌ها در این نوع کوکی‌ها باید دقت خاصی اعمال شود. اگر در این زمینه سهل انگاری هایی انجام شود، ممکن است خوراک جذابی برای هکرها فراهم شود! تبلیغات درون سایتها نیز از قسمتهایی است که استفاده بسیاری از کوکی میکند که بعضا موجب بروز خطراتی برای کاربران میشود.

تاریخچه
واژه Cookie از عبارت Magic Cookie برگرفته شده است. به طور خلاصه Magic Cookie به مجموعه‌ای از داده‌های «بدون نیاز به تغییر» میگویند که بین برنامه‌هایی که در ارتباط با یکدیگرند، ردوبدل میشود. داده‌های موجود در Magic Cookieها معمولا برای سمت دریافت کننده مفهوم خاصی ندارد و به نوعی برای ذخیره وضعیت «سمت دریافت کننده» در «برنامه ارسال کننده» و استفاده از آن در ارتباطهای بعدی کاربرد دارد. به بیان دیگر حالت یا وضعیت یا تنظیمات «برنامه مقصد در برنامه مبدأ» با استفاده از کوکی در «خود برنامه مقصد» نگهداری میشود!
در سال 1994 آقای Lou Montulli هنگامیکه در شرکت Netscape Communications در توسعه یک برنامه تجاری تحت وب مشارکت داشت، ایده استفاده از این تکنولوژی را در ارتباطات وب ارائه داد که بعدها عنوان HTTP Coockie را بخود گرفت. برای اولین بار از این کوکی‌ها در نسخه 0.9 بتای نت اسکیپ که در همان سال ارائه شد، پشتیبانی و استفاده شد. مروگر IE هم در سال 1995 و در نسخه 2.0 آن، پشتیبانی از کوکی را آغاز کرد. آقای مانتولی پتنت (Patent) تکنولوژی کوکی را در سال 1995 ارائه داد اما ثبت نهایی آن به دلیل مشکلات و مباحث حریم شخصی کاربران تا سال 1998 طول کشید.

کوکی واقعا چیست؟
یک کوکی در واقع یک فایل متنی کوچک است که در قسمتی مشخص از کامپیوتر کلاینت که توسط مرورگر تنظیم شده است، ذخیره میشود. این فایل متنی کوچک حاوی اطلاعات زیر است:
- یک جفت داده نام-مقدار (name-value pair) که داده اصلی کوکی را نگهداری میکند.
- خاصیتی برای مشخص کردن زمان انقضای کوکی (پس از این زمان این فایل متنی کوچک از درون مرورگر حذف خواهد شد)
- خاصیتهایی برای مشخص کردن محدوده‌ها و مسیرهای قابل دسترسی کوکی
- خاصیتهایی برای تعیین نحوه تبادل داده‌های کوکی و نوع دسترسی به این داده‌ها (مثلا الزام به استفاده از پروتوکلهای امن)
 
انواع کوکی
بطور کلی دو نوع اصلی کوکی وجود دارد:
1. Session cookie
از این نوع کوکی برای نگهداری موقت داده‌ها نظیر داده‌های مربوط به وضعیت یک کاربر، تنها در زمان مرور وب سایت استفاده میشود. معمولا با بستن مرورگر (یا اتمام سشن) این کوکی‌ها ازبین میروند.
2. Persistent cookie 
برخلاف کوکی‌های سشنی این نوع کوکی‌ها در سیستم کلاینت به صورت دائمی ذخیره میشوند. معمولا دارای یک تاریخ انقضا هستند که پس از آن از بین میروند. در طول زمان حیات این کوکی ها، مرورگرها داده‌های ذخیره شده در آنها را با توجه به تنظیمات درونشان در هر درخواست به سمت وب سرور سایت مربوطه ارسال میکنند.

با توجه به کاربردهای فراوان کوکی، دسته بندیها و انواع دیگری از کوکی را هم میتوان نام برد. مانند انواع زیر:
Secure cookie 
معمولا به کوکی‌هایی که خاصیت امن (Secure Attribute) در آنها فعال است این عنوان اطلاق میشود. این نوع از کوکی‌ها تنها قابل استفاده در ارتباطهای امن (با استفاده از پروتوکل HTTPS) هستند. این خاصیت اطمینان میدهد که داده‌های موجود هنگام تبادل بین سرور و کلاینت همواره کدگذاری میشود.
HttpOnly cookie
در این کوکی‌ها خاصیت HttpOnly فعال است، که موجب میشود که از آنها تنها در ارتباطات از نوع HTTP و HTTPS بتوان استفاده کرد. در سایر روشهای دسترسی به کوکی (مثلا از طریق برنامه نویسی سمت کلاینت) نمیتوان به محتوای این نوع از کوکی‌ها دسترسی پیدا کرد.
Third-party cookie 
این نوع از کوکی‌ها در مقابل کوکی‌های First party (یا شخص اول) وجود دارند. کوکی‌های شخص اول توسط وب سایت جاری تولید شده اند، یعنی نشانی دامِین این کوکیها مربوط به سایت جاری است. منظور از سایت یا دامین جاری، سایتی است که آدرس آن در نوار آدرس مرورگر نشان داده میشود. کوکی‌های Third party (یا شخص سوم) به آن دسته از کوکی‌ها میگویند که توسط دامین یا وب سایت دیگری غیر از وب سایت جاری تولید و مدیریت میشوند. مثلا کوکی‌های مربوط به سایتهای تبلیغاتی. البته در مرورگرهای مدرن این نوع از کوکی‌ها به دلیل مشکلات امنیتی و نقض حریم شخصی کاربران عموما بلاک میشوند.
Supercookie 
یک سوپرکوکی به آن دسته از کوکی‌ها گفته میشود که خاصیت دامین آنها به یک پسوند خیلی کلی مثل com. تنظیم شده باشد. به دلیل مسائل امنیتی بیشتر مرورگرهای مدرن تمامی انواع این سوپرکوکی‌ها را بلاک میکنند. امروزه لیستی از این پسوندهای کلی با عنوان Public Suffix List موجود است که در مرورگرهای مدرن برای کنترل کوکی‌ها استفاده میشود.
 
موارد استفاده از کوکی
- مدیریت جلسات (Session Management):
از کوکی میتوان برای نگهداری داده‌های مربوط به یک کاربر در بازدید از صفحات سایت و تعامل با آنها (که ممکن است در زمانهای مختلف رخ دهد) استفاده کرد. یکی از موارد بسیار پرکاربرد در این زمینه، کوکی‌های تعیین اعتبار یک کاربر است که پس از ورود به سایت در هر درخواست توسط مرورگر به سمت سرور ارسال میشود. 
مثال دیگری در مورد این کاربرد نگهداری از داده‌های سبد خرید یک کاربر است. این داده‌ها را میتوان قبل از تسویه نهایی درون یک کوکی ذخیره کرد. معمولا در تمام این موارد از یک کلید منحصر به فرد که در سمت سرور تولید شده و درون کوکی به همراه سایر اطلاعات ذخیره میشود، برای تعیین هویت کاربر استفاده میشود. 
- شخصی سازی (Personalization):
یکی دیگر از موارد پرکاربرد کوکیها ذخیره تنظیمات یا داده‌های مرتبط با شخصی سازی تعامل کاربر با سایت است. مثلا میتوان تنظیمات مربوط به استایل یک سایت یا زبان انتخابی برای یک کاربر مشخص را درون کوکی در سمت کلاینت ذخیره کرد. سایتهای بزرگ معمولا از این روش برای ذخیره تنظیمات استفاده میکنند، مثل گوگل و ویکیپدیا. همچنین میتوان از کوکی برای ذخیره شناسه آخرینکاربری که در سایت لاگ‌آن کرده استفاده کرد تا در مراجعه بعدی به عنوان اولین انتخاب در صفحه ورود به سایت به کاربر نمایش داد (هرچند این کار معایب خودش را دارد).
- پیگیری یا ردیابی (Tracking):
از کوکی‌ها میتوان برای پیگیری بازدیدهای یک کاربر در یک کلاینت از یک سایت یا مسیری به خصوص از یک سایت بهره برد. بدین صورت که برای هر کاربر یک کد شناسایی منحصر به فرد تولید شده و درون کوکی مخصوص این کار ذخیره میشود. سپس برای هردرخواست میتوان مثلا نشانی صفحه موردنظر و زمان و تاریخ آن را درون یک منبع ذخیره کرد تا برای استفاده‌های آتی به کار روند.
البته کاربردها و استفاده‌های دیگری نیز برای کوکی میتوان برشمرد که بدلیل طولانی شدن بحث از آنها صرفنظر میشود.
 
ایجاد کوکی
همانطور که در بالا نیز اشاره شد، کوکی‌ها داده هایی هستند که توسط وب سرور برای ذخیره در کلاینت (مرورگر) تولید میشوند. مرورگرها نیز باید این داده‌ها را بدون هیچ تغییری در هر درخواست عینا به سمت سرور برگردانند. درواقع با استفاده از کوکی‌ها میتوان به ارتباط بدون حالت HTTP به نوعی خاصیتی از جنس state اضافه کرد. به غیر از خود وب سرور، برای تنظیم و یا تولید کوکی میتوان از زبان‌های برنامه نویسی سمت کلاینت (مثل جاوا اسکریپت) البته درصورت پشتیبانی توسط مرورگر نیز استفاده کرد.
در جدیدترین استانداردهای موجود (RFC 6265) درباره کوکی آورده شده که مرورگرها باید بتوانند حداقلهای زیر را پشتیبانی کنند:
- توانایی ذخیره حداقل 3000 کوکی
- توانایی ذخیره کوکیها با حجم حداقل 4 کیلوبایت
- توانایی ذخیره و نگهداری حداقل 50 کوکی به ازای هر سایت یا دامین
 
نکته: توانایی مرورگرهای مدرن در مدیریت کوکی‌ها ممکن است فراتر از استانداردهای اشاره شده در بالا باشد.
 
انتقال داده‌های صفحات وب سایتها از طریق پروتوکل HTTP انجام میشود. مرورگر برای بارگذاری صفحه موردنظر کاربر از یک وب سایت، معمولا یک متن کوتاه به وب سرور مربوطه ارسال میکند که به آن HTTP Request میگویند. مثلا برای دریافت صفحه https://www.dntips.ir/index.html درخواستی به شکل زیر به سمت وب سرور ارسال میشود:
GET /index.html HTTP/1.1
Host: www.dotnettips.info 
مثلا نمونه یک درخواست کامل خام (Raw) از صفحه اول سایت جاری در نرم افزار Fiddler بصورت زیر است:
GET https://www.dntips.ir/ HTTP/1.1
Host: www.dotnettips.info
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: BlogPost-1175=NLOpR%2fgHcUGqPL8dZYv3BDDqgd4xOtiiNxHIp1rD%2bAQ%3d; BlogComment-5002=WlS1iaIsiBnQN1UDD4p%2fHFvuoxC3b8ckbw78mAWXZOSWMpxPlLo65%2bA40%2flFVR54; ReaderEmail=DP%2bx4TEtMT2LyhNQ5QqsArka%2fWALP5LYX8Y
وب سرور با ارسال محتویات صفحه موردنظر به این درخواست پاسخ میدهد که به آن HTTP Response میگویند.
در پاسخ ارسالی، وب سرور میتواند با استفاده از یک header مخصوص با نام Set-Cookie یک کوکی را ایجاد کند. در زیر یک نمونه از این پاسخها را مشاهده میکنید:
 HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: cookieName=cookieValue
Set-Cookie: 
cookieName2=cookieValue2; Expires=Thr, 10-Jun-2021 10:18:14 GMT
...
نمونه پاسخ ارسالی خام (Raw) در نرم افزار Fiddler مربوط به درخواست صفحه اول سایت جاری بصورت زیر است:
HTTP/1.1 200 OK
Date: Wed, 30 Jan 2013 20:25:15 GMT
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-Compressed-By: HttpCompress
Set-Cookie: .ASPXROLES=NzZ9qIRpCWHofryYglbsQFv_SSGPn7ivo0zKFoS94gcODVdIKQAe_IBwuc-TQ-03jGeIkZabTuxA0A3k2-nChy7iAWw9rPMYXSkqzMkizRFkDC0k3gQTkdLqLmmeIfnL9UjfMNWO8iVkYQrSv24ecbpFDSQCH827V2kEj8k2oCm_5sKRSmFpifh4N7kinEi0vomG1vW4Rbg9JWMhCgcvndvsFsXxpj-NiEikC1RqHpiLArIyalEMEN-cIuVtRe7uoo938u9l-7OXb8yzXucVl4bdqPy2DXM3ddWzb3OH1jSFM6gxwJ8qRZDlSGmEEbhji7rA-efI4aYGTKx6heWfUsY6E2k73jJLbuZ3RB4oNwRYmz8FRB0-vm1pO7rhF1JIoi1YB17ez-Ox5chNEFkPVREanHVU9DxboJ5dKgN-2B5udUFPunnshbN8EBhixbFQOpqRiiOK4uWWaWy3rVEJYpCCDBRctKCfEyYD1URFYeajB0AXmiMUTcGeuUtwb-XFjbQZnbylmMF3EJgG16bcc1IEkTAUv1JfKjaql0XGWJI1; path=/; HttpOnly
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 106727

<!DOCTYPE html>
<html>
...
وب سرور دستور Set-Cookie را تنها برای ثبت کوکی در مرورگر در پاسخ ارسالی قرار میدهد. برای آشنایی بیشتر با این هدر و ساختار آن به RFC 6265 مراجعه کنید. این دستور برای مرورگر مشخص میکند که (درصورت پشتیبانی از کوکی و فعال بودن آن در مروگر) در درخواستهای بعدی باید این کوکی‌ها را در متن درخواست ارسالی به سمت سرور ضمیمه کند. البته اینکار با توجه به تنظیمات خاصیتهای کوکی مربوطه  انجام میشود که در ادامه بحث میشود. برای ارسال کوکی به سمت وب سرور، مرورگر از هدر Cookie در درخواست خود استفاده میکند.
مثلا با توجه به مثال قبل برای درخواست صفحه https://www.dntips.ir/index2.html مرورگر میتواند متن زیر را به سمت وب سرور ارسال میکند:
 GET /index2.html HTTP/1.1
Host: www.example.org
Cookie: cookieName=cookieValue; cookieName2=cookieValue2
Accept: */*
این درخواست که برای صفحه دیگری از همان سایت قبلی است، با درخواست اول متفاوت است. با استفاده از این درخواست وب سرور میفهمد که این درخواست با درخواست قبلی مرتبط است. وب سرور در پاسخ ارسالی میتواند کوکی‌های دیگری نیز ثبت کند.
وب سرور میتواند مقدار یک کوکی ثبت شده در مروگر را با استفاده از دستوری مشابه Set-Cookie: cookieName=cookieNewValue در پاسخ ارسالی به سمت کلاینت تغییر دهد. مرورگر با دیدن این خط در پاسخ دریافتی از وب سرور مقدار کوکی را به روز رسانی میکند. البته به شرطی که سایر خواص تنظیم شده برای کوکی عینا یکسان باشد.
 
نکته: identity یا هویت کوکی با استفاده از تمام خواص آن به جز expires یا max-age تعیین میشود.
 
مقدار یک کوکی میتواند شامل هر کاراکتر قابل چاپ اَسکی (از کاراکتر ! تا کاراکتر ~ یا از کد یونیکد u0021\ تا u007E\) به غیر از کاراکترهای , و ; و فضای خالی باشد (استفاده از این سه کاراکتر و سایر کاراکترهای غیر اسکی تنها با انکدینگ میسر است). نام یک کوکی نیز از این قاعده پیروی میکند، البته شامل کاراکتر = نیز نمیتواند باشد چون این کاراکتر نقش جداکننده «مقدار» از «نام» کوکی را ایفا میکند. استانداردهای کوکی که در RFC 6265 آورده شده است، محدودیتهای بیشتری نیز دارد که ممکن است توسط مروگرهای امروزی رعایت نشود!
 
نکته: انکدینگ کوکیها کمی بحث دارد. ازآنجاکه کوکی‌ها به صورت هدرهای پروتوکل HTTP انتقال داده میشوند، بنابراین کاراکترهای موجود در آن باید تنها از نوع ASCII باشند. اما چون ارسال کننده و دریافت کننده نهایی کوکی یک وب سرور یکسان است بنابراین وب سرور برای ذخیره کاراکترهای غیر اسکی میتواند از انکدینگ خاص خود استفاده کند. مثلا عموم وب سرورها  و نیز مرورگرها از URL Encoding برای انکدینگ کوکی‌ها استفاده میکنند (^). ظاهرا در تمام مرورگرهای مدرن برای ذخیره کوکی ها، حداقل نام و مقدار کوکی به صورت جداگانه (مثلا برای ذخیره کاراکترهای نامعتبر) انکد میشود به غیر از کاراکتر تساوی (=) بین نام و مقدار کوکی.
 
با استفاده از زبانهای برنامه نویسی سمت کلاینت نیز میتوان کوکی‌ها را مدیریت کرد. مثلا در جاوا اسکریپت از document.cookie برای اینکار استفاده میشود. نحوه کاربرد و استفاده از این پراپرتی کمی غیرعادی است. مثلا دستور 'document.cookie = 'dummy=a11a یک کوکی با نام dummy و با مقدار a11a ایجاد میکند! در ادامه با این دستور و نحوه کارکردن با کوکی در جاوا اسکریپت بیشتر آشنا میشویم.
 
نکته: برای انکد رشته‌ها در جاوا اسکریپت از دستور escape استقاده میشود. عملیات عکس آن با دستور unescape انجام میشود.
 
نکته: با اینکه استاندارد تعریف کوکی مشخص کرده که برای تعریف کوکی وجود عبارتی به صورت name=value اجباری است، اما ظاهرا بیشتر مرورگرها صحت تعریف کوکی و  اعتبار آنرا برای پیروی از این طرح بررسی نمیکنند و بنابراین میتوان صرفا با استفاده از یک رشته بدون علامت مساوی یک کوکی را ایجاد کرد.
 
خواص یک کوکی
به غیر از نام و مقدار، کوکی‌ها خواص دیگری همچون دامین (domain)، مسیر (path)، تاریخ انقضا (expiration date) یا حداکثر طول عمر (maximum age)، و Secure و HttpOnly دارند که میتوانند توسط وب سرور و یا با استفاده از زبانهای برنامه نویسی کلاینتی تنظیم شوند. مرورگرها این خاصیتها را به وب سرور ارسال نمیکنند. تنها مقادیری که به سمت وب سرور برگشت داده میشوند، نام و مقدار کوکی‌هاست. مرورگرها با استفاده از خواص کوکی زمان حذف کوکی و یا کنترل دسترسی به مقدار آن با توجه به آدرس جاری مرورگر و نیز اینکه آیا اصلا کوکی را به وب سرور ارسال کنند یا نه، را تعیین میکنند. خواص کوکی در ادامه شرح داده شده است:
 
1. تاریخ انقضا و حداکثر طول عمر (Expires و Max-Age)
با استفاده از یکی از این دو خاصیت، تاریخی که دیگر نیازی نیست تا کوکی به سمت سرور ارسال شود، تعیین میشود. بنابراین پس از این تاریخ، ممکن است کوکی از مخزن مرورگر پاک شود. برپایه اطلاعات موجود در RFC 6265، در خاصیت Expires، تاریخ انقضای کوکی باید به فرمت “Wdy, DD Mon YYYY HH:MM:SS GMT” باشد. مثل Mon, 17-Mar-2014 1:00:00 GMT. همچنین خاصیت Max-Age طول عمر کوکی را برحسب ثانیه از لحظه دریافت توسط مرورگر مشخص میکند. به نمونه‌های زیر توجه کنید:
Set-Cookie: cookie1=abc; Expires=Mon, 17-Mar-2014 01:00:00 GMT; ...
Set-Cookie: cookie2=123; ...
Set-Cookie: cookie3=abc; Expires=Thu, 01-Jan-1970 00:00:01 GMT; ...
Set-Cookie: cookie3=abc; max-age=31536000; ... 
 ...... 
در دستور اول، cookie1 برای حذف در تاریخ مشخص شده تنظیم شده است. در خط دوم که بدون این دو خاصیت است، یک نوع کوکی سشنی تعریف شده است. این کوکی پس از بسته شدن مرورگر (اتمام سشن) از حافظه پاک خواهد شد.

نکته: اتمام یک سشن برای کوکی‌های سشنی دقیقا به معنی بستن مرورگر (یا تب مربوطه در مروگرهای مدرن) است.

دستور سوم که تاریخ انقضای کوکی را به تاریخی در گذشته تنظیم کرده است به مرورگر اعلام میکند که باید cookie3 را پاک کند. این روش استاندارد برای حذف یک کوکی است.

نکته: استفاده از روش تنظیم یک تاریخ انقضا در گذشته برای حذف یک کوکی تنها وقتی کار خواهد که سایر خواص تعیین شده در دستور Set-Cookie با مقادیر موجود در حافظه مرورگر دقیقا یکی باشد تا هویت کوکی موردنظر به صورت منحصربه فرد تعیین شود. 

در خط چهارم به مرورگر اعلام میشود که cookie4 باید دقیقا یک سال پس از لحظه دریافت کوکی، حذف شود. 

نکته: خاصیت max-age در مرورگر IE8 و نسخه‌های قبل از آن پشتیبانی نمیشود. 

نکته: گزینه ای که معمولا در صفحات لاگ‌آن (LogOn) یا ساین‌این (SignIn) برای ذخیره داده‌های کاربر وجود دارد (مثل «مرا به خاطر بسپار»)، مرتبط با این خاصیت از کوکی هاست. در صورت عدم انتخاب این گزینه معمولا یک کوکی سشنی (بدون خاصیت expires) ایجاد میشود. اما با انتخاب این گزینه، یک کوکی ماندگار (Persistent) با خاصیت expires برابر با تاریخی در آینده ایجاد میشود تا درصورت بسته شدن مرورگر (اتمام سشن) داده‌های کاربر پاک نشود. 
 
نکته: تاریخ انقضای کوکی با استفاده از تاریخ کلاینت تعیین میشود. متاسفانه هیچ راه مستقیمی برای همزمانی این تاریخ با تاریخ سرور وجود ندارد. 
 
2. دامین و مسیر (Domain و Path)
خاصیتهای دامین و مسیر کوکی، محدوده قابل دسترس کوکی را مشخص میکنند. با استفاده از این دو خاصیت مرورگر متوجه میشود که آیا کوکی را در موقعیت و آدرس جاری باید به سمت وب سرور ارسال کند یا خیر. همچنین دسترسی به کوکی‌ها در سمت کلاینت با توجه به این دو خاصیت محدود میشود. مقدار پیش فرض این دو خاصیت برابر مسیر و دامین جاری مرورگر است که اگر مقداری برای این دو خاصیت تعیین نشود، به کوکی تعلق میگیرد.
 
نکته: منظور از وضعیت جاری، موقعیتی است که کوکی مذبور در آن ایجاد شده است. مثلا آدرس صفحه ای که هدر Set-Cookie را ارسال کرده و یا آدرس صفحه ای که در آن با استفاده از دستوری مشابه ;'document.cookie = 'a=b کوکی مربوطه ایجاد شده است. به عنوان نمونه اگر یک کوکی در صفحه جاری همین سایت ایجاد شود و خاصیتهای دامین و مسیر آن مقداردهی نشود، مقدار دامین به www.dotnettips.info و مقدار مسیر آن به post/ تنظیم خواهد شد.

نکته: مرورگر بررسی دامین کوکی را ازطریق «مقایسه از انتها» انجام میدهد. یعنی اگر مثلا دامین یک کوکی برابر dotnettips.info باشد، این کوکی در ساب دامین‌های d1.dotettips.info و یا www.dotnettips.info و از این قبیل در دسترس خواهد بود. برای کسب اطلاعات بیشتر میتوان به RFC 6265 (قسمت Domain Matching) مراجعه کرد.
 
نکته: بررسی مسیر کوکی برخلاف دامین آن، ازطریق «مقایسه از ابتدا» انجام میشود. یعنی آدرس صفحه جاری پس از مقدار دامین سایت باید با مقدار مشخص شده در خاصیت مسیر شروع شود. مثلا مسیر یک کوکی برابر post/ و دامین آن نیز برابر www.dotnettips.info باشد، این کوکی در آدرسهایی چون www.dotnettips.info/post/1286 و یا www.dotnettips.info/post/1 و یا www.dotnettips.info/post/test/test2 و ... در دسترس خواهد بود. برای کسب اطلاعات بیشتر میتوان به RFC 6265 (قسمت Paths and Path-Match) مراجعه کرد. 

برای روشنتر شدن مطلب به هدرهای Set-Cookie زیر توجه کنید:
Set-Cookie: MyCookie1=hi; Domain=d1.d2.com; Path=/employee ...
Set-Cookie: MyCookie2=bye; Domain=.d3.com; Path=/ ...
Set-Cookie: MyCookie3=nth ... 
 ...... 
اولین دستور به مرورگر میگوید تا یک کوکی با نام MyCookie1 و مقدار hi را با دامین d1.d2.com و مسیر employee/ ثبت کند. بنابراین مرورگر از این کوکی تنها درصورتیکه آدرس درخواست موردنظر شامل d1.d2.com/employee باشد، استفاده میکند.
دستور دوم به مرورگر میگوید تا از کوکی MyCookie2 در مسیرهای شامل d3.com. استفاده کند.
در دستور سوم دامین و مسیر با توجه به آدرس صفحه جاری تنظیم میشود. در واقع تنها مسیرهایی که شامل آدرس صفحه جاری باشند به این کوکی دسترسی دارند.

نکته: مقدار domain تنها میتواند مربوط به دامین اصلی جاری و یا زیرمجموعه‌های آن باشد. یعنی نمیتوان یک کوکی با دامین www.d1.com در صفحه‌ای با آدرس www.d2.com ایجاد کرد.

نکته: همچنین کوکی هایی که مثلا دارای دامین www.dotnettips.info هستند از آدرسی نظیر my.dotnettips.info در دسترس نیستند. کوکی‌ها تنها در دامین و ساب دامین‌های مربوط به خود قابل خواندن هستند.

نکته: اگر مقدار خاصیت domain کوکی به چیزی شبیه dotnettip.info تنظیم شود آنگاه این کوکی در آدرسهایی چون www.dotnettips.info و یا d1.dotnettips.info نیز در دسترس است.

نکته: اگر برای خاصیت path مقدار / تنظیم شود، بدین معنی است که کوکی در تمام محدوده دامین کوکی در دسترس است. 

3. Secure و HttpOnly
این دوخاصیت برخلاف خواص قبلی مقداری را تنظیم نمیکنند! بلکه همانند یک flag عمل کرده که هر کدام رفتار خاصی را برای مرورگر الزام میکنند.
خاصیت Secure مرورگر را مجبور به استفاده از ارتباطات امن و کدگذاری شده (Https) برای تبادل داده‌های کوکی میکند. درضمن طبیعی است که وب سرور دستور ثبت چنین کوکی‌هایی را خود از طریق یک ارتباط امن به مرورگر ارسال کند تا مبادا طی یک فرایند خرابکارانه داده‌های مهم درون کوکی در بین راه دزدیده نشود. 

نکته: یک کوکی Secure تنها درصورتی به سمت سرور ارسال میشود که درخواست مذکور با استفاده از SSL و ازطریق پروتوکل HTTPS ایجاد شده باشد.

خاصیت HttpOnly به مرورگر اعلام میکند که استفاده از این کوکی تنها در ارتباطات از نوع پروتوکل HTTP مجاز است. بنابراین سایر روشهای دسترسی موجود (مثل document.cookie در جاوا اسکریپت) برای این نوع کوکی‌ها کار نخواهد کرد. درواقع نحوه برخورد با این نوع کوکی‌ها در سمت سرور با سایر انواع کوکی تفاوتی ندارد و تنها در سمت کلاینت و در مرورگر است که رفتاری متفاوت متوجه این کوکی‌ها میشود و اجازه دسترسی به برنامه‌های سمت کلاینت داده نمیشود.

نکته: این خاصیت ابتدا توسط مایکروسافت در نسخه IE 6 SP1 معرفی شد و بعدها بتدریج توسط سایر مرورگرها نیز پشتیبانی شد. این ویژگی همانطور که از آن برمی‌آید برای مقابله با حملات XSS پیاده سازی شده است. البته علاوه برای جلوگیری از دسترسی به این کوکی‌ها از طریق document.cookie، در مرورگرهای مدرن از دسترسی به هدر این کوکی‌ها ازطریق متدهای شی XMLHttpRequest نیز جلوگیری میشود.

نکته: امکان تنظیم این خاصیت از طریق document.cookie در جاوا اسکریپت وجود ندارد!
 
مدیریت کوکی‌ها در مرورگر
همانطور که قبلا اشاره شد هویت یک کوکی با استفاده تمامی خواص آن به جز expires یا max-age مشخص میشود. یعنی ترکیب name-domain-path-secure/httponly هویت یک کوکی را منحصربه‌فرد میکند. بنابراین تا زمانیکه حتی یکی از این خواص دو کوکی با هم فرق داشته باشد این دو کوکی از هم متمایز خواهند بود. دو کوکی زیر را درنظر بگیرید:
Set-Cookie: cookie1=value1
Set-Cookie: cookie1=value2; domain=dotnettips.info;
 با اینکه به نظر میرسد که این دو کوکی یکسان هستند و اجرای دستور دوم موجب بازنویسی کوکی اول میشود، اما بررسی یک درخواست ارسالی از این صفحه نشان میدهد که دو کوکی مجزا با نام مشابه به سمت سرور ارسال میشود:
Cookie: cookie1=value1; cookie1=value2 
حال اگر کوکی سومی به صورت زیر تعریف شود:
Set-Cookie: cookie1=value3; domain=dotnettips.info; path=/post
وضعیت از این نیز پیچیده‌تر میشود:
Cookie: cookie1=value1; cookie1=value2; cookie1=value3
بنابراین اجرای دستور زیر در همان صفحه:
Set-Cookie: cookie1=value4
تنها مقدار کوکی اول را تغییر خواهد داد. یعنی در درخواست ارسالی به سمت سرور خواهیم داشت:
Cookie: cookie1=value4; cookie1=value2; cookie1=value3
بنابراین دقت مضاعف به این نکته که «هویت یک کوکی با استفاده از تمامی خواص آن به جز expires یا max-age تعیین میشود» مهم است. برای قسمت «به جز expire یا max-age» هم به مثال زیر توجه کنید:
Set-Cookie: cookie2=value1; max-age=1000
بنابراین خواهیم داشت:
Cookie: cookie1=value1; cookie1=value2; cookie1=value4; cookie2=value1
یک کوکی با طول عمر 1000 ثانیه تولید میکند. بنابراین با دستور زیر میتوان مقدار همین کوکی را تغییر داد:
Set-Cookie: cookie2=value2
پس داریم:
Cookie: cookie1=value4; cookie1=value2; cookie1=value3; cookie2=value2
هرچند در دستور آخر به نظر میرسد که کوکی آخر به نوع سشنی تغییر یافته است (چون خاصیت expires یا max-age ندارد) اما درواقع این چنین نیست. تنها اتفاقی که رخ داده است این است که مقدار کوکی مذبور تغییر یافته است، درحالیکه تغییری در خاصیت expires یا max-age آن رخ نداده است.

نکته: با تغییر خواص یک کوکی، میتوان آنرا از نوع سشنی به نوع ماندگار (Persistent) تغییر داد، اما عکس این عملیات ممکن نیست.

SubCookie
بدلیل محدودیت موجود در تعداد کوکی‌ها به ازای هر دامین، روشی برای نگهداری تعداد بیشتری تنظیمات درون همین تعداد محدود کوکی‌ها توسط توسعه گران ابداع شده است. در این روش از طرح ساده ای که نمونه ای از آن در زیر نشان داده شده است برای نگهداری داده‌های چندین کوکی درون یک کوکی استفاده میشود:
Set-Cookie: cookieName=cookie1=value1&cookie2=value2&cookie3=value3&cookie4=value4; path=/
در نمونه بالا با اینکه عملا تنها یک کوکی تعریف شده است اما درواقع داده‌های 4 کوکی مختلف درون یک کوکی آورده شده است. تنها عیب این روش این است که زحمت بیشتری برای استخراج داده‌های کوکی‌ها باید کشید. البته امروزه برخی از فریمورکها امکاناتی جهت کار با این کوکی‌ها فراهم کرده اند.
 
Cookie2
در ابتدای هزاه سوم! مدتی بحثی مطرح شد برای بهبود کارایی و امنیت کوکی‌ها و پیشنهادهایی مبنی بر پیاده سازی نوع جدیدی از کوکی‌ها با عنوان Cookie2 نیز ارائه شد. حتی در نسخه جدیدی از استانداردهای HTTP State Management Mechanism که در RFC 2965 آورده شده است کلا به این نوع جدید از کوکی‌ها پرداخته شده است. هرچند برخی از مرورگرها پشتیبانی از این نوع جدید را آغاز کردند (مثل Opera) اما بعدها به دلیل عدم استقبال از آن، این نوع از کوکی‌ها منسوخ شد و حالت آن در نسخه‌های جدید استانداردها به Obsolete تغییر یافت (RFC 6265)! درهرصورت برای آشنایی با این نوع کوکی‌ها میتوان به مراجع زیر رجوع کرد:

 
منابع: