مطالب
Repository ها روی UnitOfWork ایده خوبی نیستند
در دنیای دات نت گرایشی برای تجزیه (abstract) کردن EF پشت الگوی Repository وجود دارد. این تمایل اساسا بد است و در ادامه سعی می‌کنم چرای آن را توضیح دهم.


پایه و اساس

عموما این باور وجود دارد که با استفاده از الگوی Repository می‌توانید (در مجموع) دسترسی به داده‌ها را از لایه دامنه (Domain) تفکیک کنید و "داده‌ها را بصورت سازگار و استوار عرضه کنید".

اگر به هر کدام از پیاده سازی‌های الگوی Repository در کنار (UnitOfWork (EF دقت کنید خواهید دید که تفکیک (decoupling) قابل ملاحظه ای وجود ندارد.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class StudentRepository : IStudentRepository, IDisposable
    {
        private SchoolContext context;

        public StudentRepository(SchoolContext context)
        {
            this.context = context;
        }

        public IEnumerable<Student> GetStudents()
        {
            return context.Students.ToList();
        }

        public Student GetStudentByID(int id)
        {
            return context.Students.Find(id);
        }

        //<snip>
        public void Save()
        {
            context.SaveChanges();
        }
    }
}

این کلاس بدون SchoolContext نمی‌تواند وجود داشته باشد، پس دقیقا چه چیزی را در اینجا decouple کردیم؟ هیچ چیز را!

در این قطعه کد - از MSDN - چیزی که داریم یک پیاده سازی مجدد از LINQ است که مشکل کلاسیک Repository API‌های بی انتها را بدست می‌دهد. منظور از Repository API‌های بی انتها، متدهای جالبی مانند GetStudentById, GetStudentByBirthday, GetStudentByOrderNumber و غیره است.

اما این مشکل اساسی نیست. مشکل اصلی روتین ()Save است. این متد یک دانش آموز (Student) را ذخیره می‌کند .. اینطور بنظر می‌رسد. دیگر چه چیزی را ذخیره می‌کند؟ آیا می‌توانید حدس بزنید؟ من که نمی‌توانم .. بیشتر در ادامه.


UnitOfWork تراکنشی است

یک UnitOfWork همانطور که از نامش بر می‌آید برای انجام کاری وجود دارد. این کار می‌تواند به سادگی واکشی اطلاعات و نمایش آنها، و یا به پیچیدگی پردازش یک سفارش جدید باشد. هنگامی که شما از EntityFramework استفاده می‌کنید و یک DbContext را وهله سازی می‌کنید، در واقع یک UnitOfWork می‌سازید.

در EF می‌توانید با فراخوانی ()SubmitChanges تمام تغییرات را فلاش کرده و بازنشانی کنید (flush and reset). این کار بیت‌های مقایسه change tracker را تغییر می‌دهد. افزودن رکوردهای جدید، بروز رسانی و حذف آنها. هر چیزی که تعیین کرده باشید. و تمام این دستورات در یک تراکنش یا Transaction انجام می‌شوند.


یک Repository مطلقا یک UnitOfWork نیست
هر متد در یک Repository قرار است فرمانی اتمی (Atomic) باشد - چه واکشی اطلاعات و چه ذخیره آنها. مثلا می‌توانید یک Repository داشته باشید با نام SalesRepository که اطلاعات کاتالوگ شما را واکشی می‌کند، و یا یک سفارش جدید را ثبت می‌کند. منظور از فرمان‌های اتمیک این است، که هر متد تنها یک دستور را باید اجرا کند. تراکنشی وجود ندارد و امکاناتی مانند ردیابی تغییرات و غیره هم جایی ندارند.

یکی دیگر از مشکلات استفاده از Repository‌ها این است که بزودی و به آسانی از کنترل خارج می‌شوند و نیاز به ارجاع دیگر مخازن پیدا می‌کنند. به دلیل اینکه مثلا نمی‌دانستید که SalesRepository نیاز به ارجاع ReportRepository داشته است (یا چیزی مانند این).

این مشکل به سرعت مشکل ساز می‌شود، و نیز به همین دلیل است که به UnitOfWork تمایل پیدا می‌کنیم.


بدترین کاری که می‌توانید انجام دهید: <Repository<T

این الگو دیوانه وار است. این کار عملا انتزاعی از یک انتزاع دیگر است (abstraction of an abstraction). به قطعه کد زیر دقت کنید، که به دلیلی نامشخص بسیار هم محبوب است.

public class CustomerRepository : Repository < Customer > {
  public CustomerRepository(DbContext context){
    //a property on the base class
    this.DB = context;
  }

  //base class has Add/Save/Remove/Get/Fetch
}

در نگاه اول شاید بگویید مشکل این کلاس چیست؟ همه چیز را کپسوله می‌کند و کلاس پایه Repository هم به کانتکست دسترسی دارد. پس مشکل کجاست؟

مشکلات عدیده اند .. بگذارید نگاهی بیاندازیم.

آیا می‌دانید این DbContext از کجا آمده است؟
خیر، نمی‌دانید. این آبجکت به کلاس تزریق (Inject) می‌شود، و نمی‌دانید که چه متدی آن را باز کرده و به چه دلیلی. ایده اصلی پشت الگوی Repository استفاده مجدد از کد است. بدین منظور که مثلا برای عملیات CRUD از کلاسی پایه استفاده کنید تا برای هر موجودیت و فرمی نیاز به کدنویسی مجدد نباشد. برگ برنده این الگو نیز دقیقا همین است. مثلا اگر بخواهید از کدی در چند فرم مختلف استفاده کنید از این الگو استفاده میشد.

الگوی UnitOfWork همه چیز در نامش مشخص است. اگر قرار باشد آنرا بدین شکل تزریق کنید، نمی‌توانید بدانید که از کجا آمده است.


شناسه مشتری جدید را نیاز داشتم
کد بالا در CustomerRepository را در نظر بگیرید - که یک مشتری جدید را به دیتابیس اضافه می‌کند. اما CustomerID جدید چه می‌شود؟ مثلا به این شناسه نیاز دارید تا یک log بسازید. چه می‌کنید؟ گزینه‌های شما اینها هستند:

  • متد ()SubmitChanges را صدا بزنید تا تغییرات ثبت شوند و بتوانید به CustomerID جدید دسترسی پیدا کنید
  • CustomerRepository خود را باز کنید و متد پایه Add را بازنویسی (override) کنید. بدین منظور که پیش از بازگشت دادن، متد ()SubmitChanges را فراخوانی کند. این راه حلی است که MSDN به آن تشویق می‌کند، و بمبی ساعتی است که در انتظار انفجار است
  • تصمیم بگیرید که تمام متدهای Add/Remove/Save در مخازن شما باید ()SubmitChanges را فراخوانی کنند

مشکل را می‌بینید؟ مشکل در خود پیاده سازی است. در نظر بگیرید که چرا New Customer ID را نیاز دارید؟ احتمالا برای استفاده از آن در ثبت یک سفارش جدید، و یا ثبت یک ActivityLog.

اگر بخواهیم از StudentRepository بالا برای ایجاد دانش آموزان جدید پس از خرید آنها از فروشگاه کتاب مان استفاده کنیم چه؟ اگر DbContext خود را به مخزن تزریق کنید و دانش آموز جدید را ذخیره کنید .. اوه .. تمام تراکنش شما فلاش شده و از بین رفته!

حالا گزینه‌های شما اینها هستند: 1) از StudentRepository استفاده نکنید (از OrderRepository یا چیز دیگری استفاده کنید). و یا 2) فراخوانی ()SubmitChanges را حذف کنید و به باگ‌های متعددی اجازه ورود به کد تان را بدهید.

اگر تصمیم بگیرید که از StudentRepository استفاده نکنید، حالا کدهای تکراری (duplicate) خواهید داشت.

شاید بگویید که برای دستیابی به شناسه رکورد جدید نیازی به ()SubmitChanges نیست، چرا که خود EF این عملیات را در قالب یک تراکنش انجام می‌دهد!

دقیقا درست است، و نکته من نیز همین است. در ادامه به این قسمت باز خواهیم گشت.

متدهای Repositories قرار است اتمیک باشند

به هر حال تئوری اش که چنین است. چیزی که در Repository‌ها داریم حتی اصلا Repository هم نیست. بلکه یک abstraction برای عملیات CRUD است که هیچ کاری مربوط به منطق تجاری اپلیکیشن را هم انجام نمی‌دهد. مخازن قرار است روی دستورات مشخصی تمرکز کنند (مثلا ثبت یک رکورد یا واکشی لیستی از اطلاعات)، اما این مثال‌ها چنین نیستند.

همانطور که گفته شده استفاده از چنین رویکردهایی به سرعت مشکل ساز می‌شوند و با رشد اپلیکیشن شما نیز مشکلات عدیده ای برایتان بوجود می‌آروند.

خوب، راه حل چیست؟

برای جلوگیری از این abstraction‌های غیر منطقی دو راه وجود دارد. اولین راه استفاده از Command/Query Separation است که ممکن است در ابتدا کمی عجیب و بنظر برسند اما لازم نیست کاملا CQRS را دنبال کنید. تنها از سادگی انجام کاری که مورد نیاز است لذت ببرید، و نه بیشتر.

آبجکت‌های Command/Query

Jimmy Bogard مطلب خوبی در اینباره نوشته است و با تغییراتی جزئی برای بکارگیری Properties کدی مانند لیست زیر خواهیم داشت. مثلا برای مطالعه بیشتر درباره آبجکت‌های Command/Query به این لینک سری بزنید.

public class TransactOrderCommand {
  public Customer NewCustomer {get;set;}
  public Customer ExistingCustomer {get;set;}
  public List<Product> Cart {get;set;}
  //all the parameters we need, as properties...
  //...

  //our UnitOfWork
  StoreContext _context;
  public TransactOrderCommand(StoreContext context){
    //allow it to be injected - though that's only for testing
    _context = context;
  }

  public Order Execute(){
    //allow for mocking and passing in... otherwise new it up
    _context = _context ?? new StoreContext();

    //add products to a new order, assign the customer, etc
    //then...
    _context.SubmitChanges();

    return newOrder;
  }
}
همین کار را با یک آبجکت Query نیز می‌توانید انجام دهید. می‌توانید پست Jimmy را بیشتر مطالعه کنید، اما ایده اصلی این است که آبجکت‌های Query و Command برای دلیل مشخصی وجود دارند. می‌توانید آبجکت‌ها را در صورت نیاز تغییر دهید و یا mock کنید.


DataContext خود را در آغوش بگیرید

ایده ای که در ادامه خواهید دید را شخصا بسیار می‌پسندم (که توسط Ayende معرفی شد). چیزهایی که به آنها نیاز دارید را در قالب یک فیلتر wrap کنید و یا از یک کلاس کنترلر پایه استفاده کنید (با این فرض که از اپلیکیشن‌های وب استفاده می‌کنید).

using System;
using System.Web.Mvc;

namespace Web.Controllers
{
  public class DataController : Controller
  {
    protected StoreContext _context;

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      //make sure your DB context is globally accessible
      MyApp.StoreDB = new StoreDB();
    }

    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      MyApp.StoreDB.SubmitChanges();
    }
  }
}

این کار به شما اجازه می‌دهد که از DataContext خود در خلال یک درخواست واحد (request) استفاده کنید. تنها کاری که باید بکنید این است که از این کلاس پایه ارث بری کنید. این بدین معنا است که هر درخواست به اپلیکیشن شما یک UnitOfWork خواهد بود. که بسیار هم منطقی و قابل قبول است. در برخی موارد هم شاید این فرض درست یا کارآمد نباشد، که در این هنگام می‌توانید از آبجکت‌های Command/Query استفاده کنید.


ایده‌های بعدی: چه چیزی بدست آوردیم؟

چیزهای متعددی بدست آوردیم.

  • تراکنش‌های روشن و صریح: دقیقا می‌دانیم که DbContext ما از کجا آمده و در هر مرحله روی چه UnitOfWork ای کار می‌کنیم. این امر هم الان، و هم در آینده بسیار مفید خواهد بود
  • انتزاع کمتر == شفافیت بیشتر: ما Repository‌ها را از دست دادیم، که دلیلی برای وجود داشتن نداشتند. به جز اینکه یک abstraction از abstraction دیگر باشند. رویکرد آبجکت‌های Command/Query تمیز‌تر است و دلیل وجود هرکدام و مسئولیت آنها نیز روشن‌تر است
  • شانس کمتر برای باگ ها: رویکردهای مبتنی بر Repository باعث می‌شوند که با تراکنش‌های ناموفق یا پاره ای (partially-executed) مواجه شویم که نهایتا به یکپارچگی و صحت داده‌ها صدمه می‌زند. لازم به ذکر نیست که خطایابی و رفع چنین مشکلاتی شدیدا زمان بر و دردسر ساز است

برای مطالعه بیشتر 

ایجاد Repositories بر روی UnitOfWork
به الگوی Repository در لایه DAL خود نه بگویید!
پیاده سازی generic repository یک ضد الگو است 
نگاهی به generic repositories
بدون معکوس سازی وابستگی‌ها، طراحی چند لایه شما ایراد دارد  

مطالب
روشی برای محاسبه‌ی تعداد کاربران آنلاین در ASP.NET Core
در این مقاله قصد دارم روشی را برای محاسبه‌ی تعداد کاربران لاگین شده‌ی فعال در یک پروژه‌ی  Asp.net Core، توضیح دهم. در این روش، کاربرانی را آنلاین در نظر گرفته‌ایم که در 10 دقیقه‌ی گذشته، فعالیتی روی سامانه داشته‌اند. البته این زمان را می‌توانید تغییر دهید. برای اینکار ابتدا یک Middleware را به صورت زیر طراحی می‌کنیم :
public class OnlineUserMiddleWare
    {
        private readonly RequestDelegate _next;
        private readonly IMemoryCache _memoryCache;

        public OnlineUserMiddleWare(RequestDelegate next, IMemoryCache memoryCache)
        {
            _next = next;
            _memoryCache = memoryCache;
        }

        public async Task Invoke(HttpContext context)
        {
            if (!_memoryCache.TryGetValue("OnlineUsers", out Dictionary<string,DateTime> onlineUsers))
            {
                onlineUsers = new Dictionary<string, DateTime>();
                _memoryCache.Set("OnlineUsers", onlineUsers);
            }
            if (context.User.Identity.IsAuthenticated)
            {
                var name = context.User.Identity.Name;
                if (name != null)
                {
                    if (onlineUsers.ContainsKey(name))
                        onlineUsers[name] = DateTime.Now;
                    else
                        onlineUsers.Add(name, DateTime.Now);
                }

            }

           
                foreach (var online in onlineUsers)
                {
                    if (online.Value < DateTime.Now.AddMinutes(-10))
                        onlineUsers.Remove(online.Key);
                }
           
            await _next(context);
        }

        
    }
در اینجا به کمک IMemoryCache، یک کلید را به نام OnlineUsers در کش ایجاد کرده‌ایم. ابتدا بررسی می‌کنیم، اگر این کلید وجود نداشت، آن را ایجاد کند. نوع این کلید، یک  Dictionary است که نام کاربری و ساعت آخرین در خواست کاربران، در آن ذخیره می‌شود.
پس از اینکه مقدار OnlineUsers را از کش دریافت کردیم، چک می‌کنیم اگر کاربر لاگین شده‌است، نام کاربری را به همراه زمان جاری، در دیکشنری ذخیره کند و اگر کاربر درون دیکشنری موجود بود، فقط زمان آخرین درخواست را تغییر دهد (به‌روز کند). بنابراین تا به اینجا هر کسی در سامانه لاگین کند، نام کاربری او، به همراه زمان آخرین درخواست، در دیکشنری ذخیره می‌شود.
نهایتا باید بررسی کنیم، اگر کاربری 10 دقیقه از آخرین درخواستش گذشته باشد، از دیکشنری حذف شود. 
لازم است این Middleware به Startup اضافه شود: 
 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
        {
         ...
            app.UseMiddleware<OnlineUserMiddleWare>();
         ...
        }

برای نمایش تعداد کاربران آنلاین، کافی است در View یا مکان مورد نظر، مقدار دیکشنری را از memorycache فراخوانی کنیم: (حتی می‌توان لیست کاربران را هم نمایش داد؛ چون نام کاربری آنها در دیکشنری ذخیره شده‌است)
public class DashboardController 
    {
        private readonly IMemoryCache _memoryCache;
        public DashboardController(memoryCache)
        {
            _memoryCache = memoryCache;
        }
        public IActionResult Index()
        {
         
            _memoryCache.TryGetValue("OnlineUsers", out Dictionary<string, DateTime> onlineUsers);
            ViewBag.OnlineUsers = onlineUsers.Count;
            return View();
        }       
    }
اما یک نکته وجود دارد و اینکه در MiddleWare به ازای هر درخواست، کل دیکشنری را بررسی کرده‌ایم و کاربرانی را که 10 دقیقه از آخرین  فعالیت آنها گذشته است، حذف کرده‌ایم. این بررسی به ازای هر درخواست، کار مناسبی نیست؛ چرا که ممکن است در ترافیک بالا، مشکل ساز شود. لذا می‌توان سناریوهای مختلفی را برای آن در نظر گرفت:
 - مثلا فقط جائیکه می‌خواهیم آمار نمایش داده شود، این بررسی صورت گیرد. البته اگر این آمار در صفحه‌ای پرتکرار نمایش داده شود، این سناریو می‌تواند مفید باشد. ولی مثلا اگر قرار است آمار تنها برای مدیر نمایش داده شود و مدیر چندین روز این صفحه را بررسی نکند، ممکن است حجم این دیکشنری هر روز حجیم‌تر شود.
- می‌توان در صفحاتی که اطمینان داریم روزانه چندین بار فراخوانی می‌شوند، این فرآیند را بررسی کنیم؛ مثل صفحه لاگین . لذا قسمت بررسی در MiddleWare را به صورت زیر تغییر دادم :
           if(context.Request.Path.StartsWithSegments("/Identity/Account/Login"))
            {
                foreach (var online in onlineUsers)
                {
                    if (online.Value < DateTime.Now.AddMinutes(-10))
                        onlineUsers.Remove(online.Key);
                }
            }

مطالب
Blazor 5x - قسمت چهارم - مبانی Blazor - بخش 1 - Data Binding
عنوان می‌شود که HTML over Web socket آینده‌ی توسعه‌ی برنامه‌های وب است و این آینده هم اکنون توسط Blazor Server در دسترس است. در این مدل توسعه، ابتدا یک اتصال SignalR برقرار شده و سپس تمام تعاملات بین سرور و کلاینت، از طریق همین اتصال که عموما web socket است، مدیریت می‌شود. به همین جهت در ادامه قصد داریم یک پروژه‌ی Blazor Server را تکمیل کنیم. پس از آن یک پروژه‌ی Blazor WASM را نیز بررسی خواهیم کرد. بنابراین هر دو مدل توسعه‌ی برنامه‌های Blazor را پوشش خواهیم داد. برای این منظور در ابتدا مبانی Blazor را بررسی می‌کنیم که در هر دو مدل یکی است.


تعریف مدل برنامه

در همان پروژه‌ی خالی Blazor Server که در قسمت دوم با دستور dotnet new blazorserver ایجاد کردیم، پوشه‌ی Models را افزوده و کلاس BlazorRoom را در آن تعریف می‌کنیم:
namespace BlazorServerSample.Models
{
    public class BlazorRoom
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public decimal Price { set; get; }

        public bool IsActive { set; get; }
    }
}
سپس برای اینکه مدام نیاز به تعریف فضای نام آن در فایل‌های مختلف razor. برنامه نباشد، به فایل Imports.razor_ مراجعه کرده و سطر زیر را به انتهای آن اضافه می‌کنیم:
@using BlazorServerSample.Models
برنامه را نیز توسط دستور dotnet watch run اجرا می‌کنیم.


Data binding یک طرفه

در ادامه به فایل Pages\Index.razor مراجعه کرده و منهای سطر اول مسیریابی آن، مابقی محتوای آن‌را حذف می‌کنیم. در اینجا می‌خواهیم مقادیر نمونه‌ای از شیء BlazorRoom را نمایش دهیم. به همین جهت این شیء را در قسمت code@ فایل razor جاری (همانند نکات قسمت قبل)، ایجاد می‌کنیم:
@page "/"

<h2 class="bg-light border p-2">
    First Room
</h2>
Room: @Room.Name
<br/>
Price: @Room.Price

@code
{
    BlazorRoom Room = new BlazorRoom
    {
        Id = 1,
        Name = "Room 1",
        IsActive = true,
        Price = 499
    };
}
در اینجا در ابتدا شیء Room را در قسمت قطعه کد فایل razor جاری ایجاد کرده و سپس اطلاعات آن‌را با استفاده از زبان Razor نمایش داده‌ایم.


 به این روش نمایش اطلاعات، one-way data-binding نیز گفته می‌شود. اما چطور می‌توان یک طرفه بودن آن‌را متوجه شد؟ برای این منظور یک text-box را نیز در ذیل تعاریف فوق، به صورت زیر اضافه می‌کنیم که مقدارش را از Room.Price دریافت می‌کند:
<input type="number" value="@Room.Price" />
اکنون اگر این مقدار را تغییر دهیم، عدد جدید قیمت اتاق، به خاصیت Room.Price منعکس نمی‌شود و تغییری نمی‌کند:



Data binding دو طرفه

اکنون می‌خواهیم اگر مقدار ورودی Room.Price توسط text-box فوق تغییر کرد، نتیجه‌ی نهایی، به خاصیت متناظر با آن نیز اعمال شود و تغییر کند. برای این منظور فقط کافی است ویژگی value را به bind-value@ تغییر دهیم:
<input type="number" @bind-value="@Room.Price" />
ویژگی bind-value@ سبب برقراری data-binding دو طرفه می‌شود. یعنی در ابتدا مقدار اولیه‌ی خاصیت Room.Price را نمایش می‌دهد. در ادامه‌ی اگر کاربر، مقدار این text-box را تغییر داد، نتیجه‌ی نهایی را به خاصیت Room.Price نیز اعمال می‌کند و همچنین این تغییر، سبب به روز رسانی UI نیز می‌شود؛ یعنی در جائیکه پیشتر مقدار اولیه‌ی Room.Price را نمایش داده بودیم، اکنون مقدار جدید آن نمایش داده خواهد شد:


البته اگر برنامه را اجرا کنیم، با تغییر مقدار text-box، بلافاصله تغییری را مشاهده نخواهیم کرد. برای اعمال تغییرات نیاز خواهد بود تا در جائی خارج از text-box کلیک و focus را به المانی دیگر منتقل کنیم. اگر می‌خواهیم همراه با تایپ اطلاعات درون text-box، رابط کاربری نیز به روز شود، می‌توان bind-value را به یک رخداد خاص، مانند oninput متصل کرد. حالت پیش‌فرض آن onchange است:
<input type="number" @bind-value="@Room.Price" @bind-value:event="oninput" />
اکنون اگر برنامه را اجرا کرده و درون text-box اطلاعاتی را وارد کنیم، بلافاصله UI نیز به روز رسانی خواهد شد.
لیست کامل رخ‌دادها را در اینجا می‌توانید مشاهده کنید. برای مثال برای یک المان input، دو رخداد onchange و oninput قابل تعریف هستند.

یک نکته: در حین کار با bind-value@، نیازی نیست مقدار آن با @ شروع شود. یعنی ذکر "bind-value="Room.Price@ نیز کافی است.


تمرین 1 - خاصیت IsActive یک اتاق را به یک checkbox متصل کرده و همچنین وضعیت جاری آن‌را نیز در یک برچسب نمایش دهید.

در اینجا می‌خواهیم مقدار خاصیت Room.IsActive را توسط یک اتصال دو طرفه، به یک checkbox متصل کنیم:
<input type="checkbox" @bind-value="Room.IsActive"  />
<br/>
This room is @(Room.IsActive? "Active" : "Inactive").
با استفاده از bind-value@، وضعیت جاری خاصیت Room.IsActive را به یک checkbox متصل کرده‌ایم. همچنین در ادامه توسط یک عبارت شرطی، این وضعیت را نمایش داده‌ایم.


بار اولی که برنامه نمایش داده می‌شود، هر چند مقدار IsActive بر اساس مقدار دهی آن در شیء Room، مساوی true است، اما chekbox، علامت نخورده باقی می‌ماند. برای رفع این مشکل نیاز است ویژگی checked این المان را نیز به صورت زیر مقدار دهی کرد:
<input type="checkbox" @bind-value="Room.IsActive"
   checked="@(Room.IsActive? "cheked" : null)" />
در این حالت اگر اتاقی فعال باشد، مقدار ویژگی checked، به checked و در غیراینصورت به null تنظیم می‌شود. به این ترتیب مشکل عدم نمایش checkbox انتخاب شده در بار اول نمایش کامپوننت جاری، برطرف می‌شود.


اتصال خواص مدل‌ها به dropdown‌ها

اکنون می‌خواهیم مدل این مثال را کمی توسعه داده و خواص تو در تویی را به آن اضافه کنیم:
using System.Collections.Generic;

namespace BlazorServerSample.Models
{
    public class BlazorRoom
    {
        // ...

        public List<BlazorRoomProp> RoomProps { set; get; }
    }

    public class BlazorRoomProp
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public string Value { set; get; }
    }
}
برای مثال یک اتاق می‌تواند ویژگی‌هایی مانند مساحت، تعداد نفرات مجاز و غیره را داشته باشد. هدف از ویژگی جدید RoomProps، تعیین لیست این نوع موارد است.
پس از این تعاریف، فیلد Room را به صورت زیر به روز رسانی می‌کنیم تا تعدادی از خواص اتاق را به همراه داشته باشد:
@code
{
    BlazorRoom Room = new BlazorRoom
    {
        Id = 1,
        Name = "Room 1",
        IsActive = true,
        Price = 499,
        RoomProps = new List<BlazorRoomProp>
        {
            new BlazorRoomProp
            {
                Id = 1, Name = "Sq Ft", Value = "100"
            },
            new BlazorRoomProp
            {
                Id = 2, Name = "Occupancy", Value = "3"
            }
        }
    };
}
در ادامه می‌خواهیم این خواص را در یک dropdown نمایش دهیم. همچنین با انتخاب یک خاصیت از دراپ‌داون، مقدار خاصیت انتخابی را در یک برچسب نیز به صورت پویا نمایش خواهیم داد:
<select @bind="SelectedRoomPropValue">
    @foreach (var prop in Room.RoomProps)
    {
        <option value="@prop.Value">@prop.Name</option>
    }
</select>
<span>The value of the selected room prop is: @SelectedRoomPropValue</span>

@code
{
    string SelectedRoomPropValue = "";
    // ...
همانطور که مشاهده می‌کنید، انجام یک چنین کاری با Blazor بسیار ساده‌است و نیازی به استفاده از جاوا اسکریپت و یا جی‌کوئری ندارد.
در اینجا یک فیلد را در قطعه کد برنامه تعریف کرده و به المان select متصل کرده‌ایم. هرگاه آیتمی در این دراپ داون انتخاب شود، این فیلد، مقدار آن آیتم انتخابی را خواهد داشت. در ادامه توسط یک حلقه‌ی foreach، تمام خواص یک اتاق را دریافت کرده و به صورت options‌های یک select استاندارد، نمایش می‌دهیم. در آخر نیز مقدار SelectedRoomPropValue را نمایش داده‌ایم که این مقدار به صورت پویا تغییر می‌کند:



تعریف لیستی از اتاق‌ها

عموما در یک برنامه‌ی واقعی، با یک تک اتاق کار نمی‌کنیم. به همین جهت در ادامه لیستی از اتاق‌ها را تعریف و مقدار دهی اولیه خواهیم کرد:
@code
{
    string SelectedRoomPropValue = "";

    List<BlazorRoom> Rooms = new List<BlazorRoom>();

    protected override void OnInitialized()
    {
        base.OnInitialized();

        Rooms.Add(new BlazorRoom
        {
            Id = 1,
            Name = "Room 1",
            IsActive = true,
            Price = 499,
            RoomProps = new List<BlazorRoomProp>
            {
                new BlazorRoomProp
                {
                    Id = 1, Name = "Sq Ft", Value = "100"
                },
                new BlazorRoomProp
                {
                    Id = 2, Name = "Occupancy", Value = "3"
                }
            }
        });

        Rooms.Add(new BlazorRoom
        {
            Id = 2,
            Name = "Room 2",
            IsActive = true,
            Price = 399,
            RoomProps = new List<BlazorRoomProp>
            {
                new BlazorRoomProp
                {
                    Id = 1, Name = "Sq Ft", Value = "250"
                },
                new BlazorRoomProp
                {
                    Id = 2, Name = "Occupancy", Value = "4"
                }
            }
        });
    }
}
در ابتدا فیلد Rooms تعریف شده که لیستی از BlazorRoomها است. در ادامه بجای مقدار دهی مستقیم آن در همان سطح قطعه کد، آن‌را در یک متد life-cycle کامپوننت جاری به نام OnInitialized که مخصوص این نوع مقدار دهی‌های اولیه است، مقدار دهی کرده‌ایم.


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

اکنون می‌خواهیم به عنوان تمرین 2، لیست جزئیات اتاق‌های تعریف شده را نمایش دهیم؛ با این شرط که نام و قیمت هر اتاق، قابل ویرایش باشد. همچنین خواص تعریف شده نیز به صورت ستون‌هایی مجزا، نمایش داده شوند. برای مثال اگر دو خاصیت در اینجا تعریف شده، 2 ستون اضافه‌تر نیز برای نمایش آن‌ها وجود داشته باشد. به علاوه از آنجائیکه می‌خواهیم اتصال دوطرفه را نیز آزمایش کنیم، نام و قیمت هر اتاق را نیز در پایین جدول، مجددا به صورت برچسب‌هایی نمایش خواهیم داد.


برای رسیدن به تصویر فوق می‌توان به صورت زیر عمل کرد:
<div class="border p-2 mt-3">
    <h2 class="text-info">Rooms List</h2>
    <table class="table table-dark">
        @foreach(var room in Rooms)
        {
            <tr>
                <td>
                    <input type="text" @bind-value="room.Name" @bind-value:event="oninput"/>
                </td>
                <td>
                    <input type="text" @bind-value="room.Price" @bind-value:event="oninput"/>
                </td>
                @foreach (var roomProp in room.RoomProps)
                {
                    <td>
                        @roomProp.Name, @roomProp.Value
                    </td>
                }
            </tr>
        }
    </table>

    @foreach(var room in Rooms)
    {
        <p>@room.Name's price is @room.Price.</p>
    }
</div>
در اینجا یک حلقه‌ی تو در تو را مشاهده می‌کنید. حلقه‌ی بیرونی، ردیف‌های جدول را که شامل نام و قیمت هر اتاق است، به صورت input-boxهای متصل به خواص متناظر با آن‌ها نمایش می‌دهد. سپس برای اینکه بتوانیم خواص هر ردیف را نیز نمایش دهیم، حلقه‌ی دومی را بر روی room.RoomProps تشکیل داده‌ایم.
هدف از foreach پس از جدول، نمایش تغییرات انجام شده‌ی در input-boxها است. برای مثال اگر نام یک ردیف را تغییر دادیم، چون یک اتصال دو طرفه برقرار است، خاصیت متناظر با آن به روز رسانی شده و بلافاصله در برچسب‌های ذیل جدول، منعکس می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-04.zip
نظرات مطالب
ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax
برای ارسال آرایه توسط FormData، نباید آرایه را مستقیما Append کرد؛ بدین شکل:
var formData = new FormData();
var splitedKeywords = $('#keywords').val().split(',');
formData.append('Keywords', JSON.stringify(splitedKeywords));
در این صورت خروجی در سمت سرور به این شکل خواهد بود؛ آرایه ایی تک عضوی با این مقدار:
"\"کلمه اول\",\"کلمه دوم\""

برای ارسال یک آرایه (لیست) باید به صورت زیر عمل شود :
var formData = new FormData();
var splitedKeywords = $('#keywords').val().split(',');
for (var counter = 0; counter < splitedKeywords.length; counter++) {
    formData.append('Keywords', JSON.stringify(splitedKeywords[counter]));
}
تمام آرایه تک به تک اضافه شود.
مطالب
C# 8.0 - Ranges & Indices
نوع Span به همراه NET Core 2.1. ارائه شد. یکی از مهم‌ترین مزایای آن امکان دسترسی به قسمتی از حافظه (توسط متد Split آن)، بدون ایجاد سربار کپی یا تخصیص مجدد حافظه‌ای برای دسترسی به آن است. قدم بعدی، بسط این قابلیت به امکانات ذاتی زبان #C است؛ تحت عنوان ویژگی Ranges که امکان دسترسی مستقیم به بازه‌ای/قسمتی از آرایه‌ها، رشته‌ها و یا Spanها را میسر می‌کند.


معرفی عملگر Hat

برای دسترسی به آخرین عضو یک آرایه عموما از روش زیر استفاده می‌شود:
var integerArray = new int[3];
var lastItem = integerArray[integerArray.Length - 1];
یعنی از آخر شروع به شمارش کرده و 1 واحد از آن کم می‌کنیم (این عدد 1 را به‌خاطر داشته باشید) و یا اگر بخواهیم از LINQ استفاده کنیم می‌توان از متد Last آن استفاده کرد:
var integerList = integerArray.ToList();
integerList.Last();
همچنین اگر بخواهیم دومین عنصر از آخر آن‌را دریافت کنیم:
var secondToLast = integerArray[integerArray.Length - 2];
نیز مجددا از آخر شروع به شمارش کرده و 2 واحد، از آن کم می‌کنیم (این عدد 2 را نیز به‌خاطر داشته باشید).

این شمردن‌های از آخر در C# 8.0 توسط ارائه‌ی عملگر hat یا همان ^ که پیشتر کار xor را انجام می‌داد (و البته هنوز هم در جای خودش همین مفهوم را دارد)، میسر شده‌است:
var lastItem = integerArray[^1];
این قطعه کد یعنی به دنبال ایندکس X، از آخر آرایه هستیم.

نکته‌ی مهم: کسانیکه شروع به آموزش برنامه نویسی می‌کنند، مدتی طول می‌کشد تا عادت کنند که اولین ایندکس یک آرایه از صفر شروع می‌شود. در اینجا باید درنظر داشت که با بکارگیری «عملگر کلاه»، آخرین ایندکس یک آرایه از «یک» شروع می‌شود و نه از صفر. برای نمونه در مثال زیر به خوبی تفاوت بین ایندکس از ابتدا و ایندکس از انتها را می‌توانید مشاهده کنید:
var words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (or words.Length) ^0
آرایه‌ی فوق، 9 عضو دارد. در این حالت اولین عنصر آن با ایندکس صفر قابل دسترسی است. در همین حالت همین ایندکس را اگر از آخر محاسبه کنیم، 9 خواهد بود و همینطور برای مابقی.
در حالت کلی ایندکس n^ معادل sequence.Length - n است. بنابراین sequence[^0] به معنای sequence[sequence.Length] است و هر دو مورد یک index out of range exception را صادر می‌کنند.

IDE نیز با فعال سازی C# 8.0، زمانیکه به قطعه کد زیر می‌رسد، زیر words.Length - 1 خط کشیده و پیشنهاد می‌دهد که بهتر است از 1^ استفاده کنید:
Console.WriteLine($"The last word is {words[words.Length - 1]}");



معرفی نوع جدید Index

در C# 8.0 زمانیکه می‌نویسم 1^، در حقیقت قطعه کد زیر را ایجاد کرده‌ایم:
var index = new Index(value: 1, fromEnd: true);
Index indexStruct = ^1;
var indexShortHand = ^1;
Index یک struct و نوع جدید در C# 8.0 می‌باشد که در فضای نام System قرار گرفته‌است. سه سطر فوق دقیقا به یک معنا هستند و هر کدام خلاصه شده و ساده شده‌ی سطر قبلی است.
در سطر اول، پارامتر fromEnd نیز قابل تعریف است. این fromEnd با مقدار true، همان عملگر ^ در اینجا است و عدم ذکر این عملگر به معنای false بودن آن است:
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine(a[a.Length – 2]); // will write 8 on console.
Console.WriteLine(a[^2]); // will write 8 on console.

Index i5 = 5;
Console.WriteLine(a[i5]);        //will write 5 on console.

Index i2fromEnd = ^2;
Console.WriteLine(a[i2fromEnd]); // will write 8 on console.
در این مثال دو نمونه کاربرد fromEnd با مقدار false و سپس true را ملاحظه می‌کنید. در حالتیکه Index i5 = 5 تعریف شده‌است، دسترسی به عناصر آرایه همانند قبل از ایندکس صفر و از آغاز شروع می‌شود و نه از ایندکس یک.


روش دسترسی به بازه‌ای از اعضای یک آرایه تا پیش از C# 8.0

فرض کنید آرایه‌ای از اعداد بین 1 تا 10 را به صورت زیر ایجاد کرده‌اید:
var numbers = Enumerable.Range(1, 10).ToArray();
اکنون اگر بخواهیم به بازه‌ی مشخصی درون این آرایه دسترسی پیدا کنیم، می‌توان حداقل به یکی از دو روش زیر عمل کرد:
var (start, end) = (1, 7); 
var length = end - start; 

// Using LINQ 
var subset1 = numbers.Skip(start).Take(length);
 
// Or using Array.Copy 
var subset2 = new int[length];
Array.Copy(numbers, start, subset2, 0, length);
یا می‌توان برای مثال توسط LINQ، از متدهای Skip و Take آن برای جدا کردن بازه‌ای از آرایه‌ی numbers استفاده کرد و یا حتی می‌توان از روش کپی کردن آرایه‌ها به آرایه‌ای جدید نیز کمک گرفت که در هر دو حالت، مراحلی که باید طی شوند قابل توجه است. با ارائه‌ی C# 8.0، این نوع دسترسی‌ها جزئی از قابلیت‌های زبان شده‌اند.


روش دسترسی به بازه‌ای از اعضای یک آرایه در C# 8.0

در C# 8.0 برای دسترسی به بازه‌ای از عناصر یک آرایه می‌توان از range expression که به صورت x..y نوشته می‌شود، استفاده کرد. در ادامه، مثال‌هایی را از کاربردهای عبارت .. ملاحظه می‌کنید:
var myArray = new string[] { "Item1", "Item2", "Item3", "Item4", "Item5" };
3..1 به معنای انتخاب بازه‌ای از المان 2 تا المان 3 است. در اینجا به واژه‌ی «المان» دقت کنید که معادل ایندکس آن در آرایه نیست. یعنی عدد ابتدای یک بازه دقیقا به ایندکس آن در آرایه اشاره می‌کند و عدد انتهای بازه، به ایندکس ماقبل آن (از این جهت که بتوان توسط 0^، انتهای بازه را مشخص کرد؛ بدون دریافت استثنای index out of range). به همین جهت به ابتدای بازه می‌گویند inclusive و به انتهای آن exclusive:
 var fromIndexToX = myArray[1..3]; // = [Item2, Item3]
1^..1 به معنای انتخاب بازه‌ای از المان 2، تا المان یکی مانده به آخر است:
var fromIndexToXFromTheEnd = myArray[1..^1]; // = [ "Item2", "Item3", "Item4" ]

ذکر انتهای بازه اجباری نیست و اگر ذکر نشود به معنای 0^ است. برای مثال ..1 به معنای انتخاب بازه‌ای از المان 2، تا آخرین المان است:
var fromAnIndexToTheEnd = myArray[1..]; // = [ "Item2", "Item3", "Item4", "Item5" ]

ذکر ابتدای بازه نیز اجباری نیست و اگر ذکر نشود به معنای عدد صفر است. برای مثال 3.. به معنای انتخاب بازه‌ای از اولین المان، تا سومین المان است:
 var fromTheStartToAnIndex = myArray[..3]; // = [ "Item1", "Item2", "Item3" ]

اگر ابتدا و انتهای بازه ذکر نشوند، کل بازه و تمام عناصر آن بازگشت داده می‌شوند:
 var entireRange = myArray[..]; // = [ "Item1", "Item2", "Item3", "Item4", "Item5" ]
همچنین [0^..0] نیز به معنای کل بازه است.

مثالی دیگر: بازنویسی یک حلقه‌ی for با foreach
حلقه‌ی for زیر را
var myArray = new string[] { "Item1", "Item2", "Item3", "Item4", "Item5" };
for (int i = 1; i <= 3; i++)
{
  Console.WriteLine(myArray[i]);
}
توسط range expression می‌توان به صورت زیر بازنویسی کرد:
foreach (var item in myArray[1..4]) // = [ "Item2", "Item3", "Item4" ]
{
  Console.WriteLine(item);
}
بنابراین همانطور که مشاهده می‌کنید، ذکر بازه‌ی 4..1 به صورت حلقه‌ی for (int i = 1; i < 4; i++) تفسیر می‌شود و نه حلقه‌ی for (int i = 1; i <= 4; i++)
یعنی ابتدای آن inclusive است و انتهای آن exclusive


چند مثال کاربردی و متداول از بازه‌ها

using System;
using System.Linq;

namespace ConsoleApp
{
    class Program
    {
        private static readonly int[] _numbers = Enumerable.Range(1, 10).ToArray();

        static void Main()
        {
            var skip2CharactersAndTake2Characters = _numbers[2..4]; // صرفنظر کردن از دو عنصر اول و سپس انتخاب دو عنصر
            var skipFirstAndLastCharacter = _numbers[1..^1]; // صرفنظر کردن از دو عنصر اول و آخر
            var last3Characters = _numbers[^3..]; // انتخاب بازه‌ای شامل سه عنصر آخر
            var first4Characters = _numbers[0..4]; // دریافت بازه‌ای از 4 عنصر اول
            var rangeStartFrom2 = _numbers[2..]; // دریافت بازه‌ای شروع شده از المان دوم تا آخر
            var skipLast3Characters = _numbers[..^3]; // صرفنظر کردن از سه المان آخر
            var rangeAll = _numbers[..]; // انتخاب کل بازه
        }
    }
}


معرفی نوع جدید Range

در C# 8.0 زمانیکه می‌نویسم 4..1، در حقیقت قطعه کد زیر را ایجاد کرده‌ایم:
var range = new Range(1, 4);
Range rangeStruct = 1..4;
var rangeShortHand = 1..4;
Range نیز یک struct و نوع جدید در C# 8.0 می‌باشد که در فضای نام System قرار گرفته‌است. سه سطر فوق دقیقا به یک معنا هستند و هر کدام خلاصه شده و ساده شده‌ی سطر قبلی است.

یک مثال: استفاده از نوع جدید Range به عنوان پارامتر یک متد
using System;
using System.Linq;

namespace ConsoleApp
{
    class Program
    {
        private static readonly int[] _numbers = Enumerable.Range(1, 10).ToArray();
        static void Print(Range range) => Console.WriteLine($"{range} => {string.Join(", ", _numbers[range])}");

        static void Main()
        {
            Print(1..3); // 1..3 => 2, 3
            Print(..3);      // 0..3 => 1, 2, 3
            Print(3..);      // 3..^0 => 4, 5, 6, 7, 8, 9, 10
            Print(1..^1);    // 1..^1 => 2, 3, 4, 5, 6, 7, 8, 9
            Print(^2..^1);   // ^2..^1 => 9
        }
    }
}
همانطور که ملاحظه می‌کنید، Range را می‌توان به عنوان پارامتر متدها نیز استفاده و بر روی آرایه‌ها اعمال کرد؛ اما با <List<T سازگار نیست.

مثالی دیگر: استفاده از Range به عنوان جایگزینی برای متد String.Substring

از Range می‌توان برای کار بر روی رشته‌ها و انتخاب قسمتی از آن‌ها نیز استفاده کرد:
Console.WriteLine("123456789"[1..4]); // Would output 234
چند مثال دیگر:
var helloWorldStr = "Hello, World!";

var hello = helloWorldStr[..5];
Console.WriteLine(hello); // Output: Hello

var world = helloWorldStr[7..];
Console.WriteLine(world); // Output: World!

var world2 = helloWorldStr[^6..]; // Take the last 6 characters
Console.WriteLine(world); // Output: World!


سؤال: زمانیکه بازه‌ای از یک آرایه را انتخاب می‌کنیم، آیا یک آرایه‌ی جدید ایجاد می‌شود، یا هنوز به همان آرایه‌ی قبلی اشاره می‌کند؟

پاسخ: یک آرایه‌ی جدید ایجاد می‌شود؛ اما می‌توان با فراخوانی متد ()array.AsSpan پیش از انتخاب یک بازه، بازه‌ای را تولید کرد که دقیقا به همان آرایه‌ی اصلی اشاره می‌کند و یک کپی جدید نیست:
var arr = (new[] { 1, 4, 8, 11, 19, 31 }).AsSpan();
var range = arr[2..5];

ref int elt1 = ref range[1];
elt1 = -1;

int copiedElement = range[2];
copiedElement = -2;

Console.WriteLine($"range[1]: {range[1]}, range[2]: {range[2]}"); // output: range[1]: -1, range[2]: 19
Console.WriteLine($"arr[3]: {arr[3]}, arr[4]: {arr[4]}"); // output: arr[3]: -1, arr[4]: 19
در این مثال، آرایه‌ی اصلی را ابتدا تبدیل به یک Span کرده‌ایم و سپس بازه‌ای از روی آن انتخاب شده‌است. به همین جهت است که زمانیکه از ref locals برای تغییر عضوی از این بازه استفاده می‌شود، این تغییر بر روی آرایه‌ی اصلی نیز تاثیر می‌گذارد.
مطالب
رمزنگاری فایل‌های PDF با استفاده از کلید عمومی توسط iTextSharp

دو نوع رمزنگاری را می‌توان توسط iTextSharp به PDF تولیدی و یا موجود، اعمال کرد:
الف) رمزنگاری با استفاده از کلمه عبور
ب) رمزنگاری توسط کلید عمومی

الف) رمزنگاری با استفاده از کلمه عبور
در اینجا امکان تنظیم read password و edit password به کمک متد SetEncryption شیء pdfWrite وجود دارد. همچنین می‌توان مشخص کرد که مثلا آیا کاربر می‌تواند فایل PDF را چاپ کند یا خیر (PdfWriter.ALLOW_PRINTING).
ذکر read password اختیاری است؛ اما جهت اعمال permissions حتما نیاز است تا edit password ذکر گردد:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.Text;

namespace EncryptPublicKey
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));

var readPassword = Encoding.UTF8.GetBytes("123");//it can be null.
var editPassword = Encoding.UTF8.GetBytes("456");
int permissions = PdfWriter.ALLOW_PRINTING | PdfWriter.ALLOW_COPY;
pdfWriter.SetEncryption(readPassword, editPassword, permissions, PdfWriter.STRENGTH128BITS);

pdfDoc.Open();

pdfDoc.Add(new Phrase("tst 0"));
pdfDoc.NewPage();
pdfDoc.Add(new Phrase("tst 1"));
}

Process.Start("TestEnc.pdf");
}
}
}


اگر read password ذکر شود، کاربران برای مشاهده محتویات فایل نیاز خواهند داشت تا کلمه‌ی عبور مرتبط را وارد نمایند:


این روش آنچنان امنیتی ندارد. هستند برنامه‌هایی که این نوع فایل‌ها را «آنی» به نمونه‌ی غیر رمزنگاری شده تبدیل می‌کنند (حتی نیازی هم ندارند که از شما کلمه‌ی عبوری را سؤال کنند). بنابراین اگر کاربران شما آنچنان حرفه‌ای نیستند، این روش خوب است؛ در غیراینصورت از آن صرفنظر کنید.


ب) رمزنگاری توسط کلید عمومی
این روش نسبت به حالت الف بسیار پیشرفته‌تر بوده و امنیت قابل توجهی هم دارد و «نیستند» برنامه‌هایی که بتوانند این فایل‌ها را بدون داشتن اطلاعات کافی، به سادگی رمزگشایی کنند.

برای شروع به کار با public key encryption نیاز است یک فایل PFX یا Personal Information Exchange داشته باشیم. یا می‌توان این نوع فایل‌ها را از CA's یا Certificate Authorities خرید، که بسیار هم نیکو یا اینکه می‌توان فعلا برای آزمایش، نمونه‌ی self signed این‌ها را هم تهیه کرد. مثلا با استفاده از این برنامه.


در ادامه نیاز خواهیم داشت تا اطلاعات این فایل PFX را جهت استفاده توسط iTextSharp استخراج کنیم. کلاس‌های زیر اینکار را انجام می‌دهند و نهایتا کلیدهای عمومی و خصوصی ذخیره شده در فایل PFX را بازگشت خواهند داد:

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.X509;

namespace EncryptPublicKey
{
/// <summary>
/// A Personal Information Exchange File Info
/// </summary>
public class PfxData
{
/// <summary>
/// Represents an X509 certificate
/// </summary>
public X509Certificate[] X509PrivateKeys { set; get; }

/// <summary>
/// Certificate's public key
/// </summary>
public ICipherParameters PublicKey { set; get; }
}
}

using System;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;

namespace EncryptPublicKey
{
/// <summary>
/// A Personal Information Exchange File Reader
/// </summary>
public class PfxReader
{
X509Certificate[] _chain;
AsymmetricKeyParameter _asymmetricKeyParameter;

/// <summary>
/// Reads A Personal Information Exchange File.
/// </summary>
/// <param name="pfxPath">Certificate file's path</param>
/// <param name="pfxPassword">Certificate file's password</param>
public PfxData ReadCertificate(string pfxPath, string pfxPassword)
{
using (var stream = new FileStream(pfxPath, FileMode.Open, FileAccess.Read))
{
var pkcs12Store = new Pkcs12Store(stream, pfxPassword.ToCharArray());
var alias = findThePublicKey(pkcs12Store);
_asymmetricKeyParameter = pkcs12Store.GetKey(alias).Key;
constructChain(pkcs12Store, alias);
return new PfxData { X509PrivateKeys = _chain, PublicKey = _asymmetricKeyParameter };
}
}

private void constructChain(Pkcs12Store pkcs12Store, string alias)
{
var certificateChains = pkcs12Store.GetCertificateChain(alias);
_chain = new X509Certificate[certificateChains.Length];

for (int k = 0; k < certificateChains.Length; ++k)
_chain[k] = certificateChains[k].Certificate;
}

private static string findThePublicKey(Pkcs12Store pkcs12Store)
{
string alias = string.Empty;
foreach (string entry in pkcs12Store.Aliases)
{
if (pkcs12Store.IsKeyEntry(entry) && pkcs12Store.GetKey(entry).Key.IsPrivate)
{
alias = entry;
break;
}
}

if (string.IsNullOrEmpty(alias))
throw new NullReferenceException("Provided certificate is invalid.");

return alias;
}
}
}


اکنون رمزنگاری فایل PDF تولیدی توسط کلید عمومی، به سادگی چند سطر کد زیر خواهد بود:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace EncryptPublicKey
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));

var certs = new PfxReader().ReadCertificate(@"D:\path\cert.pfx", "123");
pdfWriter.SetEncryption(
certs: certs.X509PrivateKeys,
permissions: new int[] { PdfWriter.ALLOW_PRINTING, PdfWriter.ALLOW_COPY },
encryptionType: PdfWriter.ENCRYPTION_AES_128);

pdfDoc.Open();

pdfDoc.Add(new Phrase("tst 0"));
pdfDoc.NewPage();
pdfDoc.Add(new Phrase("tst 1"));
}

Process.Start("Test.pdf");
}
}
}

پیش از فراخوانی متد Open باید تنظیمات رمزنگاری مشخص شوند. در اینجا ابتدا فایل PFX خوانده شده و کلیدهای عمومی و خصوصی آن استخراج می‌شوند. سپس به متد SetEncryption جهت استفاده نهایی ارسال خواهند شد.

نحوه استفاده از این نوع فایل‌های رمزنگاری شده:
اگر سعی در گشودن این فایل رمزنگاری شده نمائیم با خطای زیر مواجه خواهیم شد:


کاربران برای اینکه بتوانند این فایل‌های PDF را بار کنند نیاز است تا فایل PFX شما را در سیستم خود نصب کنند. ویندوز فایل‌های PFX را می‌شناسد و نصب آن‌ها با دوبار کلیک بر روی فایل و چندبار کلیک بر روی دکمه‌ی Next و وارد کردن کلمه عبور آن، به پایان می‌رسد.

سؤال: آیا می‌توان فایل‌های PDF موجود را هم به همین روش رمزنگاری کرد؟
بله. iTextSharp علاوه بر PdfWriter دارای PdfReader نیز می‌باشد:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace EncryptPublicKey
{
class Program
{
static void Main(string[] args)
{
PdfReader reader = new PdfReader("TestDec.pdf");
using (var stamper = new PdfStamper(reader, new FileStream("TestEnc.pdf", FileMode.Create)))
{
var certs = new PfxReader().ReadCertificate(@"D:\path\cert.pfx", "123");
stamper.SetEncryption(
certs: certs.X509PrivateKeys,
permissions: new int[] { PdfWriter.ALLOW_PRINTING, PdfWriter.ALLOW_COPY },
encryptionType: PdfWriter.ENCRYPTION_AES_128);
stamper.Close();
}

Process.Start("TestEnc.pdf");
}
}
}


سؤال: آیا می‌توان نصب کلید عمومی را خودکار کرد؟
سورس برنامه SelfCert که معرفی شد، در دسترس است. این برنامه قابلیت انجام نصب خودکار مجوزها را دارد.

بازخوردهای دوره
Lazy loading در تزریق وابستگی‌ها به کمک StructureMap
در حالت lazy 
public interface IUnitOfWork
{
    Lazy<IDbSet<TEntity>> LazySet<TEntity>() where TEntity : class; 
    int SaveChanges(); 
}
در کلاس context که از کلاس IUnitOfWork  ارث بری کرده پیاده سازی متد
Lazy <IDbSet<TEntity>> LazySet<TEntity>()  where  TEntity :  class
ایراد می‌دهد.
مطالب
گزارشگیری از تاریخچه‌ی پشتیبان‌گیری‌ها در اس کیوال سرور

آیا می‌دانید آخرین باری که از یک دیتابیس مفروض بک‌آپ گرفته شده، چه زمانی بوده است؟ (یا اینکه اصلا چه اهمیتی داره؟!)

USE master;
SELECT B.name AS Database_Name,
ISNULL(STR(ABS(DATEDIFF(day, GetDate(),
MAX(Backup_finish_date)))), 'NEVER') AS DaysSinceLastBackup,
ISNULL(CONVERT(char(10), MAX(backup_finish_date), 101), 'NEVER') AS
LastBackupDate
FROM master.dbo.sysdatabases B
LEFT OUTER JOIN msdb.dbo.backupset A
ON A.database_name = B.name
AND A.type = 'D'
GROUP BY
B.Name
ORDER BY
B.name


همانطور که مطلع هستید در SQL Server 2008 امکان فشرده سازی خودکار بک آپ‌ها هم فراهم شده است. با استفاده از اسکریپت زیر می‌توان از درصد فشرده سازی بک‌آپ‌ها گزارش گرفت:
SELECT bs.server_name,
bs.database_name AS 'Database Name',
CONVERT (BIGINT, bs.backup_size / 1048576 ) AS
'Uncompressed Backup Size (MB)',
CONVERT (BIGINT, bs.compressed_backup_size / 1048576 ) AS
'Compressed Backup Size (MB)',
CONVERT (NUMERIC (20,2), (CONVERT (FLOAT, bs.backup_size) /
CONVERT (FLOAT, bs.compressed_backup_size))) AS 'Compression Ratio',
DATEDIFF (SECOND, bs.backup_start_date, bs.backup_finish_date) AS
'Backup Elapsed Time (sec)',
bs.backup_finish_date AS 'Backup Finish Date'
FROM msdb.dbo.backupset AS bs
WHERE DATEDIFF (SECOND, bs.backup_start_date, bs.backup_finish_date) > 0
AND bs.backup_size > 0
AND bs.type = 'D' -- Change to L if you want Log backups
ORDER BY
bs.backup_finish_date DESC




برای فعال سازی گزینه فشرده سازی بک‌آپ‌ها با استفاده از دستورات T-SQL می‌توان به صورت زیر عمل کرد (در SQL server 2008):
USE master;
EXEC sp_configure 'backup compression default', '1';
RECONFIGURE WITH OVERRIDE;



ماخذ مورد استفاده:
http://www.sqlservercentral.com

مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت دوم
در مطلب «طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول» با ساختار کلی یک پروژه‌ی افزونه‌ی پذیر ASP.NET MVC آشنا شدیم. پس از راه اندازی آن و مدتی کار کردن با این نوع پروژه‌ها، این سؤال پیش خواهد آمد که ... خوب، اگر هر افزونه تصاویر یا فایل‌های CSS و JS اختصاصی خودش را بخواهد داشته باشد، چطور؟ موارد عمومی مانند بوت استرپ و جی‌کوئری را می‌توان در پروژه‌ی پایه قرار داد تا تمام افزونه‌ها به صورت یکسانی از آن‌ها استفاده کنند، اما هدف، ماژولار شدن برنامه است و جدا کردن فایل‌های ویژه‌ی هر پروژه، از پروژ‌ه‌ای دیگر و همچنین بالا بردن سهولت کار تیمی، با شکستن اجزای یک پروژه به صورت افزونه‌هایی مختلف، بین اعضای یک تیم. در این قسمت نحوه‌ی مدفون سازی انواع فایل‌های استاتیک افزونه‌ها را درون فایل‌های DLL آن‌ها بررسی خواهیم کرد. به این ترتیب دیگر نیازی به ارائه‌ی مجزای آن‌ها و یا کپی کردن آن‌ها در پوشه‌های پروژه‌ی اصلی نخواهد بود.


مدفون سازی فایل‌های CSS و JS هر افزونه درون فایل DLL آن

به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاس‌های عمومی و مشترک بین افزونه‌ها استفاده خواهیم کرد. برای مثال قصد نداریم کلاس‌های سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونه‌ای جدید کپی کنیم. کتابخانه‌ی Common، امکان استفاده‌ی مجدد از یک سری کدهای تکراری را در بین افزونه‌ها میسر می‌کند.
این پروژه برای کامپایل شدن نیاز به بسته‌ی نیوگت ذیل دارد:
 PM> install-package Microsoft.AspNet.Web.Optimization
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.
پس از این مقدمات، کلاس ذیل را به این پروژه‌ی class library جدید اضافه کنید:
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web.Optimization;
 
namespace MvcPluginMasterApp.Common.WebToolkit
{
    public class EmbeddedResourceTransform : IBundleTransform
    {
        private readonly IList<string> _resourceFiles;
        private readonly string _contentType;
        private readonly Assembly _assembly;
 
        public EmbeddedResourceTransform(IList<string> resourceFiles, string contentType, Assembly assembly)
        {
            _resourceFiles = resourceFiles;
            _contentType = contentType;
            _assembly = assembly;
        }
 
        public void Process(BundleContext context, BundleResponse response)
        {
            var result = new StringBuilder();
 
            foreach (var resource in _resourceFiles)
            {
                using (var stream = _assembly.GetManifestResourceStream(resource))
                {
                    if (stream == null)
                    {
                        throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", resource, _assembly.FullName));
                    }
 
                    using (var reader = new StreamReader(stream))
                    {
                        result.Append(reader.ReadToEnd());
                    }
                }
            }
 
            response.ContentType = _contentType;
            response.Content = result.ToString();
        }
    }
}
اگر با سیستم bundling & minification کار کرده باشید، با تعاریفی مانند ("new Bundle("~/Plugin1/Scripts آشنا هستید. سازنده‌ی کلاس Bundle، پارامتر دومی را نیز می‌پذیرد که از نوع IBundleTransform است. با پیاده سازی اینترفیس IBundleTransform می‌توان محل ارائه‌ی فایل‌های استاتیک CSS و JS را بجای فایل سیستم متداول و پیش فرض، به منابع مدفون شده‌ی در اسمبلی جاری هدایت و تنظیم کرد.
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایل‌ها و منابع مدفون شده گشته و سپس محتوای آن‌ها را بازگشت می‌دهد.
اکنون برای استفاده‌ی از آن، به پروژه‌ی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژه‌ی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آن‌را به نحو ذیل تکمیل کنید:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return null;
        }
 
        public MenuItem GetMenuItem(RequestContext requestContext)
        {
            return new MenuItem
            {
                Name = "Plugin 1",
                Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" })
            };
        }
 
        public void RegisterBundles(BundleCollection bundles)
        {
            var executingAssembly = Assembly.GetExecutingAssembly();
            // Mostly the default namespace and assembly name are the same
            var assemblyNameSpace = executingAssembly.GetName().Name;
            var scriptsBundle = new Bundle("~/Plugin1/Scripts",
                new EmbeddedResourceTransform(new List<string>
                {
                    assemblyNameSpace + ".Scripts.test1.js"
                }, "application/javascript", executingAssembly));
            if (!HttpContext.Current.IsDebuggingEnabled)
            {
                scriptsBundle.Transforms.Add(new JsMinify());
            }
            bundles.Add(scriptsBundle);
            var cssBundle = new Bundle("~/Plugin1/Content",
                new EmbeddedResourceTransform(new List<string>
                {
                    assemblyNameSpace + ".Content.test1.css"
                }, "text/css", executingAssembly));
            if (!HttpContext.Current.IsDebuggingEnabled)
            {
                cssBundle.Transforms.Add(new CssMinify());
            }
            bundles.Add(cssBundle);
            BundleTable.EnableOptimizations = true;
        }
 
        public void RegisterRoutes(RouteCollection routes)
        {
        }
 
        public void RegisterServices(IContainer container)
        {
        }
    }
}
در اینجا نحوه‌ی کار با کلاس سفارشی EmbeddedResourceTransform را مشاهده می‌کنید. ابتدا فایل‌های js و سپس فایل‌های css برنامه به سیستم Bundling برنامه اضافه شده‌اند.
این فایل‌ها به صورت ذیل در پروژه تعریف گردیده‌اند:


همانطور که مشاهده می‌کنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آن‌ها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.

یک نکته‌ی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نام‌هایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره می‌شوند:



مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن

مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using System.Web.Routing;
 
namespace MvcPluginMasterApp.Common.WebToolkit
{
    public class EmbeddedResourceRouteHandler : IRouteHandler
    {
        private readonly Assembly _assembly;
        private readonly string _resourcePath;
        private readonly TimeSpan _cacheDuration;
 
        public EmbeddedResourceRouteHandler(Assembly assembly, string resourcePath, TimeSpan cacheDuration)
        {
            _assembly = assembly;
            _resourcePath = resourcePath;
            _cacheDuration = cacheDuration;
        }
 
        IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
        {
            return new EmbeddedResourceHttpHandler(requestContext.RouteData, _assembly, _resourcePath, _cacheDuration);
        }
    }
 
    public class EmbeddedResourceHttpHandler : IHttpHandler
    {
        private readonly RouteData _routeData;
        private readonly Assembly _assembly;
        private readonly string _resourcePath;
        private readonly TimeSpan _cacheDuration;
 
        public EmbeddedResourceHttpHandler(
            RouteData routeData, Assembly assembly, string resourcePath, TimeSpan cacheDuration)
        {
            _routeData = routeData;
            _assembly = assembly;
            _resourcePath = resourcePath;
            _cacheDuration = cacheDuration;
        }
 
        public bool IsReusable
        {
            get { return false; }
        }
 
        public void ProcessRequest(HttpContext context)
        {
            var routeDataValues = _routeData.Values;
            var fileName = routeDataValues["file"].ToString();
            var fileExtension = routeDataValues["extension"].ToString();
 
            var manifestResourceName = string.Format("{0}.{1}.{2}", _resourcePath, fileName, fileExtension);
            var stream = _assembly.GetManifestResourceStream(manifestResourceName);
            if (stream == null)
            {
                throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", manifestResourceName, _assembly.FullName));
            }
 
            context.Response.Clear();
            context.Response.ContentType = "application/octet-stream";
            cacheIt(context.Response, _cacheDuration);
            stream.CopyTo(context.Response.OutputStream);
        }
 
        private static void cacheIt(HttpResponse response, TimeSpan duration)
        {
            var cache = response.Cache;
 
            var maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
            if (maxAgeField != null) maxAgeField.SetValue(cache, duration);
 
            cache.SetCacheability(HttpCacheability.Public);
            cache.SetExpires(DateTime.Now.Add(duration));
            cache.SetMaxAge(duration);
            cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
        }
    }
}
تصاویر پروژه‌ی افزونه نیز به صورت embedded resource در اسمبلی آن قرار خواهند گرفت. به همین جهت باید سیستم مسیریابی را پس درخواست رسیده‌ی جهت نمایش تصاویر، به منابع ذخیره شده‌ی در اسمبلی آن هدایت نمود. اینکار را با پیاده سازی یک IRouteHandler سفارشی، می‌توان به نحو فوق مدیریت کرد.
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت می‌دهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شده‌است و هدرهای خاص caching را به صورت خودکار اضافه می‌کند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه می‌کند.

اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    { 
        public void RegisterRoutes(RouteCollection routes)
        {
            //todo: add custom routes.
 
            var assembly = Assembly.GetExecutingAssembly();
            // Mostly the default namespace and assembly name are the same
            var nameSpace = assembly.GetName().Name;
            var resourcePath = string.Format("{0}.Images", nameSpace);
 
            routes.Insert(0,
                new Route("NewsArea/Images/{file}.{extension}",
                    new RouteValueDictionary(new { }),
                    new RouteValueDictionary(new { extension = "png|jpg" }),
                    new EmbeddedResourceRouteHandler(assembly, resourcePath, cacheDuration: TimeSpan.FromDays(30))
                ));
        } 
    }
}
در مسیریابی تعریف شده، تمام درخواست‌های رسیده‌ی به مسیر NewsArea/Images به EmbeddedResourceRouteHandler هدایت می‌شوند.
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی می‌شوند از نوع png یا jpg تعریف شده‌اند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شده‌است.


استفاده‌ی نهایی از تنظیمات فوق در یک View افزونه

پس از اینکه تصاویر و فایل‌های css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آن‌ها را نیز مشخص نمودیم، اکنون نوبت به استفاده‌ی از آن‌ها در یک View است:
@{
    ViewBag.Title = "From Plugin 1";
}
@Styles.Render("~/Plugin1/Content")
 
<h2>@ViewBag.Message</h2>
 
<div class="row">
    Embedded image:
    <img src="@Url.Content("~/NewsArea/Images/chart.png")" alt="clock" />
</div>
 
@section scripts
{
    @Scripts.Render("~/Plugin1/Scripts")
}
در اینجا نحوه‌ی تعریف فایل‌های CSS و JS ارائه شده‌ی توسط سیستم Bundling را مشاهده می‌کنید.
همچنین مسیر تصویر مشخص شده‌ی در آن، اینبار یک NewsArea اضافه‌تر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفته‌است اما می‌خواهیم این درخواست‌ها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.

اینبار اگر برنامه را اجرا کنیم، می‌توان به سه نکته در آن دقت داشت:


الف) alert اجرا شده از فایل js مدفون شده خوانده شده‌است.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شده‌است.
ج) تصویر نمایش داده شده، همان تصویر مدفون شده‌ی در فایل DLL برنامه است.
و هیچکدام از این فایل‌ها، به پوشه‌های پروژه‌ی اصلی برنامه، کپی نشده‌اند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
 MvcPluginMasterApp-Part2.zip
مطالب
دریافت زمانبندی شده به روز رسانی‌های آنتی ویروس Symantec به کمک کتابخانه‌های Quartz.NET و Html Agility Pack
در این رابطه آقای راد در دو قسمت به صورت مختصر و مفید این کتابخانه قدرتمند رو همراه با ارائه چندین مثال کاربردی معرفی کردند:
قسمت اول
قسمت دوم
در تکمیل قسمت‌های فوق بنده می‌خوام مثالی رو در این رابطه براتون بذارم، هدف از ارائه این مثال اتوماتیک سازی یک فرآیند روتین می‌باشد، به این صورت که در جایی که بنده مشغول به کار هستم یک سری لایسنس آنتی ویروس برای کلاینت‌ها در یک شبکه با مقیاس متوسط تهیه گردیده است، حال یک نسخه رایگان نیز برای کاربرانی که قصد دارند آنتی ویروس را برای سیستم شخصی خود نصب کنند نیز موجود می‌باشد که نیاز به آپدیت دارد معمولا آپدیت‌ها هر چند روز یکبار یا هر هفته در دو نسخه 64 و 32 بیتی ارائه می‌شوند، روال معمول برای دریافت آپدیت مراجعه به سایت و دانلود نسخه‌های مربوطه میباشد.

حال توسط کتابخانه قدرتمند Quartz.NET این فرآیند روتین را به صورت اتوماتیک می‌خواهیم انجام دهیم، استفاده از کتابخانه ذکر شده سخت نیست همانطور که در دو مطلب قبلی مرتبط ذکر گردیده، تنها پیاده سازی چندین اینترفیس است و بس.

namespace SymantecUpdateDownloader
{
    using System;
    using System.IO;
    using Quartz;
    using Quartz.Impl;
    using System.Globalization;
    public class TestJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            new Download().Scraping();
        }
    }
    public interface ISchedule
    {
        void Run();
    }
    public class TestSchedule : ISchedule
    {
        public void Run()
        {
            
            DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second);

            IJobDetail job = JobBuilder.Create<HelloJob>()
                                       .WithIdentity("job1")
                                       .Build();

            ITrigger trigger = TriggerBuilder.Create()
                                             .WithIdentity("trigger1")
                                             .StartAt(startTime)
                                             .WithDailyTimeIntervalSchedule(x => x.OnEveryDay().StartingDailyAt(new TimeOfDay(7, 0)).WithRepeatCount(0))
                                             .Build();

            ISchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sc = sf.GetScheduler();
            sc.ScheduleJob(job, trigger);

            sc.Start();
        }
    }
}
در این کد که همانند کدهای پیشنیازهای مطلب است، در خط 33 از متد WithDailyTimeIntervalSchedule استفاده شده است و همانطور که مشخص است وظیفه تعیین شده و هر روز ساعت 7 اجرا میشود.
مورد بعدی عملیات دانلود فایل می‌باشد که در ادامه مشاهده خواهید کرد، صفحه ایی که لینک فایل‌های دانلود را ارائه داده است دو نسخه مد نظر ما را در ابتدا لیست کرده است و با استفاده از web scrapingمی توانیم موارد تعیین شده را استخراج کنیم برای این منظور از کتابخانه htmlagilitypack استفاده میکنیم، تطبیق دو مورد(لینک) اول جهت دریافت نسخه‌های 32 و 64 بیتی به کمک Regular Expression میسر است و همانطور که در شکل زیر مشاهده میکنید از سمت چپ تاریخ به صورت 8 رقم، سه رقم قسمت دوم و ارقام و حروف قسمت سوم است به اضافه پسوند فایل مشخص است :


public class Download
    {
        static WebClient wc = new WebClient();
        static ManualResetEvent handle = new ManualResetEvent(true);
        
        private DateTime myDate = new DateTime();
        public void Scraping()
        {
            using (WebClient client = new WebClient())
            {
                client.Encoding = System.Text.Encoding.UTF8;
                var doc = new HtmlAgilityPack.HtmlDocument();
                ArrayList result = new ArrayList();
                doc.LoadHtml(client.DownloadString("https://www.symantec.com/security_response/definitions/download/detail.jsp?gid=savce"));
                var tasks = new List<Task>();
                foreach (var href in doc.DocumentNode.Descendants("a").Select(x => x.Attributes["href"]))
                {
                    if (href == null) continue;
                    string s = href.Value;
                    Match m = Regex.Match(s, @"http://definitions.symantec.com/defs/(\d{8}-\d{3}-v5i(32|64)\.exe)");
                    if (m.Success)
                    {
                        Match date = Regex.Match(m.Value, @"(\d{4})(\d{2})(\d{2})");
                        Match filename = Regex.Match(m.Value, @"\d{8}-\d{3}-v5i(32|64)\.exe");
                        int year = Int32.Parse(date.Groups[0].Value);
                        int month = Int32.Parse(date.Groups[1].Value);
                        int day = Int32.Parse(date.Groups[3].Value);

                        myDate = new DateTime(
                                Int32.Parse(date.Groups[1].Value),
                                Int32.Parse(date.Groups[2].Value),
                                Int32.Parse(date.Groups[3].Value));
                        if (myDate == DateTime.Today)
                        {
                            tasks.Add(DownloadUpdate(m.Value, filename.Value));
                        }
                        else
                        {
                            MessageBox.Show("امروز آپدیت موجود نیست");
                        }
                    }
                }
                DownloadTask = Task.WhenAll(tasks);
            }
        }
        private static Task DownloadTask;
        private Task DownloadUpdate(string url, string fileName)
        {
            var wc = new WebClient();
            return wc.DownloadFileTaskAsync(new Uri(url), @"\\10.1.0.15\SymantecUpdate\\" + fileName);
        }

    }
توضیح کدهای فوق :
ابتدا توسط متد LoadHtml خط 14 صفحه مورد نظر که حاوی لینک‌ها می‌باشد رو Load میکنیم، سپس توسط یک حلقه foreach خط 16 مقدار خصوصیت href تمام لینک‌های موجود  در صفحه را استخراج میکنیم مثلا مقدار خصوصیت href در لینک‌ها به صورت  زیر می‌باشد :
 
http://definitions.symantec.com/defs/20130622-007-v5i32.exe

http://definitions.symantec.com/defs/20130622-007-v5i64.exe

همانطور که مشخص است در دو مورد فوق تنها نام فایل متفاوت می‌باشد، همانطور که بحث شد برای نام فایل‌ها هم می‌توانیم یک Pattern را به صورت زیر داشته باشیم :
(\d{8}-\d{3}-v5i(32|64)\.exe)
در خط 20 نیز عملیات تطبیق تمام href‌های موجود در صفحه را توسط Regular Expression فوق تطبیق می‌دهیم، اگر تطبیق با موفقیت انجام پذیرفت باید نام فایل و همچنین تاریخ موجود در نام فایل را نیز توسط دو Regular Expression استخراج کنیم(خط 23 و 24) در ادامه برای جدا کردن مقادیر سال ، ماه ، روز از امکان Groups در RegEx استفاده کرده ایم:
int year = Int32.Parse(date.Groups[0].Value);
int month = Int32.Parse(date.Groups[1].Value);
int day = Int32.Parse(date.Groups[3].Value);
در ادامه تاریخ استخراج شده را با تاریخ روز جاری مقایسه می‌کنیم اگر مساوی بود عملیات دانلود فایل‌ها توسط یک Task تعریف شده به صورت همزمان بر روی سرور مربوطه دانلود می‌شوند.
البته لازم به ذکر است که کدهای فوق مسلما نیاز یه Refactoring دارند منتها هدف از ارائه این مثال آشنایی بیشتر با کتابخانه‌های فوق می‌باشد.
نکته آخر اینکه برنامه فوق به حالت‌های مختلفی می‌تواند اجرا گردد مثل یک برنامه وب  یا یک سرویس ویندوزی و ... ، بهترین حالت یک سرویس ویندوز می‌باشد، ولی در حالت خام در حال حاضر یک ویندوز اپلیکیشن ساده می‌باشد که بر روی سرور RUN شده است که در آینده به صورت یک سرویس ویندوز ارائه خواهد شد.