مطالب
نمایش اشیاء موجود در View بر اساس دسترسی‌ها در ASP.NET MVC
سیستم دسترسی در یک سیستم، همیشه برای من چالش برانگیز بوده است. با دیدن کدهای مختلف از افراد مختلف، شیوه‌های گوناگونی از کدنویسی را دیده‌ام؛ ولی یکی از نکاتی که در بین آن‌ها بررسی نشده بود و یا از آن غافل مانده بودند، بررسی بعضی از عناصر موجود در ویو بود که باید با توجه به نقش کاربر سیستم، وضعیت آن بررسی میشد. 
برای مثال تصور کنید که شما دو کاربر دارید که هر دو سطح دسترسی به پروفایل کاربران دیگر را دارند. ولی یکی از کاربرها این توانایی را دارد تا با کلیک بر روی دکمه‌ای، لاگین کاربر مورد نظر را مسدود نماید، ولی دیگری نمی‌تواند این دکمه را ببیند، یا برای او به صورت غیرفعال نمایش داده شود. در اکثر این مواقع کاری که برنامه نویسان انجام میدهند، نوشتن توابع به شیوه‌های گوناگون در لابلای انبوهی از کدهای View هست تا بتوانند این مسئله را پیاده سازی کنند. ولی با اضافه شدن هر چه بیشتر این عناصر به صفحه و با توجه به انبوهی از ویووها و ویووکامپوننت‌ها کار بسیار سخت‌تر میشود.
 به همین جهت من از فیلترها در MVC کمک گرفتم و این موضوع را با توجه به خروجی نهایی صفحه بررسی میکنم. به این معنا که وقتی صفحه بدون هیچ گونه اعتبارسنجی در سطح ویو آماده شد، با استفاده از فیلتر مورد نظر که به صورت سراسری اضافه شده است، بررسی میکنم که آیا این کاربر حق دارد بعضی از المان‌ها را ببیند یا خیر؟ در صورتیکه برای هر المان موجود در صفحه اعتباری نداشته باشد، آن المان از صفحه حذف و یا غیرفعال میشود.
ابتدا یک صفحه را با المان‌های زیر میسازیم. از آنجا که بیشتر تگ‌های عملیاتی از نوع لینک و دکمه هستند، از هر کدام، سه عنصر به صفحه اضافه میکنیم:
<button data-perm="true" data-controller="c" data-action="r" data-method="post" data-type="disable">Test 1</button>
<button data-perm="true" data-controller="c" data-action="t" data-method="post">Test 2</button>
<button data-perm="false" data-controller="c" data-action="r" data-method="post">Test 3</button>
<a data-perm="true" data-controller="c" data-action="m" data-method="post">Test 4</a>
<a data-perm="false" data-controller="c" data-action="m" data-method="post">Test 5</a>
<a data-perm="true" data-controller="c" data-action="t" data-method="post">Test 6</a>
در اینجا ما از همان ساختار آشنای *-data استفاده میکنیم و معنی هر کدام از ویژگی‌ها برابر زیر است:
ویژگی
توضیحات
 perm   آیا نیاز به اعتبارسنجی دارد یا خیر؟ در صورتی که المانی مقدار Perm آن با مقدار true پر گردد، اعتبارسنجی روی آن اعمال خواهد شد.   
 controller   نام کنترلری که به آن دسترسی دارد.  
 action  نام اکشنی که به آن در کنترلر ذکر شده دسترسی دارد. 
 method   در صورتیکه دسترسی get و post و ... هر یک متفاوت باشد.  
 type  نحوه برخورد با المان غیرمجاز. در صورتیکه با disable مقداردهی شود، المان غیرفعال و در غیر اینصورت، از روی صفحه حذف میشود.

سپس یک کلاس جدید ساخته و با ارث بری از ActionFilterAttribute، کار ساخت فیلتر را آغاز میکنیم:

    public class AuthorizePage: ActionFilterAttribute
    {
        private HtmlTextWriter _htmlTextWriter;
        private StringWriter _stringWriter;
        private StringBuilder _stringBuilder;
        private HttpWriter _output;
        IAuthorization _auth;
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {        
            _stringBuilder = new StringBuilder();
            _stringWriter = new StringWriter(_stringBuilder);
            _htmlTextWriter = new HtmlTextWriter(_stringWriter);
            _output = (HttpWriter) filterContext.RequestContext.HttpContext.Response.Output;
            filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter;
            _auth = new Auth();
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var response = _stringBuilder.ToString();

            response = AuthorizeTags(response);
            _output.Write(response);
        }

        public string AuthorizeTags(string response)
        {

            var doc = GetHtmlDocument(response);
            var nodes=doc.DocumentNode.SelectNodes("//*[@data-perm]");

            if (nodes == null)
                return response;

            foreach(var node in nodes)
            {
                var dataPermission = node.Attributes["data-perm"];
                if(!dataPermission.Value.TryBooleanParse())
                {
                    continue;
                }

                var controller = node.Attributes["data-controller"].Value;
                var action = node.Attributes["data-action"].Value;
                var method = node.Attributes["data-method"].Value;

                var access=_auth.Authorize(HttpContext.Current.User.Identity.Name , controller, action, method);

                if (access)
                    continue;

                var removeElm = true;
                var type = node.Attributes["data-type"]?.Value;
                if (type!=null && type.ToLower()== "disable")
                {
                    removeElm = false;
                }

                if(removeElm)
                {
                    node.Remove();
                    continue;
                }


                node.Attributes.Add("disabled", "true");
            }

            return doc.DocumentNode.OuterHtml;
        }

        private HtmlDocument GetHtmlDocument(string htmlContent)
        {
            var doc = new HtmlDocument
            {
                OptionOutputAsXml = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(htmlContent);

            return doc;
        }

    }
در خطوط اولیه، از متد OnActionExecuting به عنوان یک سازنده برای پر کردن در لحظه هر درخواست استفاده میکنیم. htmlTextWriter که یک پیاده سازی از TextWriter هست، برای نوشتن بهینه کدهای HTML سودمند میباشد و از آن جهت که نیاز زیادی برای کار با رشته‌ها دارد، از StringBuilder در پشت صحنه استفاده می‌کند. جهت آشنایی بیشتر با این کلاس‌ها، مطلب StringBuilder و قسمت نظراتش را مطالعه بفرمایید. همچنین یک httpWriter هم برای ایجاد خروجی در اکشن مورد نظر نیازمندیم و مورد آخر یک اینترفیس میباشد که جهت اعتبارسنجی مورد استفاده قرار میگیرد و این اینترفیس میتواند از طریق تزریق وابستگی‌ها پر شود:
  public interface IAuthorization
    {
        bool Authorize(string userId, string controller, string action, string method);
    }

ادامه کد در متد OnResultExecuted قرار دارد و متد اصلی کار ما میباشد. این متد بعد از صدور خروجی از اکشن، صدا زده شده اجرا میشود و شامل خروجی اکشن میباشد. خروجی اکشن را به متدی به نام AuthorizeResponse داده و با استفاده از بسته htmlagilitypack که یک HTML Parser میباشد، کدهای HTML را تحلیل میکنیم. قاعده فیلترسازی المان‌ها در این کتابخانه بر اساس قواعد تعریف شده در XPath میباشد. بر اساس این قاعده ما گفتیم هر نوع تگی که دارای ویژگی data-perm میباشد، باید به عنوان گره‌های فیلتر شده برگشت داده شود. سپس مقادیر نام کنترلر و اکشن و ... از المان دریافت شده و با استفاده از اینترفیسی که ما اینجا تعریف کرده‌ایم، بررسی میکنیم که آیا این کاربر به این موارد دسترسی دارد یا خیر. در صورتیکه پاسخ برگشتی، از عدم اعتبار کاربر بگوید، گره مورد نظر حذف و یا در صورتیکه ویژگی data-type وجود داشته و مقدارش برابر disable باشد، آن المان غیرفعال خواهد شد. در نهایت کد تولیدی سند را به رشته تبدیل کرده و جایگزین خروجی فعلی میکنیم.
جهت تعریف سراسری آن در Global.asax داریم:
protected void Application_Start()
{
  GlobalFilters.Filters.Add(new AuthorizePage());
}
مطالب
ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF
طی این  مقاله، نحوه‌ی ذخیره سازی تنظیمات متغیر و پویای یک برنامه را به صورت Strongly Typed ارائه خواهم داد. برای این منظور، یک API را که از Lazy Loading ، Cache ، Reflection و Entity Framework بهره میگیرد، خواهیم ساخت.
برنامه‌ی هدف ما که از این API استفاده می‌کند، یک اپلیکیشن Asp.net MVC است. قبل از شروع به ساخت API مورد نظر، یک دید کلی در مورد آنچه که قرار است در نهایت توسعه یابد، در زیر مشاهده میکنید:
public SettingsController(ISettings settings)
{
  // example of saving 
  _settings.General.SiteName = "دات نت تیپس";
  _settings.Seo.HomeMetaTitle = ".Net Tips";
  _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection";
  _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";
  _settings.Save();
}

همانطور که در کدهای بالا مشاهده میکنید، شی setting_ ما دارای دو پراپرتی فقط خواندنی بنام‌های General و Seo است که شامل  تنظیمات مورد نظر ما هستند و این دو کلاس از کلاس پایه‌ی SettingBase ارث بری کرده‌اند. دو دلیل برای انجام این کار وجود دارد:
  1. تنظیمات به صورت گروه بندی شده در کنار  هم قرار گرفته‌اند و یافتن تنظیمات برای زمانی که نیاز به دسترسی  به آنها داریم، راحت‌تر و ساده‌تر خواهد بود. 
  2. به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.

اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟ 

شاید فکر کنید چرا باید تنظیمات را در دیتابیس ذخیره کنیم در حالی که فایل web.config در درسترس است و می‌توان توسط کلاس ConfigurationManager به اطلاعات آن دسترسی داشت.
جواب: دلیل این است که با تغییر فایل web.config، برنامه‌ی وب شما ری استارت خواهد شد (چه زمان‌هایی یک برنامه Asp.net ری استارت میشود).
برای جلوگیری از این مساله، راه حل مناسب برای ذخیره سازی اطلاعاتی که نیاز به تغییر در زمان اجرا دارند، استفاده از از دیتابیس می‌باشد. در این مقاله از Entity Framework و پایگاه داده Sql Sever استفاده می‌کنم.

مراحل ساخت Setting API مورد نظر به شرح زیر است:
  1. ساخت یک Asp.net Web Application 
  2. ساخت مدل Setting و افزودن آن به کانتکست Entity Framework 
  3. ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
  4. ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کرده‌اند.
  5. ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات 

یک برنامه‌ی Asp.Net Web Application را از نوع MVC ایجاد کنید. تا اینجا مرحله‌ی اول ما به پایان رسید؛ چرا که ویژوال استودیو کار‌های مورد نیاز ما را انجام خواهد داد.
 لازم است مدل خود را به ApplicationDbContext موجود در فایل IdentityModels.cs معرفی کنیم. به شکل زیر:
namespace DynamicSettingAPI.Models
{
    public interface IUnitOfWork
    {
        DbSet<Setting> Settings { get; set; }
        int SaveChanges();
    }
} 

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork
    {
        public DbSet<Setting> Settings { get; set; }
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }


namespace DynamicSettingAPI.Models
{
    public class Setting
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string Value { get; set; }
    }
}
مدل تنظیمات ما خیلی ساده است و دارای سه پراپرتی به نام‌های Name ، Type ، Value هست که به ترتیب برای دریافت مقدار تنظیمات، نام کلاسی که از کلاس SettingBase ارث برده و نام تنظیمی که لازم داریم ذخیره کنیم، در نظر گرفته شده‌اند. 
لازم است تا متد OnModelCreating مربوط به ApplicationDbContext را نیز تحریف کنیم تا کانفیگ مربوط به مدل خود را نیز اعمال نمائیم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Setting>()
                    .HasKey(x => new { x.Name, x.Type });

            modelBuilder.Entity<Setting>()
                        .Property(x => x.Value)
                        .IsOptional();

            base.OnModelCreating(modelBuilder);
        }
ساختاری به شکل زیر مد نظر ماست:

  کلاس SettingBase ما همچین ساختاری را خواهد داشت:
namespace DynamicSettingAPI.Service
{
    public abstract class SettingsBase
    {
        //1
        private readonly string _name;
        private readonly PropertyInfo[] _properties;

        protected SettingsBase()
        {
            //2
            var type = GetType();
            _name = type.Name;
            _properties = type.GetProperties();
        }

        public virtual void Load(IUnitOfWork unitOfWork)
        {
            //3 get setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                //get the setting from setting list
                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    //4 set 
                    propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
                }
            }
        }
        public virtual void Save(IUnitOfWork unitOfWork)
        {
            //5 get all setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                var propertyValue = propertyInfo.GetValue(this, null);
                var value = (propertyValue == null) ? null : propertyValue.ToString();

                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    // 6 update existing value
                    setting.Value = value;
                }
                else
                {
                    // 7 create new setting
                    var newSetting = new Setting()
                    {
                        Name = propertyInfo.Name,
                        Type = _name,
                        Value = value,
                    };
                    unitOfWork.Settings.Add(newSetting);
                }
            }
        }
    }
}
این کلاس قرار است توسط کلاس‌های تنظیمات ما به ارث برده شود و در واقع کارهای مربوط به رفلکشن را در این کلاس کپسوله کرده‌ایم. همانطور که مشخص است ما دو فیلد را به نام‌های name_ و properties_ به صورت فقط خواندنی در نظر گرفته ایم که نام کلاس مورد نظر ما که از این کلاس به ارث خواهد برد، به همراه پراپرتی‌های آن، در این ظرف‌ها قرار خواهند گرفت.
متد Load وظیفه‌ی واکشی تمام تنظیمات مربوط به Type و ست کردن مقادیر به دست آمده را به خصوصیات کلاس ما، برعهده دارد. کد زیر مقدار دریافتی از دیتابیس را به نوع داده پراپرتی مورد نظر تبدیل کرده و نتیجه را به عنوان Value پراپرتی ست میکند. 
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
متد Save نیز وظیفه‌ی ذخیره سازی مقادیر موجود در خصوصیات کلاس تنظیماتی را که از کلاس SettingBase ما به ارث برده است، به عهده دارد. 
این متد دیتا‌های موجود دردیتابیس را که متعلق به کلاس ارث برده مورد نظر ما هستند، واکشی میکند و در یک حلقه، اگر خصوصیتی در دیتابیس موجود بود، آن را ویرایش کرده وگرنه یک رکورد جدید را ثبت میکند.

  کلاس‌های تنظیمات شخصی سازی شده خود را به شکل زیر تعریف میکنیم :
  public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
        public bool RegisterUsersEnabled { get; set; }
    }

 public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
    }
نیازی به توضیح ندارد.
برای اینکه تنظیمات را به صورت یکجا داشته باشیم و Abstraction ای را برای استفاده از این API ارائه دهیم، یک اینترفیس و یک کلاس که اینترفیس مذکور را پیاده کرده است در نظر میگیریم: 
public interface ISettings
{
    GeneralSettings General { get; }
    SeoSettings Seo { get; }
    void Save();
}

public class Settings : ISettings
{
    // 1
    private readonly Lazy<GeneralSettings> _generalSettings;
    // 2
    public GeneralSettings General { get { return _generalSettings.Value; } }

    private readonly Lazy<SeoSettings> _seoSettings;
    public SeoSettings Seo { get { return _seoSettings.Value; } }

    private readonly IUnitOfWork _unitOfWork;
    public Settings(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        // 3
        _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>);
        _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>);
    }

    public void Save()
    {
        // only save changes to settings that have been loaded
        if (_generalSettings.IsValueCreated)
            _generalSettings.Value.Save(_unitOfWork);

        if (_seoSettings.IsValueCreated)
            _seoSettings.Value.Save(_unitOfWork);

        _unitOfWork.SaveChanges();
    }
    // 4
    private T CreateSettings<T>() where T : SettingsBase, new()
    {
        var settings = new T();
        settings.Load(_unitOfWork);
        return settings;
    }
}
این اینترفیس مشخص می‌کند که ما به چه نوع تنظیماتی، دسترسی داریم و متد Save آن برای آپدیت کردن تنظیمات، در نظر گرفته شده است. هر کلاسی که از کلاس SettingBase ارث بری کرده را به صورت فیلد فقط خواندنی و با استفاده از کلاس Lazy درون آن ذکر میکنیم و به این صورت کلاس تنظیمات ما زمانی ساخته خواهد شد که برای اولین بار به آن دسترسی داشته باشیم.
متد CreateSetting وظیفه‌ی لود دیتا را از دیتابیس، بر عهده دارد که برای این منظور، متد لود Type مورد نظر را فراخوانی میکند. این متد وقتی به کلاس تنظیمات مورد نظر برای اولین بار دسترسی پیدا کنیم، فراخوانی خواهد شد.

 حتما امکان این وجود دارد که شما از امکان Caching هم بهره ببرید برای مثال همچین متد و سازنده‌ای را در کلاس Settings در نظر بگیرید:
private readonly ICache _cache;
public Settings(IUnitOfWork unitOfWork, ICache cache)
{
    // ARGUMENT CHECKING SKIPPED FOR BREVITY
    _unitOfWork = unitOfWork;
    _cache = cache;
    _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>);
    _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>);
}

private T CreateSettingsWithCache<T>() where T : SettingsBase, new()
{
    // this is where you would implement loading from ICache
    throw new NotImplementedException();
}
در آخر هم به شکل زیر میتوان (به عنوان دمو فقط ) از این API استفاده کرد.
   public ActionResult Index()
        {
            using (var uow = new ApplicationDbContext())
            {
                var _settings = new Settings(uow);
                _settings.General.SiteName = "دات نت تیپس";
                _settings.General.AdminEmail = "admin@gmail.com";
                _settings.General.RegisterUsersEnabled = true;
                _settings.Seo.HomeMetaTitle = ".Net Tips";
                _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection";
                _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";

                var settings2 = new Settings(uow);
                var output = string.Format("SiteName: {0} HomeMetaDescription: {1}  MetaKeywords:  {2}  MetaTitle:  {3}  RegisterEnable:  {4}",
                    settings2.General.SiteName,
                    settings2.Seo.HomeMetaDescription,
                    settings2.Seo.MetaKeywords,
                    settings2.Seo.HomeMetaTitle,
                    settings2.General.RegisterUsersEnabled.ToString()
                    );
                return Content(output);
            }

        }

خروجی :

نکته: در پروژه ای که جدیدا در سایت ارائه داده‌ام و در حال تکمیل آن هستم، از بهبود یافته‌ی این مقاله استفاده می‌شود. حتی برای اسلاید شو‌های سایت هم میشود از این روش استفاده کرد و از فرمت json بهره برد برای این منظور. حتما در پروژه‌ی مذکور همچین امکانی را هم در نظر خواهم گرفتم.
پیشنها میکنم سورس SmartStore را بررسی کنید. آن هم به شکل مشابهی ولی پیشرفته‌تر از این مقاله، همچین امکانی را دارد.
نظرات مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامه‌های وب
یک نکته‌ی تکمیلی: امکان تزریق وابستگی‌های سرویس‌های سفارشی، در سازنده‌ی کلاس Startup برنامه‌های وب


اگر به سازنده‌ی پیش‌فرض کلاس Startup یک برنامه‌ی وب دقت کنید، چنین تزریق وابستگی در قالب ابتدایی آن وجود دارد:
public class Startup 
{ 
   public Startup(IConfiguration configuration) 
   { 
       Configuration = configuration; 
   }
در اینجا ممکن است چند سؤال مطرح شوند:
الف) چه سرویس‌های پیش‌فرض دیگری را نیز می‌توان در اینجا تزریق کرد؟
ب) آیا می‌توان سرویس‌های سفارشی تهیه شده‌ی توسط خودمان را نیز در اینجا تزریق کرد؟

الف) بر روی ابتدای متد ConfigureServices کلاس Startup یک break-point را قرار دهید. لیست پارامتر services آن، شامل سرویس‌های پیش‌فرضی است که قابلیت تزریق وابستگی‌ها را در سازنده‌ی این کلاس دارند و بیش از 40 کلاس هستند.

ب) برای این منظور به فایل Program.cs مراجعه کرده و سرویس سفارشی خود را به صورت زیر، توسط متد ConfigureServices آن، اضافه کنید:
using CoreIocServices;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace CoreIocSample02
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .ConfigureServices(serviceCollection =>
            {
                serviceCollection.AddScoped<ISomeService, SomeService>();
            })
            .UseStartup<Startup>();
    }
}
اکنون ISomeService سفارشی ما قابلیت تزریق در سازنده‌ی کلاس Startup را نیز پیدا کرده‌است (علاوه بر سایر نقاط برنامه):
namespace CoreIocSample02
{
    public class Startup
    {
        private readonly ISomeService _someService;

        public Startup(IConfiguration configuration, ISomeService someService)
        {
            Configuration = configuration;
            _someService = someService;
        }

        public IConfiguration Configuration { get; }
مطالب
لینک‌های هفته‌ی آخر بهمن

وبلاگ‌ها ، سایت‌ها و مقالات ایرانی (داخل و خارج از ایران)

امنیت

Visual Studio

ASP. Net

طراحی و توسعه وب

اس‌کیوال سرور

سی شارپ

عمومی دات نت

مسایل اجتماعی و انسانی برنامه نویسی

کتاب‌های رایگان جدید

متفرقه
مطالب
ایده‌ی ثبت خودکار سرویس‌ها، به همراه تنظیمات؛ بدون نوشتن هیچ کدی در ConfigureServices با روش Installer
خودکارسازی، در قسمت‌های مختلف یک پروژه می‌تواند انجام شود. نمونه‌های مختلف این خودکارسازی‌ها که اکثرا توسط رفلکشن انجام می‌شوند شامل نگاشت خودکار Dto به Entity و بالعکس (توسط AutoMapper)، ثبت خودکار تمام Entityها در DbContext بدون نیاز به ثبت تک تک آن‌ها به صورت  public DbSet<Person> People { get; set; }  (که در این روش خودکار، اسم جداول می‌تواند به صورت جمع ثبت شود)، ثبت خودکار EntityTypeConfigurationها، ثبت خودکار کلیه‌ی کلاس‌های Profile برای کانفیگ AutoMapper و رجیستر خودکار DI سرویس‌ها، تا نیازی به نوشتن کدهای تکراری و مشابه   ;()<services.AddTransient<IUserService, UserService نداشته باشیم. 
 برای مشاهده‌ی عملی پیاده‌سازی این نمونه‌ها می‌توانید به پروژه‌ی ASP.NET Core WebAPI مراجعه کنید. در این مقاله می‌خواهیم همین سناریو را برای ثبت سرویس‌هایمان در متد ConfigureServices انجام دهیم، تا نیازی به نوشتن هیچ کدی برای آن‌ها نداشته باشیم. 

ثبت سرویس‌های مختلف، به همراه تنظیمات آن‌ها (مانند Authentication، Swagger، DbContext، ApiVersioning و ...) در استارتاپ می‌تواند به چندین صورت انجام شود.
روش اول اینکه به صورت دستی تمام کدهای مربوط به رجیستر کردن سرویس‌ها و تنظیمات آن‌ها، در متد ConfigureServices نوشته شود که خیلی جالب نیست و موجب شلوغ شدن سریع این متد می‌شود. نمونه‌ی این شیوه را برای ثبت سرویس مربوط به DbContext می‌بینیم:
public void ConfigureServices(IServiceCollection services) {
      // DbContext Service
      services.AddDbContext<AppDbContext>(options =>
            {
                options
             .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {     sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });

      // register other services ....

}


راه دوم روش استفاده از متدهای الحاقی است؛ طوریکه برای هر سرویس، یک متد الحاقی را تعریف کنیم و از آن، در این متد استفاده کنیم که حجم کدها را تا حد زیادی کم می‌کند. برای مثال ثبت سرویس بالا را می‌توانیم در کلاس دیگری با نام DbContextServiceCollectionExtensions.cs ثبت کنیم:
public static class DbContextServiceCollectionExtensions
    {
        public static void AddDbContext(this IServiceCollection services)
        {
              services.AddDbContext<AppDbContext>(options =>
            {
                options
                    .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {
                    sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });
        }
    }
و سپس در متد ConfigureServices می‌توان آن را به صورت زیر استفاده کرد:
public void ConfigureServices(IServiceCollection services) {
// Add DbContext
 services.AddDbContext();

//.... Register other services
}

ولی اگه پروژه‌ی ما متوسط به بالا باشد، کم‌کم تعداد سرویس‌های ما زیاد می‌شود (برای مثال چند نمونه از سرویس‌های رایج مورد استفاده، شامل سرویس‌های لاگ خطاها مثل Elmah و سرویس HttpClientFactory و AutoRegisterDi (توضیح در ادامه مقاله) و AutoMapper و Cache و EFSecondLevelCache و Hangfire و ....) می‌بینیم که تعداد این سرویس‌ها هم زیاد است و حتی به صورت اکستنشن هم به مرور زمان باعث شلوغ شدن استارتاپ می‌شوند. ضمن اینکه یک کار تکراری است که باید هر بار انجام شود.

راه سوم ثبت سرویس، استفاده از یک اینترفیس به نام IServiceInstaller و استفاده از آن در کلاس‌های مختلف مربوط به ثبت سرویس و بعد خواندن خودکار این تنظیمات با یک خط کد ساده‌ی رفلکشن است که در ادامه می‌بینیم: 
اینترفیس IServiceInstaller را تعریف می‌کنیم: 
public interface IServiceInstaller
    {
        void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly);
    }
توضیح: پارامتر appSettings کلاسی شامل کلیه‌ی مقادیر فایل appsettings.json است. شما می‌توانید بجای آن از IConfiguration استفاده کنید و مقدار آن را در Startup پاس دهید. پارامتر آخر برای حالتی است که این فایل‌ها را در لایه‌ی دیگری به غیر از لایه‌ی اصلی Api (مثل لایه‌ی WebFamewrk) پیاده سازی می‌کنید.
سپس کلاس‌های ثبت سرویس‌هایمان را با ارث بری از این اینترفیس می‌سازیم. برای نمونه رجیستر DbContext را با ایجاد کلاسی به نام DbContextInstaller انجام می‌دهیم:
public class DbContextInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            services.AddDbContext<AppDbContext>(options =>
            {
                options
               .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {
                    sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });
        }
    }

حالا برای ثبت این کلاس و کلاس‌های مشابه Installer، می‌آییم یک متد الحاقی را برای متد ConfigureServices می‌نویسیم که در آن از رفلکشن استفاده می‌کنیم: 
public static class ServiceInstallerExtensions
    {
        public static void InstallServicesInAssemblies(this IServiceCollection services, AppSettings appSettings)
        {
            var startupProjectAssembly = Assembly.GetCallingAssembly();
            var assemblies = new[] { startupProjectAssembly, Assembly.GetExecutingAssembly() };
            var installers = assemblies.SelectMany(a => a.GetExportedTypes())
                .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(IServiceInstaller).IsAssignableFrom(c))
                .Select(Activator.CreateInstance).Cast<IServiceInstaller>().ToList();
            installers.ForEach(i => i.InstallServices(services, appSettings, startupProjectAssembly));
        }
    }

در نهایت متد ConfigureServices ما به صورت زیر خواهد بود (بعد از اضافه کردن تمام سرویس‌ها!):
public void ConfigureServices(IServiceCollection services)
        {
            //* HttpContextAccessor
            // services.AddHttpContextAccessor();

            //* Controllers
            services.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); })
                .AddNewtonsoftJson();

            //* Installers
            services.InstallServicesInAssemblies(_appSettings);
        }
کار تمام شد. حالا تمام سرویس‌های ما با ایجاد کلاس مرتبط و implement شدن از اینترفیس IServiceInstaller، به طور خودکار در استارتاپ و متد ConfigureServies ثبت خواهند شد.

فقط یک نکته آخر اینکه برای رجیستر خودکار DI سرویس‌ها (و ننوشتن کدهایی مانند   ()<services.AddTransient<IUserService, UserService برای رجیستر هر سرویس) می‌توانیم از Autofac استفاده کنیم (در پروژه‌ی بالا آمده است) و یا از پکیج AutoRegisterDi استفاده کنیم (متعلق به Jon P Smith) که از خود Container داخلی Core استفاده می‌کند و از Autofac سبکتر است. کلاسی می‌سازیم به نام RegisterServicesUsingAutoRegisterDiInstaller: 
public class RegisterServicesUsingAutoRegisterDiInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            var dataAssembly = typeof(SomeRepository).Assembly;
            var serviceAssembly = typeof(SomeService).Assembly;
            var webFrameworkAssembly = Assembly.GetExecutingAssembly();
            var startupAssembly = startupProjectAssembly;
            var assembliesToScan = new[] { dataAssembly, serviceAssembly, webFrameworkAssembly, startupAssembly };

            #region Generic Type Dependencies
            services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
            #endregion

            #region Scoped Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(IScopedDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Scoped);
            #endregion

            #region Singleton Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Singleton);
            #endregion

            #region Transient Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(ITransientDependency)))
                .AsPublicImplementedInterfaces(); // Default is Transient
            #endregion

            #region Register DIs By Name
            services.RegisterAssemblyPublicNonGenericClasses(dataAssembly)
                .Where(c => c.Name.EndsWith("Repository")
                            && !c.GetInterfaces().Contains(typeof(ITransientDependency))
                            && !c.GetInterfaces().Contains(typeof(IScopedDependency))
                            && !c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Scoped);

            services.RegisterAssemblyPublicNonGenericClasses(serviceAssembly)
                .Where(c => c.Name.EndsWith("Service")
                            && !c.GetInterfaces().Contains(typeof(ITransientDependency))
                            && !c.GetInterfaces().Contains(typeof(IScopedDependency))
                            && !c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces();
            #endregion
        }
    }
 (رجیستر در اینجا با اولویت اینترفیس‌های ITransiantDependency، IScopedDependency، ISingletonDependency و سپس اتمام نام سرویس با کلمه‌های Repository و Service انجام می‌شود که شما می‌توانید با منطق و نیاز خودتان آن‌ها را تغییر دهید)
اشتراک‌ها
دوره 3 روزه Rust از تیم اندروید

Comprehensive Rust 🦀 

This is a three day Rust course developed by the Android team. The course covers the full spectrum of Rust, from basic syntax to advanced topics like generics and error handling. It also includes Android-specific content on the last day. 

دوره 3 روزه Rust از تیم اندروید
اشتراک‌ها
کالبدشکافی محدودیت های جنریک جدید در C# 7.3

During the last Build conference, Microsoft has announced the next version of Visual Studio with C# 7.3 support. This is yet another minor language update with some quite interesting features. The main change was related to generics, starting from C# 7.3 there 3 more constraints: unmanaged, System.Enum and System.Delegate. 

کالبدشکافی محدودیت های جنریک جدید در C# 7.3
نظرات مطالب
بهبود صفحه‌‌ی بارگذاری اولیه در Blazor WASM
نمایش درصد بارگذاری اولیه‌ی یک برنامه‌ی Blazor WASM در دات نت 7

اگر یک برنامه‌ی جدید blazor wasm را در دات نت 7، برای مثال با دستور dotnet new blazorwasm --hosted ایجاد کنیم، با اجرای آن، یک progress-bar حلقوی نمایش میزان درصد بارگذاری اولیه‌ی برنامه ظاهر می‌شود که به نوعی پیاده سازی توکار نکات مطلب جاری است. این پیاده سازی از اجزای زیر تشکیل شده‌است:
الف) تغییرات فایل index.html برنامه
برای این منظور، فایل Client\wwwroot\index.html به صورت زیر تغییر کرده‌است:
<body>
    <div id="app">
        <svg class="loading-progress">
            <circle r="40%" cx="50%" cy="50%" />
            <circle r="40%" cx="50%" cy="50%" />
        </svg>
        <div class="loading-progress-text"></div>
    </div>
که در اینجا progress-bar حلقوی را با یک طرح SVG ایجاد کرده‌اند.

ب) تغییرات فایل app.css برنامه
کلاس‌های progress-bar را به این صورت در فایل Client\wwwroot\css\app.css اضافه کرده‌اند:
.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto 1rem auto;
}
    .loading-progress circle {
        fill: none;
        stroke: #e0e0e0;
        stroke-width: 0.6rem;
        transform-origin: 50% 50%;
        transform: rotate(-90deg);
    }
        .loading-progress circle:last-child {
            stroke: #1b6ec2;
            stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
            transition: stroke-dasharray 0.05s ease-in-out;
        }
.loading-progress-text {
    position: absolute;
    text-align: center;
    font-weight: bold;
    inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
    .loading-progress-text:after {
        content: var(--blazor-load-percentage-text, "Loading");
    }

روش سفارشی سازی این progress-bar بر اساس دو CSS variable فوق صورت می‌گیرد:
--blazor-load-percentage: درصد بارگذاری جاری را مقدار دهی می‌کند.
--blazor-load-percentage-text: متن Loading نمایش داده شده را مشخص می‌کند.

برای مثال اگر علاقمند باشیم بجای SVG پیش‌فرض از progress-bar توکار خود فریم‌ورک بوت‌استرپ استفاده کنیم، روش کار به صورت زیر خواهد بود:
<body>
    <div id="app">
      <div class="progress">
        <div class="progress-bar progress-bar-striped progress-bar-animated" 
            role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" 
            style="width: var(--blazor-load-percentage, 0%)">
            <div class="loading-text"></div>
        </div>        
      </div>
که در اینجا همان CSS variable معادل درصد بارگذاری، بجای width استفاده شده تا به صورت خودکار سبب پیشرفت progress-bar شود. همچنین کلاس جدید loading-text را هم همانند loading-progress-text:after موجود به صورت زیر به فایل app.css اضافه می‌کنیم تا سبب نمایش متن درصد پیشرفت جاری نیز شود:
.loading-text:after {
        content: var(--blazor-load-percentage-text, "Loading");
    }

مطالب
نمایش تعداد کل صفحات در iTextSharp

در مورد نحوه‌ی نمایش شماره صفحه جاری در مثلا header یک گزارش PDF تهیه شده به کمک writer.PageNumber و ارث بری از کلاس PdfPageEventHelper،‌ در پایان مطلب فارسی نویسی در iTextSharp توضیح داده شد. این مورد جزو ضروریات یک گزارش خوب است، اما عموما نیاز است تا تعداد کل صفحات هم نمایش داده شود. مثلا صفحه n از 100 جایی در تمام صفحات درج شود و ... هیچ خاصیتی به نام TotalNumberOfPages را در این کتابخانه نمی‌توان یافت. علت هم این است که تعداد واقعی کل صفحات فقط در حین بسته شدن شیء Document مشخص می‌شود و نه در هنگام تهیه صفحات. بنابراین نکته تهیه و نمایش تعداد صفحات، در iTextSharp به صورت خلاصه به شرح زیر است:
الف) باید در همان کلاسی که از PdfPageEventHelper به ارث رسیده است، متد OnCloseDocument را تحریف (override) کرد. در اینجا به خاصیت writer.PageNumber دسترسی داریم و writer.PageNumber - 1 مساوی است با تعداد کل صفحات.
ب) در مرحله بعد نیاز است تا این عدد را به نحوی به تمام صفحات تولید شده اضافه کنیم. این کار هم ساده است و مبتنی است بر بکارگیری یک PdfTemplate :
  • در متد تحریف شده‌ی OnOpenDocument ، یک قالب PDF ساده را تولید می‌کنیم (مثلا یک مستطیل کوچک خالی).
  • سپس در متد OnEndPage ، این قالب را به انتهای تمام صفحات در حال تولید اضافه خواهیم کرد.
  • زمانیکه متد OnCloseDocument فراخوانده شد، عدد تعداد کل صفحات را در این قالب که به تمام صفحات اضافه شده، درج خواهیم کرد. به این ترتیب این عدد به صورت خودکار در تمام صفحات نمایش داده خواهد شد.

پیاده سازی این توضیحات را در ادامه ملاحظه خواهید کرد:
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
public class PdfWriterPageEvents : PdfPageEventHelper
{
PdfContentByte _pdfContentByte;
// عدد نهایی تعداد کل صفحات را در این قالب قرار خواهیم داد
PdfTemplate _template;
BaseFont _baseFont;

public override void OnOpenDocument(PdfWriter writer, Document document)
{
_baseFont = BaseFont.CreateFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
_pdfContentByte = writer.DirectContent;
_template = _pdfContentByte.CreateTemplate(50, 50);
}

public override void OnEndPage(PdfWriter writer, Document document)
{
base.OnEndPage(writer, document);
String text = writer.PageNumber + "/";
float len = _baseFont.GetWidthPoint(text, 8);
Rectangle pageSize = document.PageSize;
_pdfContentByte.SetRGBColorFill(100, 100, 100);
_pdfContentByte.BeginText();
_pdfContentByte.SetFontAndSize(_baseFont, 8);
_pdfContentByte.SetTextMatrix(pageSize.GetLeft(40), pageSize.GetBottom(30));
_pdfContentByte.ShowText(text);
_pdfContentByte.EndText();
//در پایان هر صفحه یک جای خالی را مخصوص تعداد کل صفحات رزرو خواهیم کرد
_pdfContentByte.AddTemplate(_template, pageSize.GetLeft(40) + len, pageSize.GetBottom(30));
}
public override void OnCloseDocument(PdfWriter writer, Document document)
{
base.OnCloseDocument(writer, document);
_template.BeginText();
_template.SetFontAndSize(_baseFont, 8);
_template.SetTextMatrix(0, 0);
//درج تعداد کل صفحات در تمام قالب‌های اضافه شده
_template.ShowText((writer.PageNumber - 1).ToString());
_template.EndText();
}
}

public class AddTotalNoPages
{
public static void CreateTestPdf()
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("tpn.pdf", FileMode.Create));
pdfWriter.PageEvent = new PdfWriterPageEvents();
pdfDoc.Open();


pdfDoc.Add(new Phrase("Page1"));
pdfDoc.NewPage();
pdfDoc.Add(new Phrase("Page2"));
pdfDoc.NewPage();
pdfDoc.Add(new Phrase("Page3"));
}
}
}
}

مطالب
SignalR
چند وقتی هست که در کنار بدنه اصلی دات‌نت فریم‌ورک چندین کتابخونه به صورت متن‌باز در حال توسعه هستند. این مورد در ASP.NET بیشتر فعاله و مثلا دو کتابخونه SignalR و WebApi توسط خود مایکروسافت توسعه داده میشه.
SignalR همونطور که در سایت بسیار خلاصه و مفید یک صفحه‌ای! خودش توضیح داده شده (^) یک کتابخونه برای توسعه برنامه‌های وب «زمان واقعی»! (real-time web) است:
Async library for .NET to help build real-time, multi-user interactive web applications.
برنامه‌های زمان واقعی به صورت خلاصه و ساده به‌صورت زیر تعریف میشن (^):
The real-time web is a set of technologies and practices that enable users to receive information as soon as it is published by its authors, rather than requiring that they or their software check a source periodically for updates.
یعنی کاربر سیستم ما بدون نیاز به ارسال درخواستی صریح! برای دریافت آخرین اطلاعات به روز شده در سرور، در برنامه کلاینتش از این تغییرات آگاه بشه. مثلا برنامه‌هایی که برای نمایش نمودارهای آماری داده‌ها استفاده میشه (بورس، قیمت ارز و طلا و ...) و یا مهمترین مثالش میتونه برنامه «چت» باشه. متاسفانه پروتوکل HTTP مورد استفاده در وب محدودیت‌هایی برای پیاده‌سازی این گونه برنامه‌ها داره. روش‌های گوناگونی برای پیاده‌سازی برنامه‌های زمان واقعی در وب وجود داره که کتابخونه SignalR فعلا از موارد زیر استفاده میکنه:
  1. تکنولوژی جدید WebSocket (^) که خوشبختانه پشتیبانی کاملی از اون در دات نت 4.5 (چهار نقطه پنج! نه چهار و نیم!) وجود داره. اما تمام مرورگرها و تمام وب سرورها از این تکنولوژی پشتیبانی نمیکنند و تنها برخی نسخه‌های جدید قابلیت استفاده از آخرین ورژن WebSocket رو دارند که میشه به کروم 16 به بالا و فایرفاکس 11 به بالا و اینترنت اکسپلورر 10 اشاره کرد (برای استفاده از این تکنولوژی در ویندوز نیاز به IIS 8.0 است که متاسفانه فقط در ویندوز 8.0 موجوده):
    Chrome 16, Firefox 11 and Internet Explorer 10 are currently the only browsers supporting the latest specification (RFC 6455).
  2.  یه روش دیگه Server-sent Events نام داره که داده‌های جدید رو به فرم رویدادهای DOM به سمت کلاینت میفرسته(^).
  3. روش دیگه‎‌ای که موجوده به Forever Frame معروفه که در این روش یک iframe مخفی درون کد html مسئول تبادل داده‌هاست. این iframe مخفی به‌صورت یک بلاک Chunked (^) به سمت کلاینت فرستاده میشه. این iframe که مسئول رندر داده‌های جدید در سمت کلاینت هست ارتباط خودش رو با سرور تا ابد! (برای همین بهش forever میگن) حفظ میکنه. هر وقت رویدادی سمت سرور رخ میده با استفاده از این روش داده‌ها به‌صورت تگ‌های script به این فریم مخفی فرستاده می‌شوند و چون مرورگرها محتوای html رو به صورت افزایشی (incrementally) رندر میکنن بنابراین این اسکریپتها به‌ترتیب زمان دریافت اجرا می‌شوند. (البته ظاهرا عبارت forever frame در صنعت عکاسی! معروف‌تره بنابراین در جستجو در زمینه این روش ممکنه کمی مشکل داشته باشین) (^).
  4. روش آخر که در کتابخونه SignalR ازش استفاده میشه long-polling نام داره. در روش polling معمولی پس از ارسال درخواست توسط کلاینت، سرور بلافاصله نتیجه حاصله رو به سمت کلاینت میفرسته و ارتباط قطع میشه. بنابراین برای داده‌های جدید درخواست جدیدی باید به سمت سرور فرستاده بشه که تکرار این روش باعث افزایش شدید بار بر روی سرور و کاهش کارآمدی اون می‌شه. اما در روش long-polling پس از برقراری ارتباط کلاینت با سرور این ارتباط تا مدت زمان معینی (که توسط یه مقدار تایم اوت مشخص میشه و مقدار پیش‌فرضش 2 دقیقه است) برقرار میمونه. بنابراین کلاینت میتونه بدون ایجاد مشکلی در کارایی، داده‌های جدید رو از سرور دریافت کنه. به این روش در برنامه‌نویسی وب اصطلاحا برنامه‌نویسی کامت (Comet Programming) میگن (^ ^).
(البته روش‌های دیگری هم برای پیاده‌سازی برنامه‌های زمان اجرا وجود داره مثل کتابخونه node.js که جستجوی بیشتر به خوانندگان واگذار میشه)
SignalR برای برقراری ارتباط ابتدا بررسی میکنه که آیا هر دو سمت سرور و کلاینت قابلیت پشتیبانی از WebSocket رو دارند. در غیراینصورت سراغ روش Server-sent Events میره. اگر باز هم موفق نشد سعی به برقراری ارتباط با روش forever frame میکنه و اگر باز هم موفق نشد در آخر سراغ long-polling میره.
با استفاده از SignalR شما میتونین از سرور، متدهایی رو در سمت کلاینت فراخونی کنین. یعنی درواقع با استفاده از کدهای سی شارپ میشه متدهای جاوااسکریپت سمت کلاینت رو صدا زد!
بطور خلاصه در این کتابخونه دو کلاس پایه وجود داره:
  1. کلاس سطح پایین PersistentConnection
  2. کلاس سطح بالای Hub
علت این نامگذاری به این دلیله که کلاس سطح پایین پیاده‌سازی پیچیده‌تر و تنظیمات بیشتری نیاز داره اما امکانات بیشتری هم در اختیار برنامه‌نویس قرار می‌ده.
خوب پس از این مقدمه نسبتا طولانی برای دیدن یک مثال ساده میتونین با استفاده از نوگت (Nuget) مثال زیر رو نصب و اجرا کنین (اگه تا حالا از نوگت استفاده نکردین قویا پیشنهاد میکنم که کار رو با دریافتش از اینجا آغاز کنین) :
PM> Install-Package SignalR.Sample
پس از کامل شدن نصب این مثال اون رو اجرا کنین. این یک مثال فرضی ساده از برنامه نمایش ارزش آنلاین سهام برخی شرکتهاست. میتونین این برنامه رو همزمان در چند مرورگر اجرا کنین و نتیجه رو مشاهده کنین.
حالا میریم سراغ یک مثال ساده. میخوایم یک برنامه چت ساده بنویسیم. ابتدا یک برنامه وب اپلیکیشن خالی رو ایجاد کرده و با استفاده از دستور زیر در خط فرمان نوگت، کتابخونه SignalR رو نصب کنین:
PM> Install-Package SignalR
پس از کامل شدن نصب این کتابخونه، ریفرنس‌های زیر به برنامه اضافه میشن:
Microsoft.Web.Infrastructure
Newtonsoft.Json
SignalR
SignalR.Hosting.AspNet
SignalR.Hosting.Common
برای کسب اطلاعات مختصر و مفید از تمام اجزای این کتابخونه به اینجا مراجعه کنین.
همچنین اسکریپت‌های زیر به پوشه Scripts اضافه میشن (این نسخه‌ها مربوط به زمان نگارش این مطلب است):
jquery-1.6.4.js
jquery.signalR-0.5.1.js
بعد یک کلاس با نام SimpleChat به برنامه اضافه و محتوای زیر رو در اون وارد کنین:
using SignalR.Hubs;
namespace SimpleChatWithSignalR
{
  public class SimpleChat : Hub
  {
    public void SendMessage(string message)
    {
      Clients.reciveMessage(message);
    }
  }
} 
دقت کنین که این کلاس از کلاس Hub مشتق شده و همچنین خاصیت Clients از نوع dynamic است. (در مورد جزئیات این کتابخونه در قسمت‌های بعدی توضیحات مفصل‌تری داده میشه)
سپس یک فرم به برنامه اضافه کرده و محتوای زیر رو در اون اضافه کنین:
<input type="text" id="msg" />
<input type="button" value="Send" id="send" /><br />
<textarea id='messages' readonly="true" style="height: 200px; width: 200px;"></textarea>
<script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="Scripts/jquery.signalR-0.5.1.min.js" type="text/javascript"></script>
<script src="signalr/hubs" type="text/javascript"></script>
<script type="text/javascript">
  var chat = $.connection.simpleChat;
  chat.reciveMessage = function (msg) {
   $('#messages').val($('#messages').val() + "-" + msg + "\r\n"); 
  };
  $.connection.hub.start();
  $('#send').click(function () {
    chat.sendMessage($('#msg').val());
  });
</script>
همونطور که میبینین برنامه چت ما آماده شد! حالا برنامه رو اجرا کنین و با استفاده از دو مرورگر مختلف نتیجه رو مشاهده کنین.
نکته کلیدی کار SignalR در خط زیر نهفته است:
<script src="signalr/hubs" type="text/javascript"></script>
اگر محتوای آدرس فوق رو دریافت کنین می‌بینین که موتور این کتابخانه تمامی متدهای موردنیاز در سمت کلاینت رو با استفاده از کدهای جاوااسکریپت تولید کرده. البته در این کد تولیدی از نامگذاری camel Casing استفاده میشه، بنابراین متد SendMessage در سمت سرور به‌صورت sendMessage در سمت کلاینت در دسترسه.
امیدوارم تا اینجا تونسته باشم علاقه شما به استفاده از این کتابخونه رو جلب کرده باشم. در قسمت‌های بعد موارد پیشرفته‌تر این کتابخونه معرفی میشه.
اگه علاقه‌مند باشین میتونین از این ویکی اطلاعات بیشتری بدست بیارین.


به روز رسانی
در دوره‌ای به نام SignalR در سایت، به روز شده‌ای این مباحث را می‌توانید مطالعه کنید.