مطالب
اسکرول روان لیست‌های مجازی سازی شده در WPF 4.5
جهت «بهبود کارآیی کنترل‌های لیستی WPF در حین بارگذاری تعداد زیادی از رکوردها» توصیه شده‌است که مجازی سازی UI فعال گردد. به این ترتیب بجای تولید یکباره‌ی برای مثال 1000 ردیف، تنها 10 ردیفی که نمایان هستند تولید می‌شوند. بنابراین مصرف حافظه و سرعت برنامه به نحو قابل ملاحظه‌ای افزایش خواهد یافت. اما ... این مجازی سازی، اسکرول مطلوبی ندارد و بریده بریده به نظر می‌رسد.


خاصیت‌های جدید VirtualizingPanel در دات نت 4.5

تمام کنترل‌های مشتق شده‌ی از ListBox مانند ListView و امثال آن، در WPF 4.5 امکان تنظیم یک چنین خواصی را یافته‌اند:
<ListBox ItemsSource="{Binding Persons}"
        VirtualizingPanel.IsVirtualizing="True"
        VirtualizingPanel.ScrollUnit="Pixel"
        VirtualizingPanel.IsVirtualizingWhenGrouping="True"
        VirtualizingPanel.CacheLength="100"        
        VirtualizingPanel.CacheLengthUnit="Pixel"/>
در WPF 4.5 پس از نمایش تعداد ردیف‌های قابل ملاحظه‌ی توسط کاربر، سیستم کش خودکاری با حق تقدم پایین فعال شده و آیتم‌هایی را که هنوز نمایان نیستند، ایجاد و کش می‌کند. به این ترتیب کاربر با اسکرول به سمت پایین یا بالا، کندی رندر یا بریده بریده به نظر رسیدن اسکرول را حس نخواهد کرد.
در اینجا می‌توان دو خاصیت CacheLength و CacheLengthUnit را مقدار دهی کرد. مقدار پیش فرض CacheLength مساوی 1.1 است. CacheLengthUnit می‌تواند یکی از مقادیر Pixel, Item یا Page را بپذیرد. هر Page در اینجا بر اساس اندازه‌ی viewport یا قسمت نمایان لیست، تعریف می‌شود. مقدار پیش فرض آن نیز Item است.
همچنین در اینجا می‌توان ScrollUnit را نیز تنظیم کرد که مقادیر Item یا Pixel را می‌پذیرد. حالت پیش فرض آن Item است؛ به این معنا که اگر ردیفی کاملا در viewport جای نگرفت، نمایش داده نمی‌شود. این مورد را با تنظیم مقدار ScrollUnit به Pixel می‌توان بهبود بخشید.
IsVirtualizingWhenGrouping سبب فعال سازی مجازی سازی حتی در حالت گروه بندی اطلاعات می‌گردد. به صورت پیش فرض اگر گروه بندی فعال شود، دیگر مجازی سازی رخ نخواهد داد.


فعال سازی قابلیت‌های دات نت 4.5 در برنامه‌های WPF 4

اگر برنامه‌ی WPF 4 خود را فعلا قصد ندارید به دات نت 4.5 ارتقاء دهید، با توجه به اینکه اگر کاربر دات نت 4.5 را نصب کرده باشد، برنامه‌ی شما به صورت خودکار همانند یک برنامه‌ی WPF 4.5 رفتار می‌کند (دات نت 4.5 جایگزین دات نت 4 می‌شود)، می‌توان با اندکی Reflection این قابلیت‌ها را در صورت وجود، فعال کرد:
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;

namespace DNTProfiler.Common.Behaviors
{
    /// <summary>
    /// Smooth scrolling VirtualizingStackPanels, without sacrificing virtualization.
    /// </summary>
    public static class PixelBasedScrollingBehavior
    {
        private static readonly MethodInfo _setScrollUnit = typeof(VirtualizingPanel)
            .GetMethod("SetScrollUnit", BindingFlags.Public | BindingFlags.Static);

        private static readonly MethodInfo _setCacheLengthUnit = typeof(VirtualizingPanel)
            .GetMethod("SetCacheLengthUnit", BindingFlags.Public | BindingFlags.Static);

        private static readonly MethodInfo _setCacheLength = typeof(VirtualizingPanel)
            .GetMethod("SetCacheLength", BindingFlags.Public | BindingFlags.Static);

        public static bool GetIsEnabled(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsEnabledProperty);
        }

        public static void SetIsEnabled(DependencyObject obj, bool value)
        {
            obj.SetValue(IsEnabledProperty, value);
        }

        public static readonly DependencyProperty IsEnabledProperty =
            DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior), new UIPropertyMetadata(false, handleIsEnabledChanged));

        private static void handleIsEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var listView = obj as ListView;
            if (listView == null)
            {
                throw new InvalidOperationException("This behavior can only be attached to a ListView.");
            }

            if (_setScrollUnit != null)
            {
                // It's .NET 4.5
                _setScrollUnit.Invoke(listView, new object[] { listView, /*Pixel*/ 0 });
            }

            if (_setCacheLengthUnit != null)
            {
                // It's .NET 4.5
                _setCacheLengthUnit.Invoke(listView, new object[] { listView, /*Pixel*/ 0 });
            }


            if (_setCacheLength != null)
            {
                // It's .NET 4.5
                var type = Type.GetType("System.Windows.Controls.VirtualizationCacheLength, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
                if (type == null) return;
                var instance = Activator.CreateInstance(type, 100.0);

                _setCacheLength.Invoke(listView, new[] { listView, instance });
            }
        }
    }
}
در اینجا با تعریف یک Attached property جدید، خواصی مانند ScrollUnit، CacheLengthUnit و CacheLength توسط Reflection یافت خواهند شد. اگر مقدار آن‌ها نال نباشند، یعنی برنامه‌ی دات نت 4 در سیستمی که بر روی آن دات نت 4.5 نصب است، در حال اجرا می‌باشد. بنابراین در این حالت می‌توان اسکروال مبتنی بر Pixel را فعال کرد تا برنامه اسکرول روان‌تری را پیدا کند.
برای استفاده از آن خواهیم داشت:
<ListView ItemsSource="{Binding}"
               behaviors:PixelBasedScrollingBehavior.IsEnabled="True">
بازخوردهای دوره
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
البته راه دیگری هم پیدا کردم.
به نظرم جالب اومد.
ابتدا یک اکشن فیلتر تعریف شده و در ان هدر درخواست خوانده شده و با محتوی کوکی چک می‌شود در صورتی که برابر باشند عمل انجام خواهد شد با این تفاوت باید توکن در زمان ارسال در هدر درخواست قرار گیرد مانند زیر:
کد اکشن فیلتر:
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
    {
#region Methods (1) 

// Public Methods (1) 

        /// <summary>
        /// Called when [action executing].
        /// </summary>
        /// <param name="actionContext">The action context.</param>
        public void OnActionExecuting(HttpActionContext actionContext)
        {
            try
            {
                var cookieName = AntiForgeryConfig.CookieName;
                var headers = actionContext.Request.Headers;
                var cookie = headers
                    .GetCookies()
                    .Select(c => c[AntiForgeryConfig.CookieName])
                    .FirstOrDefault();
                var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
                AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
            }
            catch
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
            }
        }

#endregion Methods 
    }
سپس در اکشن دلخواه باید این اکشن فیلتر را بکار برد (در درخواست‌های json)
[HttpPost]
        //[ValidateAntiForgeryToken]
        [ValidateJsonAntiForgeryToken]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult DeletePhoto(int? id)
        {
            string ret="error";
            if (id != null && id > 0)
            {
                bool result = _imageGalleryService.Value.Photos.Value.Delete(id.ToInt32());
                ret = result ? "ok" : "nok";
            }
            return Content(ret);
            //return Json(new { ret });
        }
سپس در طرف ویو به صورت زیر عمل شود
$.ajax({
                       ....
                    type: "POST",
                    url: '@Url',
                    data: JSON.stringify({ id: Id }),
                    contentType: "application/json; charset=utf-8",
                    // این قسمت اضافه شود
                    headers: { __RequestVerificationToken: $("input[name=__RequestVerificationToken]").val() },
                    dataType: "json",
                     ..........
امیدوارم برای دوستان هم کاربرد داشته باشد




نظرات مطالب
ایجاد جداول بهینه سازی شده برای حافظه در SQL Server 2014
ارزش واقعی جداول درون حافظه‌ای را باید با اعمال تراکنش‌های همزمان و بررسی میزان پاسخگویی سیستم بررسی کرد و نه صرفا با یک آزمایش ساده تک ریسمانی. برای این منظور برنامه‌ای به نام ostress.exe توسط مایکروسافت تهیه شده‌است که امکان انجام یک چنین آزمایشاتی را میسر می‌کند. برای دریافت آن به آدرس‌های ذیل مراجعه کنید:
RML Utilities X64
RML Utilities X86

که نهایتا در این مسیر C:\Program Files\Microsoft Corporation\RMLUtils نصب خواهد شد.
سپس در خط فرمان این سه دستور را امتحان کنید:

-- Insert 10000 records using 20 threads, Repeat Execution 3 times

-- disk-based
"C:\Program Files\Microsoft Corporation\RMLUtils\ostress.exe" –n20 –r3 -S. -E -dTestdb2 -q -Q"set statistics time off; SET STATISTICS IO Off; set nocount on; DECLARE @start datetime = getdate(); declare @insertCount int = 10000; declare @startId int = 1; while @startId < @insertCount begin insert into tblNormal values ('Test', '2013-01-01T00:00:00') set @startId +=1 end; Print DATEDIFF(ms,@start,getdate());" –oc:\temp\output

-- memory-optimized, tblMemoryOptimized_Schema_And_Data
"C:\Program Files\Microsoft Corporation\RMLUtils\ostress.exe" –n20 –r3 -S. -E -dTestdb2 -q -Q"set statistics time off; SET STATISTICS IO Off; set nocount on; DECLARE @start datetime = getdate(); declare @insertCount int = 10000; declare @startId int = 1; while @startId < @insertCount begin insert into tblMemoryOptimized_Schema_And_Data values ('Test', '2013-01-01T00:00:00') set @startId +=1 end; Print DATEDIFF(ms,@start,getdate());" –oc:\temp\output

-- memory-optimized, tblMemoryOptimized_Schema_Only
"C:\Program Files\Microsoft Corporation\RMLUtils\ostress.exe" –n20 –r3 -S. -E -dTestdb2 -q -Q"set statistics time off; SET STATISTICS IO Off; set nocount on; DECLARE @start datetime = getdate(); declare @insertCount int = 10000; declare @startId int = 1;  while @startId <  @insertCount begin insert into tblMemoryOptimized_Schema_Only values ('Test', '2013-01-01T00:00:00') set @startId +=1 end; Print DATEDIFF(ms,@start,getdate());" –oc:\temp\output
زمانیکه را که در پایان کار نمایش می‌دهد، مبنای واقعی مقایسه است.

البته برای اجرای این دستورات نیاز است فیلد CustomerID را identity تعریف کنید (در هر سه جدول مطلب جاری).

-- It is not Memory Optimized
CREATE TABLE tblNormal
(
       [CustomerID] int identity NOT NULL PRIMARY KEY NONCLUSTERED, 
       [Name] nvarchar(250) NOT NULL,
   CustomerSince DATETIME not NULL
      INDEX [ICustomerSince] NONCLUSTERED
)

--  DURABILITY = SCHEMA_AND_DATA
CREATE TABLE tblMemoryOptimized_Schema_And_Data
(
    [CustomerID] INT  identity NOT NULL 
PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 131072),
    [Name] NVARCHAR(250) NOT NULL,
    [CustomerSince] DATETIME NOT NULL
INDEX [ICustomerSince] NONCLUSTERED
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA)


-- DURABILITY = SCHEMA_ONLY
CREATE TABLE tblMemoryOptimized_Schema_Only
(
    [CustomerID] INT  identity NOT NULL 
PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 131072),
    [Name] NVARCHAR(250) NOT NULL,
    [CustomerSince] DATETIME NOT NULL
INDEX [ICustomerSince] NONCLUSTERED
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY)
مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش دوم
در بخش اول، کارهایی که انجام دادیم به طور خلاصه عبارت بودند از:
1-  حذف کاربرانی که نام کاربری و ایمیل تکراری داشتند
2-  تغییر نام فیلد Password به PasswordHash در جدول User
 
سیستم مدیریت محتوای IRIS، برای استفاده از Entity Framework، از الگوی واحد کار (Unit Of Work) و تزریق وابستگی استفاده کرده است و اگر با نحوه‌ی پیاده سازی این الگو‌ها آشنا نیستید، خواندن مقاله EF Code First #12  را به شما توصیه می‌کنم.
برای استفاده از ASP.NET Identity نیز باید از الگوی واحد کار استفاده کرد و برای این کار، ما از مقاله اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity استفاده خواهیم کرد.
نکته مهم: در ادامه اساس کار ما بر پایه‌ی مقاله اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity است و چیزی که بیشتر برای ما اهمیت دارد کدهای نهایی آن هست؛ پس حتما به مخزن کد آن مراجعه کرده و کدهای آن را دریافت کنید.
 
تغییر نام کلاس User به ApplicationUser

اگر به کدهای مثال رسمی ASP.NET Identity نگاهی بیندازید، می‌بینید که کلاس مربوط به جدول کاربران ApplicationUser نام دارد، ولی در سیستم IRIS نام آن User است. بهتر است که ما هم نام کلاس خود را از User به ApplicationUser تغییر دهیم چرا که مزایای زیر را به دنبال دارد:

1- به راحتی می‌توان کدهای مورد نیاز را از مثال Identity کپی کرد.
2- در سیستم Iris، بین کلاس User متعلق به پروژه خودمان و User مربوط به HttpContext تداخل رخ می‌داد که با تغییر نام کلاس User دیگر این مشکل را نخواهیم داشت.
 
برای این کار وارد پروژه Iris.DomainClasses شده و نام کلاس User را به ApplicationUser تغییر دهید. دقت کنید که این تغییر نام را از طریق Solution Explorer انجام دهید و نه از طریق کدهای آن. پس از این تغییر ویژوال استودیو می‌پرسد که آیا نام این کلاس را هم در کل پروژه تغییر دهد که شما آن را تایید کنید.

برای آن که نام جدول Users در دیتابیس تغییری نکند، وارد پوشه‌ی Entity Configuration شده و کلاس UserConfig را گشوده و در سازنده‌ی آن کد زیر را اضافه کنید:
ToTable("Users");

نصب ASP.NET Identity

برای نصب ASP.NET Identity دستور زیر را در کنسول Nuget وارد کنید:
Get-Project Iris.DomainClasses, Iris.Datalayer, Iris.Servicelayer, Iris.Web | Install-Package Microsoft.AspNet.Identity.EntityFramework
از پروژه AspNetIdentityDependencyInjectionSample.DomainClasses کلاس‌های CustomUserRole، CustomUserLogin، CustomUserClaim و CustomRole را به پروژه Iris.DomainClasses منتقل کنید. تنها تغییری که در این کلاس‌ها باید انجام دهید، اصلاح namespace آنهاست.
همچنین بهتر است که به کلاس CustomRole، یک property به نام Description اضافه کنید تا توضیحات فارسی نقش مورد نظر را هم بتوان ذخیره کرد:

 
    public class CustomRole : IdentityRole<int, CustomUserRole>
    {
        public CustomRole() { }
        public CustomRole(string name) { Name = name; }

        public string Description { get; set; }

    }

نکته: پیشنهاد می‌کنم که اگر می‌خواهید مثلا نام CustomRole را به IrisRole تغییر دهید، این کار را از طریق find and replace انجام ندهید. با همین نام‌های پیش فرض کار را تکمیل کنید و سپس از طریق خود ویژوال استودیو نام کلاس را تغییر دهید تا ویژوال استودیو به نحو بهتری این نام‌ها را در سرتاسر پروژه تغییر دهد.
 
سپس کلاس ApplicationUser پروژه IRIS را باز کرده و تعریف آن را به شکل زیر تغییر دهید:
public class ApplicationUser : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>

اکنون می‌توانید property‌های Id،  UserName، PasswordHash و Email را حذف کنید؛ چرا که در کلاس پایه IdentityUser تعریف شده اند.
 
تغییرات DataLayer

وارد Iris.DataLayer شده و کلاس IrisDbContext را به شکل زیر ویرایش کنید:
public class IrisDbContext : IdentityDbContext<ApplicationUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>,
        IUnitOfWork

اکنون می‌توانید property زیر را نیز حذف کنید چرا که در کلاس پایه تعریف شده است: 
 public DbSet<ApplicationUser> Users { get; set; }

 

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

        public IrisDbContext()
            : base("IrisDbContext")
        {
        }

همچنین درون متد OnModelCreating کدهای زیر را پس از فراخوانی متد (base.OnModelCreating(modelBuilder  جهت تعیین نام جداول دیتابیس بنویسید:
            modelBuilder.Entity<CustomRole>().ToTable("AspRoles");
            modelBuilder.Entity<CustomUserClaim>().ToTable("UserClaims");
            modelBuilder.Entity<CustomUserRole>().ToTable("UserRoles");
            modelBuilder.Entity<CustomUserLogin>().ToTable("UserLogins");
از این جهت نام جدول CustomRole را در دیتابیس AspRoles انتخاب کردم تا با نام جدول Roles نقش‌های کنونی سیستم Iris تداخلی پیش نیاید.
اکنون دستور زیر را در کنسول Nuget وارد کنید تا کدهای مورد نیاز برای مهاجرت تولید شوند:
Add-Migration UpdateDatabaseToAspIdentity
public partial class UpdateDatabaseToAspIdentity : DbMigration
{
        public override void Up()
        {
            CreateTable(
                "dbo.UserClaims",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        UserId = c.Int(nullable: false),
                        ClaimType = c.String(),
                        ClaimValue = c.String(),
                        ApplicationUser_Id = c.Int(),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.Users", t => t.ApplicationUser_Id)
                .Index(t => t.ApplicationUser_Id);
            
            CreateTable(
                "dbo.UserLogins",
                c => new
                    {
                        LoginProvider = c.String(nullable: false, maxLength: 128),
                        ProviderKey = c.String(nullable: false, maxLength: 128),
                        UserId = c.Int(nullable: false),
                        ApplicationUser_Id = c.Int(),
                    })
                .PrimaryKey(t => new { t.LoginProvider, t.ProviderKey, t.UserId })
                .ForeignKey("dbo.Users", t => t.ApplicationUser_Id)
                .Index(t => t.ApplicationUser_Id);
            
            CreateTable(
                "dbo.UserRoles",
                c => new
                    {
                        UserId = c.Int(nullable: false),
                        RoleId = c.Int(nullable: false),
                        ApplicationUser_Id = c.Int(),
                    })
                .PrimaryKey(t => new { t.UserId, t.RoleId })
                .ForeignKey("dbo.Users", t => t.ApplicationUser_Id)
                .ForeignKey("dbo.AspRoles", t => t.RoleId, cascadeDelete: true)
                .Index(t => t.RoleId)
                .Index(t => t.ApplicationUser_Id);
            
            CreateTable(
                "dbo.AspRoles",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Description = c.String(),
                        Name = c.String(nullable: false, maxLength: 256),
                    })
                .PrimaryKey(t => t.Id)
                .Index(t => t.Name, unique: true, name: "RoleNameIndex");
            
            AddColumn("dbo.Users", "EmailConfirmed", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "SecurityStamp", c => c.String());
            AddColumn("dbo.Users", "PhoneNumber", c => c.String());
            AddColumn("dbo.Users", "PhoneNumberConfirmed", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "TwoFactorEnabled", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "LockoutEndDateUtc", c => c.DateTime());
            AddColumn("dbo.Users", "LockoutEnabled", c => c.Boolean(nullable: false));
            AddColumn("dbo.Users", "AccessFailedCount", c => c.Int(nullable: false));
        }
        
        public override void Down()
        {
            DropForeignKey("dbo.UserRoles", "RoleId", "dbo.AspRoles");
            DropForeignKey("dbo.UserRoles", "ApplicationUser_Id", "dbo.Users");
            DropForeignKey("dbo.UserLogins", "ApplicationUser_Id", "dbo.Users");
            DropForeignKey("dbo.UserClaims", "ApplicationUser_Id", "dbo.Users");
            DropIndex("dbo.AspRoles", "RoleNameIndex");
            DropIndex("dbo.UserRoles", new[] { "ApplicationUser_Id" });
            DropIndex("dbo.UserRoles", new[] { "RoleId" });
            DropIndex("dbo.UserLogins", new[] { "ApplicationUser_Id" });
            DropIndex("dbo.UserClaims", new[] { "ApplicationUser_Id" });
            DropColumn("dbo.Users", "AccessFailedCount");
            DropColumn("dbo.Users", "LockoutEnabled");
            DropColumn("dbo.Users", "LockoutEndDateUtc");
            DropColumn("dbo.Users", "TwoFactorEnabled");
            DropColumn("dbo.Users", "PhoneNumberConfirmed");
            DropColumn("dbo.Users", "PhoneNumber");
            DropColumn("dbo.Users", "SecurityStamp");
            DropColumn("dbo.Users", "EmailConfirmed");
            DropTable("dbo.AspRoles");
            DropTable("dbo.UserRoles");
            DropTable("dbo.UserLogins");
            DropTable("dbo.UserClaims");
        }
}

بهتر است که در کدهای تولیدی فوق، اندکی متد Up را با کد زیر تغییر دهید: 
AddColumn("dbo.Users", "EmailConfirmed", c => c.Boolean(nullable: false, defaultValue:true));

چون در سیستم جدید احتیاج به تایید ایمیل به هنگام ثبت نام است، بهتر است که ایمیل‌های قبلی موجود در سیستم نیز به طور پیش فرض تایید شده باشند.
در نهایت برای اعمال تغییرات بر روی دیتابیس دستور زیر را در کنسول Nuget وارد کنید:
Update-Database
 
تغییرات ServiceLayer

ابتدا دستور زیر را در کنسول Nuget  وارد کنید: 
Get-Project Iris.Servicelayer, Iris.Web | Install-Package Microsoft.AspNet.Identity.Owin
سپس از فولدر Contracts پروژه AspNetIdentityDependencyInjectionSample.ServiceLayer فایل‌های IApplicationRoleManager، IApplicationSignInManager، IApplicationUserManager، ICustomRoleStore و ICustomUserStore را در فولدر Interfaces پروژه Iris.ServiceLayer کپی کنید. تنها کاری هم که نیاز هست انجام بدهید اصلاح namespace هاست.

باز از پروژه AspNetIdentityDependencyInjectionSample.ServiceLayer کلاس‌های ApplicationRoleManager، ApplicationSignInManager،  ApplicationUserManager، CustomRoleStore، CustomUserStore، EmailService و SmsService را به پوشه EFServcies پروژه‌ی Iris.ServiceLayer کپی کنید.
نکته: پیشنهاد می‌کنم که EmailService را به IdentityEmailService تغییر نام دهید چرا که در حال حاضر سیستم Iris دارای کلاسی به نامی EmailService هست.
 
تنظیمات StructureMap برای تزریق وابستگی ها
پروژه Iris.Web  را باز کرده، به فولدر DependencyResolution بروید و به کلاس IoC کدهای زیر را اضافه کنید:
                x.For<IIdentity>().Use(() => (HttpContext.Current != null && HttpContext.Current.User != null) ? HttpContext.Current.User.Identity : null);

                x.For<IUnitOfWork>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<IrisDbContext>();

                x.For<IrisDbContext>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (IrisDbContext)context.GetInstance<IUnitOfWork>());
                x.For<DbContext>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (IrisDbContext)context.GetInstance<IUnitOfWork>());

                x.For<IUserStore<ApplicationUser, int>>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<CustomUserStore>();

                x.For<IRoleStore<CustomRole, int>>()
                    .HybridHttpOrThreadLocalScoped()
                    .Use<RoleStore<CustomRole, int, CustomUserRole>>();

                x.For<IAuthenticationManager>()
                      .Use(() => HttpContext.Current.GetOwinContext().Authentication);

                x.For<IApplicationSignInManager>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<ApplicationSignInManager>();

                x.For<IApplicationRoleManager>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<ApplicationRoleManager>();

                // map same interface to different concrete classes
                x.For<IIdentityMessageService>().Use<SmsService>();
                x.For<IIdentityMessageService>().Use<IdentityEmailService>();

                x.For<IApplicationUserManager>().HybridHttpOrThreadLocalScoped()
                   .Use<ApplicationUserManager>()
                   .Ctor<IIdentityMessageService>("smsService").Is<SmsService>()
                   .Ctor<IIdentityMessageService>("emailService").Is<IdentityEmailService>()
                   .Setter<IIdentityMessageService>(userManager => userManager.SmsService).Is<SmsService>()
                   .Setter<IIdentityMessageService>(userManager => userManager.EmailService).Is<IdentityEmailService>();

                x.For<ApplicationUserManager>().HybridHttpOrThreadLocalScoped()
                   .Use(context => (ApplicationUserManager)context.GetInstance<IApplicationUserManager>());

                x.For<ICustomRoleStore>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<CustomRoleStore>();

                x.For<ICustomUserStore>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use<CustomUserStore>();

اگر ()HttpContext.Current.GetOwinContext شناسایی نمی‌شود دلیلش این است که متد GetOwinContext یک متد الحاقی است که برای استفاده از آن باید پکیج نیوگت زیر را نصب کنید:
Install-Package Microsoft.Owin.Host.SystemWeb

تغییرات Iris.Web
در ریشه پروژه‌ی Iris.Web  یک کلاس به نام Startup  بسازید و کدهای زیر را در آن بنویسید:
using System;
using Iris.Servicelayer.Interfaces;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using StructureMap;

namespace Iris.Web
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            configureAuth(app);
        }

        private static void configureAuth(IAppBuilder app)
        {
            ObjectFactory.Container.Configure(config =>
            {
                config.For<IDataProtectionProvider>()
                      .HybridHttpOrThreadLocalScoped()
                      .Use(() => app.GetDataProtectionProvider());
            });

            //ObjectFactory.Container.GetInstance<IApplicationUserManager>().SeedDatabase();

            // Enable the application to use a cookie to store information for the signed in user
            // and to use a cookie to temporarily store information about a user logging in with a third party login provider
            // Configure the sign in cookie
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    // Enables the application to validate the security stamp when the user logs in.
                    // This is a security feature which is used when you change a password or add an external login to your account.
                    OnValidateIdentity = ObjectFactory.Container.GetInstance<IApplicationUserManager>().OnValidateIdentity()
                }
            });
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
            app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

            // Enables the application to remember the second login verification factor such as phone or email.
            // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
            // This is similar to the RememberMe option when you log in.
            app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

            app.CreatePerOwinContext(
               () => ObjectFactory.Container.GetInstance<IApplicationUserManager>());

            // Uncomment the following lines to enable logging in with third party login providers
            //app.UseMicrosoftAccountAuthentication(
            //    clientId: "",
            //    clientSecret: "");

            //app.UseTwitterAuthentication(
            //   consumerKey: "",
            //   consumerSecret: "");

            //app.UseFacebookAuthentication(
            //   appId: "",
            //   appSecret: "");

            //app.UseGoogleAuthentication(
            //    clientId: "",
            //    clientSecret: "");

        }
    }
}

تا به این جای کار اگر پروژه را اجرا کنید نباید هیچ مشکلی مشاهده کنید. در بخش بعدی کدهای مربوط به کنترلر‌های ورود، ثبت نام، فراموشی کلمه عبور و ... را با سیستم Identity پیاده سازی می‌کنیم.
مطالب
طراحی گردش کاری با استفاده از State machines - قسمت دوم
معرفی کتابخانه stateless به عنوان جایگزین سبک وزنی برای Windows workflow foundation

کتابخانه سورس باز Stateless، برای طراحی و پیاده سازی «ماشین‌های حالت گردش کاری مانند» تهیه شده و مزایای زیر را نسبت به Windows workflow foundation دارا است:
- جمعا  30 کیلوبایت است!
- تمام اجزای آن سورس باز است.
- دارای API روان و ساده‌ای است.
- امکان تبدیل UML state diagrams، به نمونه معادل Stateless بسیار ساده و سریع است.
- به دلیل code first بودن، کار کردن با آن برای برنامه نویس‌ها ساده‌تر بوده و افزودن یا تغییر اجزای آن با کدنویسی به سادگی میسر است.

دریافت کتابخانه Stateless از Google code و یا از NuGet


پیاده سازی مثال کلید برق با Stateless

در ادامه همان مثال ساده  کلید برق قسمت قبل را با Stateless پیاده سازی خواهیم کرد:
using System;
using Stateless;

namespace StatelessTests
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                string on = "On", off = "Off";
                var space = ' ';

                var onOffSwitch = new StateMachine<string, char>(initialState: off);

                onOffSwitch.Configure(state: off).Permit(trigger: space, destinationState: on);
                onOffSwitch.Configure(state: on).Permit(trigger: space, destinationState: off);

                Console.WriteLine("Press <space> to toggle the switch. Any other key will raise an error.");

                while (true)
                {
                    Console.WriteLine("Switch is in state: " + onOffSwitch.State);
                    var pressed = Console.ReadKey(true).KeyChar;
                    onOffSwitch.Fire(trigger: pressed);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception: " + ex.Message);
                Console.WriteLine("Press any key to continue...");
                Console.ReadKey(true);
            }
        }
    }
}
کار با ایجاد یک وهله از ماشین حالت (new StateMachine) آغاز می‌شود. حالت آغازین آن (initialState) مطابق مثال قسمت قبل، مساوی off است.
امضای کلاس StateMachine را در ذیل مشاهده می‌کنید؛ جهت توضیح آرگومان‌های جنریک string و char معرفی شده در مثال:
public class StateMachine<TState, TTrigger>
که اولی بیانگر نوع حالات قابل تعریف است و دومی نوع رویداد قابل دریافت را مشخص می‌کند.
برای مثال در اینجا حالات روشن و خاموش، با رشته‌های on و off مشخص شده‌اند و رویداد قابل قبول دریافتی، کاراکتر فاصله است.
سپس نیاز است این ماشین حالت را برای معرفی رویدادهایی (trigger در اینجا) که سبب تغییر حالت آن می‌شوند، تنظیم کنیم. اینکار توسط متدهای Configure و Permit انجام خواهد شد. متد Configure، یکی از حالات از پیش تعیین شده را جهت تنظیم، مشخص می‌کند و سپس در متد Permit تعیین خواهیم کرد که بر اساس رخدادی مشخص (برای مثال در اینجا فشرده شدن کلید space) وضعیت حالت جاری، به وضعیت جدیدی (destinationState) منتقل شود.
نهایتا این ماشین حالت در یک حلقه بی‌نهایت مشغول به کار خواهد شد. برای نمونه یک Thread پس زمینه (BackgroundWorker) نیز می‌تواند همین کار را در برنامه‌های ویندوزی انجام دهد.


یک نکته
علاوه بر روش‌های یاد شده‌ی تشخیص الگوی ماشین حالت که در قسمت قبل بررسی شدند، مورد refactoring انبوهی از if و elseها و یا switchهای بسیار طولانی را نیز می‌توان به این لیست افزود.



استفاده از Stateless Designer برای تولید کدهای ماشین حالت

کتابخانه Stateless دارای یک طراح و Code generator بصری سورس باز است که آن‌را به شکل افزونه‌ای برای VS.NET می‌توانید در سایت Codeplex دریافت کنید. این طراح از کتابخانه GLEE برای رسم گراف استفاده می‌کند.

کار مقدماتی با آن به نحو زیر است:
الف) فایل StatelessDesignerPackage.vsix را از سایت کدپلکس دریافت و نصب کنید. البته نگارش فعلی آن فقط با VS 2012 سازگار است.
ب) ارجاعی را به اسمبلی stateless به پروژه خود اضافه نمائید (به یک پروژه جدید یا از پیش موجود).
ج) از منوی پروژه، گزینه Add new item را انتخاب کرده و سپس در صفحه ظاهر شده، گزینه جدید Stateless state machine را انتخاب و به پروژه اضافه نمائید.
کار با این طراح، با ادیت XML آن شروع می‌شود. برای مثال گردش کاری ارسال و تائید یک مطلب جدید را در بلاگی فرضی، به نحو زیر وارد نمائید:
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
  <settings>
    <itemname>BlogPostStateMachine</itemname>
    <namespace>StatelessTests</namespace>
    <class>public</class>
  </settings>
  <triggers>     
    <trigger>Save</trigger>
    <trigger>RequireEdit</trigger>
    <trigger>Accept</trigger>
    <trigger>Reject</trigger>
  </triggers>
  <states>     
    <state start="yes">Begin</state>
    <state>InProgress</state>     
    <state>Published</state>      
    <state>Rejected</state>      
  </states>
  <transitions>
    <transition trigger="Save" from="Begin" to="InProgress" />

    <transition trigger="Accept" from="InProgress" to="Published" />
    <transition trigger="Reject" from="InProgress" to="Rejected" />

    <transition trigger="Save" from="InProgress" to="InProgress" />

    <transition trigger="RequireEdit" from="Published" to="InProgress" />
    <transition trigger="RequireEdit" from="Rejected" to="InProgress" />
  </transitions>
</statemachine>
حاصل آن گراف زیر خواهد بود:


به علاوه کدهای زیر که به صورت خودکار تولید شده‌اند:
using Stateless;

namespace StatelessTests
{
  public class BlogPostStateMachine
  {
    public delegate void UnhandledTriggerDelegate(State state, Trigger trigger);
    public delegate void EntryExitDelegate();
    public delegate bool GuardClauseDelegate();

    public enum Trigger
    {
      Save,
      RequireEdit,
      Accept,
      Reject,
    }

    public enum State
    {
      Begin,
      InProgress,
      Published,
      Rejected,
    }

    private readonly StateMachine<State, Trigger> stateMachine = null;

    public EntryExitDelegate OnBeginEntry = null;
    public EntryExitDelegate OnBeginExit = null;
    public EntryExitDelegate OnInProgressEntry = null;
    public EntryExitDelegate OnInProgressExit = null;
    public EntryExitDelegate OnPublishedEntry = null;
    public EntryExitDelegate OnPublishedExit = null;
    public EntryExitDelegate OnRejectedEntry = null;
    public EntryExitDelegate OnRejectedExit = null;
    public GuardClauseDelegate GuardClauseFromBeginToInProgressUsingTriggerSave = null;
    public GuardClauseDelegate GuardClauseFromInProgressToPublishedUsingTriggerAccept = null;
    public GuardClauseDelegate GuardClauseFromInProgressToRejectedUsingTriggerReject = null;
    public GuardClauseDelegate GuardClauseFromInProgressToInProgressUsingTriggerSave = null;
    public GuardClauseDelegate GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = null;
    public GuardClauseDelegate GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = null;
    public UnhandledTriggerDelegate OnUnhandledTrigger = null;

    public BlogPost()
    {
      stateMachine = new StateMachine<State, Trigger>(State.Begin);
      stateMachine.Configure(State.Begin)
        .OnEntry(() => { if (OnBeginEntry != null) OnBeginEntry(); })
        .OnExit(() => { if (OnBeginExit != null) OnBeginExit(); })
        .PermitIf(Trigger.Save, State.InProgress , () => { if (GuardClauseFromBeginToInProgressUsingTriggerSave != null) return GuardClauseFromBeginToInProgressUsingTriggerSave(); return true; } )
      ;
      stateMachine.Configure(State.InProgress)
        .OnEntry(() => { if (OnInProgressEntry != null) OnInProgressEntry(); })
        .OnExit(() => { if (OnInProgressExit != null) OnInProgressExit(); })
        .PermitIf(Trigger.Accept, State.Published , () => { if (GuardClauseFromInProgressToPublishedUsingTriggerAccept != null) return GuardClauseFromInProgressToPublishedUsingTriggerAccept(); return true; } )
        .PermitIf(Trigger.Reject, State.Rejected , () => { if (GuardClauseFromInProgressToRejectedUsingTriggerReject != null) return GuardClauseFromInProgressToRejectedUsingTriggerReject(); return true; } )
        .PermitReentryIf(Trigger.Save , () => { if (GuardClauseFromInProgressToInProgressUsingTriggerSave != null) return GuardClauseFromInProgressToInProgressUsingTriggerSave(); return true; } )
      ;
      stateMachine.Configure(State.Published)
        .OnEntry(() => { if (OnPublishedEntry != null) OnPublishedEntry(); })
        .OnExit(() => { if (OnPublishedExit != null) OnPublishedExit(); })
        .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit(); return true; } )
      ;
      stateMachine.Configure(State.Rejected)
        .OnEntry(() => { if (OnRejectedEntry != null) OnRejectedEntry(); })
        .OnExit(() => { if (OnRejectedExit != null) OnRejectedExit(); })
        .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit(); return true; } )
      ;
      stateMachine.OnUnhandledTrigger((state, trigger) => { if (OnUnhandledTrigger != null) OnUnhandledTrigger(state, trigger); });
    }

    public bool TryFireTrigger(Trigger trigger)
    {
      if (!stateMachine.CanFire(trigger))
      {
        return false;
      }
      stateMachine.Fire(trigger);
      return true;
    }

    public State GetState
    {
      get
      {
        return stateMachine.State;
      }
    }
  }
}
توضیحات:

ماشین حالت فوق دارای چهار حالت شروع، در حال بررسی، منتشر شده و رد شده است. معمول است که این چهار حالت را به شکل یک enum معرفی کنند که در کدهای تولیدی فوق نیز به همین نحو عمل گردیده و public enum State معرف چهار حالت ذکر شده است. همچنین رویدادهای ذخیره، نیاز به ویرایش، ویرایش، تائید و رد نیز توسط public enum Trigger معرفی شده‌اند.
در قسمت Transitions، بر اساس یک رویداد (Trigger در اینجا)، انتقال از یک حالت به حالتی دیگر را سبب خواهیم شد.
تعاریف اصلی تنظیمات ماشین حالت، در سازنده کلاس BlogPostStateMachine انجام شده است. این تعاریف نیز بسیار ساده هستند. به ازای هر حالت، یک Configure داریم. در متدهای OnEntry و OnExit هر حالت، یک سری callback function فراخوانی خواهند شد. برای مثال در حالت Rejected یا Approved می‌توان ایمیلی را به ارسال کننده مطلب جهت یادآوری وضعیت رخ داده، ارسال نمود.
متدهای PermitIf سبب انتقال شرطی، به حالتی دیگر خواهند شد. برای مثال رد یا تائید یک مطلب نیاز به دسترسی مدیریتی خواهد داشت. این نوع موارد را توسط delgateهای Guard ایی که برای مدیریت شرط‌ها ایجاد کرده است، می‌توان تنظیم کرد. PermitReentryIf سبب بازگشت مجدد به همان حالت می‌گردد. برای مثال ویرایش و ذخیره یک مطلب در حال انتشار، سبب تائید یا رد آن نخواهد شد؛ صرفا عملیات ذخیره صورت گرفته و ماشین حالت مجددا در همان مرحله باقی خواهد ماند.

نحوه استفاده از ماشین حالت تولیدی:
همانطور که عنوان شد، حداقل استفاده از ماشین‌های حالت، refactoing انبوهی از if و else‌ها است که در حالت مدیریت یک چنین گردش‌های کاری باید تدارک دید.
namespace StatelessTests
{
    public class BlogPostManager
    {
        private BlogPostStateMachine _stateMachine;
        public BlogPostManager()
        {
            configureWorkflow();
        }

        private void configureWorkflow()
        {
            _stateMachine = new BlogPostStateMachine();

            _stateMachine.GuardClauseFromBeginToInProgressUsingTriggerSave = () => { return UserCanPost; };
            _stateMachine.OnBeginExit = () => { /* save data + save state + send an email to admin */ };

            _stateMachine.GuardClauseFromInProgressToPublishedUsingTriggerAccept = () => { return UserIsAdmin; };
            _stateMachine.GuardClauseFromInProgressToRejectedUsingTriggerReject = () => { return UserIsAdmin; };
            _stateMachine.GuardClauseFromInProgressToInProgressUsingTriggerSave = () => { return UserHasEditRights; };
            _stateMachine.OnInProgressExit = () => { /* save data + save state + send an email to user */ };

            _stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ };
            _stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };

            _stateMachine.OnRejectedExit = () => { /* save data + save state + send an email to admin */ };
            _stateMachine.GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };
        }

        public bool UserIsAdmin
        {
            get
            {
                return true; // TODO: Evaluate if user is an admin.
            }
        }

        public bool UserCanPost
        {
            get
            {
                return true; // TODO: Evaluate if user is authenticated
            }
        }

        public bool UserHasEditRights
        {
            get
            {
                return true; // TODO: Evaluate if user is owner or admin
            }
        }

        // User actions
        public void Save() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Save); }
        public void RequireEdit() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.RequireEdit); }

        // Admin actions        
        public void Accept() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Accept); }
        public void Reject() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Reject); }
    }
}
در کلاس فوق، نحوه استفاده از ماشین حالت تولیدی را مشاهده می‌کنید. در delegateهای Guard، سطوح دسترسی انجام عملیات بررسی خواهند شد. برای مثال، از بانک اطلاعاتی بر اساس اطلاعات کاربر جاری وارد شده به سیستم اخذ می‌گردند. در متدهای Exit هر مرحله، کارهای ذخیره سازی اطلاعات در بانک اطلاعاتی، ذخیره سازی حالت (مثلا در یک فیلد که بعدا قابل بازیابی باشد) صورت می‌گیرد و در صورت نیاز ایمیلی به اشخاص مختلف ارسال خواهد شد.
برای به حرکت درآوردن این ماشین، نیاز به یک سری اکشن متد نیز می‌باشد. تعدادی از این موارد را در انتهای کلاس فوق ملاحظه می‌کنید. کد نویسی آن‌ها در حد فراخوانی متد TryFireTrigger ماشین حالت است.

یک نکته:
ماشین حالت تولیدی به صورت پیش فرض در حالت State.Begin قرار دارد. می‌توان این مورد را از بانک اطلاعاتی خواند و سپس مقدار دهی نمود تا با هر بار وهله سازی ماشین حالت دقیقا مشخص باشد که در چه مرحله‌ای قرار داریم و TryFireTrigger بتواند بر این اساس تصمیم‌گیری کند که آیا مجاز است عملیاتی را انجام دهد یا خیر.
مطالب
مدیریت خروجی نال از اکشن متدهای برنامه‌های ASP.NET Core
فرض کنید اکشن متد Web API شما قرار است اطلاعات رکوردی را بازگشت دهد:
using Microsoft.AspNetCore.Mvc;

namespace Core3xWebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return null;
        }
    }
}
و در این حالت خاص، خروجی کوئری مدنظر، نال است. اگر کلاینت سعی کند این اطلاعات را دریافت نماید، با تصویر/خروجی زیر مواجه خواهد شد:


همانطور که مشاهده می‌کنید، هیچ خروجی تولید نشده و تنها به تنظیم status-code مساوی 204 که به معنای no content است، اکتفا کرده‌است.


چرا از کشن متدهای ASP.NET Core نمی‌توان خروجی نال گرفت؟

فرمت خروجی اکشن متدها در ASP.NET Core بر اساس output formatter‌های متفاوتی مانند JSON ،XML و غیره، تعیین می‌شود. اما زمانیکه به نال می‌رسد، از یک output formatter خاص به نام HttpNoContentOutputFormatter کمک می‌گیرد. کار آن تبدیل null، به خروجی مخصوصی است که توضیح داده شد. به این معنا که هیچگاه خروجی نال برنامه، برای مثال به نحو متداولی تبدیل به یک خروجی استاندارد JSON نمی‌شود و این مساله برای Http Client‌های مختلفی که منتظر یک خروجی استاندارد هستند (مانند انواع برنامه‌های تک صفحه‌ای وب)، عموما مساله ساز است؛ چون هر status-code دیگری بجز 200 را به صورت بروز یک خطا تفسیر می‌کنند که در حالت فوق، فقط به معنای نبود اطلاعات است و نه بروز خطایی.


چگونه خروجی نال را به همان شکلی که هست بازگشت دهیم؟

HttpNoContentOutputFormatter به همراه خاصیت TreatNullValueAsNoContent نیز هست که اگر در ابتدای تنظیمات برنامه به false تنظیم شود، دیگر مقادیر نال را به خروجی no content تبدیل نمی‌کند:
namespace Core3xWebApi
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers(options =>
            {
                // remove formatter that turns nulls into 204 - No Content responses
                // this formatter breaks SPA's Http response JSON parsing
                options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
                options.OutputFormatters.Insert(0, new HttpNoContentOutputFormatter
                {
                    TreatNullValueAsNoContent = false
                });
            });
        }
پس از این تغییر، اگر برنامه‌ی سمت کلاینت، سعی در دریافت اطلاعاتی از API فوق کند، اینبار یک خروجی استاندارد JSON را که در آن null درج شده‌است، دریافت خواهد کرد؛ به همراه status-code مساوی 200 که در سمت کلاینت، به یک خطا نگاشت نخواهد شد (اگر کوئری ما خروجی ندارد، دلیل بر بروز خطایی نبوده؛ فقط اطلاعاتی برای نمایش نیست):

اشتراک‌ها
5.Visual Studio 2017 15.9 منتشر شد

These are the customer-reported issues addressed in 15.9.5:

Security Advisory Notices

5.Visual Studio 2017 15.9 منتشر شد
نظرات مطالب
امکان استفاده از کتابخانه‌های native در Blazor WASM 6x
برای اجرا شدن دستور زیر:
* خط به خط کد‌های زیر را، در cmd و پشت سر هم اجرا کنید
(نکته): باید پایتون را از قبل نصب داشته باشید (آموزش نصب پایتون)
# Get the emsdk repo
git clone https://github.com/emscripten-core/emsdk.git

# Enter that directory
cd emsdk

# Download and install the latest SDK tools.
emsdk install latest

# Make the "latest" SDK "active" for the current user. (writes .emscripten file)
emsdk activate latest

# Activate PATH and other environment variables in the current terminal
emsdk_env.bat
حالا اگر cmd شما در همچین مسیری قرار دارد:
C:\Users\{your pc name}\emsdk
میتوانید دستوری که در متن مقاله ذکر شده را اجرا کنید (البته با اندکی تغییر):
emcc C:\Users\{your pc name}\sqlite\sqlite3.c -shared -o D:\e_sqlite3.o
فایل کامپایل شده e_sqlite3.o رو در پروژه خود کپی کنید و طبق مقاله پیش بروید  و این فایل نیتو را به پروژه رفرنس بدهید.
اکنون اگر دستور dotnet bulid را در ترمینال بزنید،  احتمالا با همچین هشداری مواجه خواهید شد:
warning : @(NativeFileReference) is not empty, but the native references won't be linked in, because neither $(WasmBuildNative), nor $(RunAOTCompilation) are 'true'. NativeFileReference=Data\e_sqlite3.o [D:\project\Yourproject\Yourproject.csproj] 1 Warning(s)
در صورت مشاهده همچین هشداری و حل این مشکل (اضافه نشدن فایل نیتیو به برنامه) ابتدا 
<RunAOTCompilation>true</RunAOTCompilation>
را به قسمت بالای فایل csproj خود و در قسمت 
<PropertyGroup>
اضافه کنید و سپس دستور زیر را اجرا کنید:
dotnet workload install wasm-tools
مطالب
Globalization در ASP.NET MVC - قسمت هفتم
در قسمت قبل مطالب تکمیلی تولید پرووایدر سفارشی منابع دیتابیسی ارائه شد. در این قسمت نحوه بروزرسانی ورودی‌های منابع در زمان اجرا بحث می‌شود.

.

تولید یک پرووایدر منابع دیتابیسی - بخش سوم

برای پیاده‌سازی ویژگی به‌روزرسانی ورودی‌های منابع در زمان اجرا راه‌حل‌های مخنلفی ممکن است به ذهن برنامه‌نویس خطور کند که هر کدام معایب و مزایای خودش را دارد. اما درنهایت بسته به شرایط موجود انتخاب روش مناسب برعهده خود برنامه‌نویس است.

مثلا برای پرووایدر سفارشی دیتابیسی تهیه‌شده در مطالب قبلی، تنها کافی است ابزاری تهیه شود تا به کاربران اجازه به‌روزرسانی مقادیر موردنظرشان در دیتابیس را بدهد که کاری بسیار ساده است. بدین ترتیب به‌روزرسانی این مقادیر در زمان اجرا کاری بسیار ابتدایی به نظر می‌رسد. اما در قسمت قبل نشان داده شد که برای بالا بردن بازدهی بهتر است که مقادیر موجود در دیتابیس در حافظه سرور کش شوند. استراتژی اولیه و ساده‌ای نیز برای نحوه پیاده‌سازی این فرایند کشینگ ارائه شد. بنابراین باید امکاناتی فراهم شود تا درصورت تغییر مقادیر کش‌شده در سمت دیتابیس، برنامه از این تغییرات آگاه شده و نسبت به به‌روزرسانی این مقادیر در متغیر کشینگ اقدامات لازم را انجام دهد.

اما همان‌طور که در قسمت قبل نیز اشاره شد، نکته‌ای که باید درنظر داشت این است که مدیریت تمامی نمونه‌های تولیدشده از کلاس‌های موردبحث کاملا برعهده ASP.NET است، بنابراین دسترسی مستقیمی به این نمونه‌ها در بیرون و در زمان اجرا وجود ندارد تا این ویژگی را بتوان در مورد آن‌ها پیاده کرد.

یکی از روش‌های موجود برای حل این مشکل این است که مکانیزمی پیاده شود تا بتوان به تمامی نمونه‌های تولیدی از کلاس DbResourceManager در بیرون از محیط سیستم مدیریت منابع ASP.NET دسترسی داشت. مثلا یک کلاس حاول متغیری استاتیک جهت ذخیره نمونه‌های تولیدی از کلاس DbResourceManager، به کتابخانه خود اضافه کرد تا با استفاده از یکسری امکانات بتوان این نمونه‌های تولیدی را از تغییرات رخداده در سمت دیتابیس آگاه کرد. در این قسمت پیاده‌سازی این راه‌حل شرح داده می‌شود.


نکته: قبل از هرچیز برای مناسب شدن طراحی کتابخانه تولیدی و افزایش امنیت آن بهتر است تا سطح دسترسی تمامی کلاس‌های پیاده‌سازی شده تا این مرحله به internal تغییر کند. ازآنجاکه سیستم مدیریت منابع ASP.NET از ریفلکشن برای تولید نمونه‌های موردنیاز خود استفاده می‌کند، بنابراین این تغییر تاثیری بر روند کاری آن نخواهد گذاشت.


نکته: با توجه به شرایط خاص موجود، ممکن است نام‌های استفاده شده برای کلاس‌های این کتابخانه کمی گیج‌کننده باشد. پس با دقت بیشتری به مطلب توجه کنید.


پیاده‌سازی امکان پاک‌سازی مقادیر کش‌شده

برای این‌کار باید تغییراتی در کلاس DbResourceManager داده شود تا بتوان این کلاس را از تغییرات بوجود آمده آگاه ساخت. روشی که من برای این کار درنظر گرفتم استفاده از یک اینترفیس حاوی اعضای موردنیاز برای پیاده‌سازی این امکان است تا مدیریت این ویژگی در ادامه راحت‌تر شود.


اینترفیس IDbCachedResourceManager

این اینترفیس به صورت زیر تعریف شده است:

namespace DbResourceProvider
{
  internal interface IDbCachedResourceManager
  {
    string ResourceName { get; }

    void ClearAll();
    void Clear(string culture);
    void Clear(string culture, string resourceKey);
  }
}

در پراپرتی فقط خواندنی ResourceName نام منبع کش شده ذخیره خواهد شد.

متد ClearAll برای پاک‌سازی تمامی ورودی‌های کش‌شده استفاده می‌شود.

متدهای Clear برای پاک‌سازی ورودی‌های کش‌شده یک کالچر به خصوص و یا یک ورودی خاص استفاده می‌شود.

با استفاده از این اینترفیس، پیاده‌سازی کلاس DbResourceManager به صورت زیر تغییر می‌کند:

using System.Collections.Generic;
using System.Globalization;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  internal class DbResourceManager : IDbCachedResourceManager
  {
    private readonly string _resourceName;
    private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture;
    public DbResourceManager(string resourceName)
    {
      _resourceName = resourceName;
      _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>();
    }
    public object GetObject(string resourceKey, CultureInfo culture) { ... }
    private object GetCachedObject(string resourceKey, string cultureName) { ... }

    #region Implementation of IDbCachedResourceManager
    public string ResourceName
    {
      get { return _resourceName; }
    }
    public void ClearAll()
    {
      lock (this)
      {
        _resourceCacheByCulture.Clear(); 
      }
    }
    public void Clear(string culture)
    {
      lock (this)
      {
        if (!_resourceCacheByCulture.ContainsKey(culture)) return;
        _resourceCacheByCulture[culture].Clear(); 
      }
    }
    public void Clear(string culture, string resourceKey)
    {
      lock (this)
      {
        if (!_resourceCacheByCulture.ContainsKey(culture)) return;
        _resourceCacheByCulture[culture].Remove(resourceKey); 
      }
    }
    #endregion
  }
}

اعضای اینترفیس IDbCachedResourceManager به صورت مناسبی در کد بالا پیاده‌سازی شدند. در تمام این پیاده‌سازی‌ها مقادیر مربوطه از درون متغیر کشینگ پاک می‌شوند تا پس از اولین درخواست، بلافاصله از دیتابیس خوانده شوند. برای جلوگیری از دسترسی هم‌زمان نیز از بلاک lock استفاده شده است.

برای استفاده از این امکانات جدید همان‌طور که در بالا نیز اشاره شد باید بتوان نمونه‌های تولیدی از کلاس DbResourceManager توسط ASP.NET درون متغیری استاتیک ذخیره شوند. برای اینکار از کلاس جدیدی با عنوان DbResourceCacheManager استفاده می‌شود که برخلاف تمام کلاس‌های تعریف‌شده تا اینجا با سطح دسترسی public تعریف می‌شود.


کلاس DbResourceCacheManager

مدیریت نمونه‌های تولیدی از کلاس DbResourceManager در این کلاس انجام می‌شود. این کلاس پیاده‌سازی ساده‌ای به‌صورت زیر دارد:

using System.Collections.Generic;
using System.Linq;
namespace DbResourceProvider
{
  public static class DbResourceCacheManager
  {
    internal static List<IDbCachedResourceManager> ResourceManagers { get; private set; }
    static DbResourceCacheManager()
    {
      ResourceManagers = new List<IDbCachedResourceManager>();
    }
    public static void ClearAll()
    {
      ResourceManagers.ForEach(r => r.ClearAll());
    }
    public static void Clear(string resourceName)
    {
      GetResouceManagers(resourceName).ForEach(r => r.ClearAll());
    }
    public static void Clear(string resourceName, string culture)
    {
      GetResouceManagers(resourceName).ForEach(r => r.Clear(culture));
    }
    public static void Clear(string resourceName, string culture, string resourceKey)
    {
      GetResouceManagers(resourceName).ForEach(r => r.Clear(culture, resourceKey));
    }

    private static List<IDbCachedResourceManager> GetResouceManagers(string resourceName)
    {
      return ResourceManagers.Where(r => r.ResourceName.ToLower() == resourceName.ToLower()).ToList();
    }
  }
}

ازآنجاکه نیازی به تولید نمونه ای از این کلاس وجود ندارد، این کلاس به صورت استاتیک تعریف شده است. بنابراین تمام اعضای درون آن نیز استاتیک هستند.

از پراپرتی ResourceManagers برای نگهداری لیستی از نمونه‌های تولیدی از کلاس DbResourceManager استفاده می‌شود. این پراپرتی از نوع <List<IDbCachedResourceManager تعریف شده است و برای جلوگیری از دسترسی بیرونی، سطح دسترسی آن internal درنظر گرفته شده است.

در کانستراکتور استاتیک این کلاس (اطلاعات بیشتر درباره static constructor در اینجا) این پراپرتی با مقداردهی به یک نمونه تازه از لیست، اصطلاحا initialize می‌شود.

سایر متدها نیز برای فراخوانی متدهای موجود در اینترفیس IDbCachedResourceManager پیاده‌سازی شده‌اند. تمامی این متدها دارای سطح دسترسی public هستند. همان‌طور که می‌بینید از خاصیت ResourceName برای مشخص‌کردن نمونه موردنظر استفاده شده است که دلیل آن در قسمت قبل شرح داده شده است.

دقت کنید که برای اطمینان از انتخاب درست همه موارد موجود در شرط انتخاب نمونه موردنظر در متد GetResouceManagers از متد ToLower برای هر دو سمت شرط استفاده شده است.


نکته مهم: درباره علت برگشت یک لیست از متد انتخاب نمونه موردنظر از کلاس DbResourceManager در کد بالا (یعنی متد GetResouceManagers) باید نکته‌ای اشاره شود. در قسمت قبل عنوان شد که سیستم مدیریت منابع ASP.NET نمونه‌های تولیدی از پرووایدرهای منابع را به ازای هر منبع کش می‌کند. اما یک نکته بسیار مهم که باید به آن توجه کرد این است که این کش برای «عبارات بومی‌سازی ضمنی» و نیز «متد مربوط به منابع محلی» موجود در کلاس HttpContext و یا نمونه مشابه آن در کلاس TemplateControl (همان متد GetLocalResourceObject که درباره این متدها در قسمت سوم این سری شرح داده شده است) از یکدیگر جدا هستند و استفاده از هریک از این دو روش موجب تولید یک نمونه مجزا از پرووایدر مربوطه می‌شود که متاسفانه کنترل آن از دست برنامه نویس خارج است. دقت کنید که این اتفاق برای منابع کلی رخ نمی‌دهد.

بنابراین برای پاک کردن مناسب ورودی‌های کش‌شده در کلاس فوق به جای استفاده از متد Single در انتخاب نمونه موردنظر از کلاس DbResourceManager (در متد GetResouceManagers) از متد Where استفاده شده و یک لیست برگشت داده می‌شود. چون با توجه به توضیح بالا امکان وجود دو نمونه DbResourceManager از یک منبع درخواستی محلی در لیست نمونه‌های نگهداری شده در این کلاس وجود دارد.

.

افزودن نمونه‌ها به کلاس DbResourceCacheManager

برای نگهداری نمونه‌های تولید شده از DbResourceManager، باید در یک قسمت مناسب این نمونه‌ها را به لیست مربوطه در کلاس DbResourceCacheManager اضافه کرد. بهترین مکان برای انجام این عمل در کلاس پایه BaseDbResourceProvider است که درخواست تولید نمونه را در متد EnsureResourceManager درصورت نال بودن آن می‌دهد. بنابراین این متد را به صورت زیر تغییر می‌دهیم:

private void EnsureResourceManager()
{
  if (_resourceManager != null) return;
  {
    _resourceManager = CreateResourceManager();
    DbResourceCacheManager.ResourceManagers.Add(_resourceManager);
  }
}

تا اینجا کار پیاده‌سازی امکان مدیریت مقادیر کش‌شده در کتابخانه تولیدی به پایان رسیده است.

استفاده از کلاس DbResourceCacheManager

پس از پیاده‌سازی تمامی موارد لازم، حالتی را درنظر بگیرید که مقادیر ورودی‌های تعریف شده در منبع "dir1/page1.aspx" تغییر کرده است. بنابراین برای بروزرسانی مقادیر کش‌شده کافی است تا از کدی مثل کد زیر استفاده شود:

DbResourceCacheManager.Clear("dir1/page1.aspx");

کد بالا کل ورودی‌های کش‌شده برای منبع "dir1/page1.aspx" را پاک می‌کند. برای پاک کردن کالچر یا یک ورودی خاص نیز می‌توان از کدهایی مشابه زیر استفاده کرد:

DbResourceCacheManager.Clear("Default.aspx", "en-US");
DbResourceCacheManager.Clear("GlobalTexts", "en-US", "Yes");

.

دریافت کد پروژه

کد کامل پروژه DbResourceProvider به همراه مثال و اسکریپت‌های دیتابیسی مربوطه از لینک زیر قابل دریافت است:

DbResourceProvider.rar

برای استفاده از این مثال ابتدا باید کتابخانه Entity Framework (با نام EntityFramework.dll) را مثلا از طریق نوگت دریافت کنید. نسخه‌ای که من در این مثال استفاده کردم نسخه 4.4 با حجم حدود 1 مگابایت است.

نکته: در این کد یک بهبود جزئی اما مهم در کلاس ResourceData اعمال شده است. در قسمت سوم این سری، اشاره شد که نام ورودی‌های منابع Case Sensitive نیست. بنابراین برای پیاده‌سازی این ویژگی، متدهای این کلاس باید به صورت زیر تغییر کنند:

public Resource GetResource(string resourceKey, string culture)
{
  using (var data = new TestContext())
  {
    return data.Resources.SingleOrDefault(r => r.Name.ToLower() == _resourceName.ToLower() && r.Key.ToLower() == resourceKey.ToLower() && r.Culture == culture);
  }
}

public List<Resource> GetResources(string culture)
{
  using (var data = new TestContext())
  {
    return data.Resources.Where(r => r.Name.ToLower() == _resourceName.ToLower() && r.Culture == culture).ToList();
  }
}
تغییرات اعمال شده همان استفاده از متد ToLower در دو طرف شرط مربوط به نام منابع و کلید ورودی‌هاست.


در آینده...

در ادامه مطالب، بحث تهیه پرووایدر سفارشی فایلهای resx. برای پیاده‌سازی امکان به‌روزرسانی در زمان اجرا ارائه خواهد شد. بعد از پایان تهیه این پرووایدر سفارشی، این سری مطالب با ارائه نکات استفاده از این پرووایدرها در ASP.NET MVC پایان خواهد یافت.


منابع

http://msdn.microsoft.com/en-us/library/aa905797.aspx

http://www.west-wind.com/presentations/wwdbresourceprovider

مطالب
MethodCallSerializer یا Serialize کردن فراخوانی متد

مدتی پیش نیاز پیدا کردم تا فراخوانی متدهایی را Serialize کرده و در مواقعی خاص، آن متدها را فراخوانی کنم که نتیجه‌ی آن را در زیر با هم می‌بینیم. 

در نظر بگیرید متدی داریم به شکل زیر: 

public class EmailSender
    {
        public void Send(string emailAddress)
        {
            Console.WriteLine($"an email was sent to {emailAddress}");
        }
    }

و می‌خواهیم نحوه فراخوانی این متد را Serialize کرده

new EmailSender().Send("eng.younos1986@gmail.com")

یعنی با چنین کدی، متد مورد نظر را Serialize کرده، Type ، Method.Name و Argument ‌های آن را بدست آورده و در دیتابیس ذخیره کنیم:

Serialize(() => new EmailSender().Send("eng.younos1986@gmail.com"));
که در ادامه برای Serialize کردن به صورت زیر عمل می‌کنیم:  
public void Serialize(Expression<Action> methodCall)
        {
            var callExpression = methodCall.Body as MethodCallExpression;

            var rawArguments = new object[callExpression.Arguments.Count];
            var i = 0;
            foreach (var argg in callExpression.Arguments)
            {
                rawArguments[i++] = Expression.Lambda(Expression.Convert(argg, argg.Type)).Compile().DynamicInvoke();
            }
            string typeName = string.Empty;
            var serializedArgumentsObject = rawArguments.ObjectToByteArray();              //todo: save this to db as method parameters  [Binary field]

            if (callExpression.Object != null)                                             //todo: save this to db as class name to be instanciated later {nvarchar field}
                typeName = callExpression.Object.Type.ToString();                          // instance methods
            else
                typeName = callExpression.Method.ReflectedType.ToString();                 // static methods

            var methodname = callExpression.Method.Name;                                   //todo: save this to db as method name to be called via reflection [nvarchar field]
            var deserializedArgumentsObject = serializedArgumentsObject.ByteArrayToObject();                                             //todo:  retrieve  serializedObject fro db and deserialize 
            var objInstance = GetInstance(typeName);                                       //todo:  retrieve  typeName fro db and deserialize 
            if (objInstance != null)
            {
                objInstance.GetType().GetMethod(methodname).Invoke(objInstance, (object[])deserializedArgumentsObject);
            }
        }  


برای اینکه بتوانیم نحوه فراخوانی متد را به صورت Lambda Expressions به متد Serialize بفرستیم، باید نوع پارارمتر آن از جنس <Expression<Action باشد. 
بعد با Cast کردن methodCall.Body به MethodCallExpression می‌توانیم آرگیومنت‌ها، نام متد و Type آن را بدست بیاوریم:



در خطوط 5 تا 10 کد بالا، آرایه‌ای به طول تعداد Argument ها ساخته و هر Argument را به نوع خودش تبدیل کرده و درون آرایه می‌ریزیم. 

var rawArguments = new object[callExpression.Arguments.Count];
var i = 0;
foreach (var argg in callExpression.Arguments)
{
    rawArguments[i++] = Expression.Lambda(Expression.Convert(argg, argg.Type)).Compile().DynamicInvoke();
}  

و با این کد 

Expression.Lambda(Expression.Convert(argg, argg.Type)).Compile().DynamicInvoke();

هر آرگیومنت را به نوع خودش تبدیل کرده سپس Lambdaی آن را ساخته و کامپایل کرده تا Expression Tree به delegate  تبدیل شده ( به کد معادل IL تبدیل شده)، سپس آن را با متد DynamicInvoke  اجرا کرده تا دقیقا معادل آرگیومنت ارسالی را بدست آورده و در آرایه ذخیره می‌کنیم. در آخر آرایه بدست آمده را به بایت تبدیل کرده و می‌توان در یک منبع داده دائمی ذخیره کرد.

با کد زیر، Typeی که متد، درون آن قرار دارد را بدست می‌آوریم:

if (callExpression.Object != null)                            
    typeName = callExpression.Object.Type.ToString();         
else
    typeName = callExpression.Method.ReflectedType.ToString();

 و همچنین نام متد:

var methodname = callExpression.Method.Name;

تا به اینجای کار Type، نام متد و آرگیومنت‌های Serialize شده آن بدست آمدند که می‌توان آنها را در دیتابیس ذخیره کرد. سپس استخراج، DeSerialize و فراخوانی کرد:

نحوه فراخوانی:

var methodname = callExpression.Method.Name;                                   
var deserializedArgumentsObject = serializedArgumentsObject.ByteArrayToObject();                           
var objInstance = GetInstance(typeName);                                        
if (objInstance != null)
{
    objInstance.GetType().GetMethod(methodname).Invoke(objInstance, (object[])deserializedArgumentsObject);
}

و برای instance ساختن از Type مورد نظر از کد زیر استفاده می‌کنیم:

public object GetInstance(string strFullyQualifiedName)
        {
            Type type = Type.GetType(strFullyQualifiedName);
            if (type != null)
                return Activator.CreateInstance(type);

            foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
            {
                type = asm.GetType(strFullyQualifiedName);
                if (type != null)
                    return Activator.CreateInstance(type);
            }
            return null;
        }


موارد استفاده: نوشتن نرم افزارهایی برای مدیریت یک سری Job

کد کامل MethodCallSerialization.rar