مطالب
اسکرول روان لیست‌های مجازی سازی شده در 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">
مطالب
سطح دوم کش در Nhibernate 4
 Second Level Cache In NHibernate 4

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

جهت پیاده سازی باید قسمت‌های ذیل را در کانفیگ مربوط به NHibernate اضافه نمود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
         <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate"/>
          <section name="syscache" type="NHibernate.Caches.SysCache.SysCacheSectionHandler, NHibernate.Caches.SysCache" requirePermission="false"/>
</configSections>

<hibernate-configuration  xmlns="urn:nhibernate-configuration-2.2" >
<session-factory>
        <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
      <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
      <property name="connection.connection_string_name">LocalSqlServer</property>
      <property name="show_sql">false</property>
      <property name="hbm2ddl.keywords">none</property>
      <property name="cache.use_second_level_cache">true</property>
      <property name="cache.use_query_cache" >true</property>      
      <property name="cache.provider_class">NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache</property>
</session-factory>
</hibernate-configuration>

<syscache>
       <cache region="LongExpire" expiration="3600" priority="5"/>
       <cache region="ShortExpire" expiration="600" priority="3"/>    
</syscache>
</configuration>

پیاده سازی Caching در NHibernate در سه مرحله قابل اعمال می‌باشد :
- کش در سطح Load موجودیت‌های مستقل
- کش در سطح Load موجودیت‌های وابسته Bag , List , Set , …
- کش در سطح Query ها

Providerهای مختلفی برای اعمال و پیاده سازی آن وجود دارند که معروف‌ترین آن‌ها SysCache بوده و ما هم از همان استفاده می‌نماییم.
- مدت زمان پیش فرض کش سطح دوم، ۵ دقیقه می‌باشد و در صورت نیاز به تغییر آن، باید تگ مربوط به SysCache را تنظیم نمود. محدودیتی در تعریف تعداد متفاوتی از زمان‌های خالی شدن کش وجود ندارد و مدت زمان آن بر حسب ثانیه مشخص می‌گردد. نحوه‌ی تخصیص زمان انقضای کش به هر مورد بدین شکل صورت می‌گیرد که region مربوطه در آن معرفی می‌گردد.

جهت اعمال کش در سطح Load موجودیت‌های مستقل، علاوه بر کانفیگ اصلی، می‌بایستی کدهای زیر را به Mapping موجودیت اضافه نمود مانند :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model">
  <class name="Organization" table="Core_Enterprise_Organization">
    <cache usage="nonstrict-read-write" region="ShortExpire"/>                  
    <id name="Id" >
      <generator/>
    </id>
    <version name="Version"/>
    <property name="Title" not-null="true" unique="true"/>
    <property name="Code" not-null="true" unique="true"/>
  </class>
</hibernate-mapping>
این مورد برای موجودیت‌های وابسته هم نیز صادق است؛ به شکل کد زیر:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model">
  <class name="Party" table="Core_Enterprise_Party">  
    <id name="Id" >
      <generator />
    </id>
    <version name="Version"/>
    <property name="Username" unique="true"/>
    <property name="DisplayName" not-null="true"/>
    <bag name="PartyGroups" inverse="true" table="Core_Enterprise_PartyGroup" cascade="all-delete-orphan">      
      <cache usage="nonstrict-read-write" region="ShortExpire"/>
      <key column="Party_id_fk"/>
      <one-to-many/>
    </bag>
  </class>
</hibernate-mapping>

ویژگی usage نیز با مقادیر زیر قابل تنظیم است:

- read-only : این مورد جهت موجودیت‌هایی مناسب است که امکان بروزرسانی آن‌ها توسط کاربر وجود ندارد. این مورد بهترین کارآیی را دارد.
- read-write : این مورد جهت موجودیت‌هایی بکار می‌رود که امکان بروزرسانی آن‌ها توسط کاربر وجود دارد. این مورد کارآیی پایین‌تری دارد.
- nonstrict-read-write : این مورد جهت موجودیت‌هایی مناسب می‌باشد که امکان بروزرسانی آن‌ها توسط کاربر وجود دارد؛ اما امکان همزمان بروز کردن آن‌ها توسط چندین کاربر وجود نداشته باشد. این مورد در قیاس، کارآیی بهتر و بهینه‌تری نسبت به مورد قبل دارد.

جهت اعمال کش در کوئری‌ها نیز باید مراحل خاص خودش را انجام داد. به عنوان مثال برای یک کوئری Linq به شکل زیر خواهیم داشت:
public IList<Organization> Search(QueryOrganizationDto dto)
{
     var q = SessionInstance.Query<Organization>();            

     if (!String.IsNullOrEmpty(dto.Title))
          q = q.Where(x => x.Title.Contains(dto.Title));

     if (!String.IsNullOrEmpty(dto.Code))
          q = q.Where(x => x.Code.Contains(dto.Code));

     q = q.OrderBy(x => x.Title);

      q = q.CacheRegion("ShortExpire").Cacheable(); 
     return q.ToList();
}
در واقع کد اضافه شده به کوئری بالا، قابل کش بودن کوئری را مشخص می‌نماید و مدت زمان کش شدن آن نیز از طریق کانفیگ مربوطه مشخص می‌گردد. این نکته را هم درنظر داشته باشید که کش در سطح کوئری برای کوئری‌هایی که دقیقا مثل هم هستند اعمال می‌گردد و با افزوده یا کاسته شدن یک شرط جدید به کوئری، مجددا کوئری سمت پایگاه داده ارسال می‌گردد.


در انتها لینک‌های زیر هم جهت مطالعه بیشتر پیشنهاد می‌گردند:
http://www.nhforge.org/doc/nh/en/index.html#performance-cache-readonly
http://nhforge.org/blogs/nhibernate/archive/2009/02/09/quickly-setting-up-and-using-nhibernate-s-second-level-cache.aspx
http://www.klopfenstein.net/lorenz.aspx/using-syscache-as-secondary-cache-in-nhibernate
http://stackoverflow.com/questions/1837651/hibernate-cache-strategy
نظرات مطالب
حذف فضاهای خالی در خروجی صفحات ASP.NET MVC
با تشکر از مطلب ارسالی شما 
برای اینکه فضای خالی به درستی حذف شود و همچنین تگ Pre هم در این الگوریتم لحاظ نشود. می‌توان از اکشن فیلتر زیر استفاده کرد 
public class RemoveWhitespacesAttribute : ActionFilterAttribute
    {

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {

            var response = filterContext.HttpContext.Response;
      
            if (filterContext.HttpContext.Request.RawUrl != "/sitemap.xml")
            {

                if (response.ContentType == "text/html" && response.Filter != null)
                {
                    response.Filter = new HelperClass(response.Filter);
                }
            }
        }

        private class HelperClass : Stream
        {

            private System.IO.Stream Base;

            public HelperClass(System.IO.Stream ResponseStream)
            {

                if (ResponseStream == null)
                    throw new ArgumentNullException("ResponseStream");
                this.Base = ResponseStream;
            }

            StringBuilder s = new StringBuilder();

            public override void Write(byte[] buffer, int offset, int count)
            {

                string HTML = Encoding.UTF8.GetString(buffer, offset, count);

                Regex reg = new Regex(@"(?<=\s)\s+(?![^<>]*</pre>)");
                HTML = reg.Replace(HTML, string.Empty);

                buffer = System.Text.Encoding.UTF8.GetBytes(HTML);
                this.Base.Write(buffer, 0, buffer.Length);
            }

            #region Other Members

            public override int Read(byte[] buffer, int offset, int count)
            {

                throw new NotSupportedException();
            }

            public override bool CanRead { get { return false; } }

            public override bool CanSeek { get { return false; } }

            public override bool CanWrite { get { return true; } }

            public override long Length { get { throw new NotSupportedException(); } }

            public override long Position
            {

                get { throw new NotSupportedException(); }
                set { throw new NotSupportedException(); }
            }

            public override void Flush()
            {

                Base.Flush();
            }

            public override long Seek(long offset, SeekOrigin origin)
            {

                throw new NotSupportedException();
            }

            public override void SetLength(long value)
            {

                throw new NotSupportedException();
            }

            #endregion
        }

    }
برای اجرا هم در Global.asax آن را فراخوانی کرد.  
 protected void Application_Start()
        {
            try
            {
                GlobalFilters.Filters.Add(new App_Start.RemoveWhitespacesAttribute());
            }
            catch
            {
                HttpRuntime.UnloadAppDomain(); // سبب ری استارت برنامه و آغاز مجدد آن با درخواست بعدی می‌شود
                throw;
            }

        }
در نهایت خروجی به شکل زیر رندر می‌شود

برای Gzip هم  اکثر در این حالت که هردو مورد با هم قرار داده شده است در برخی از موارد فایل‌های جاواسکریپ را با مشکل روبرو می‌کند .به نظر من از Gzip توکار IIS استفاده شود بهتر است. البته باید ماژول آن در ISS فعال شده باشد.

برای اینکار هم داخل Web.config کد‌های زیر را داخل configuration قرار بدید.


<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files">
      <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" />
      <dynamicTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/x-javascript" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="application/json" enabled="true" />
        <add mimeType="application/json; charset=utf-8" enabled="true" />
        <add mimeType="application/atom+xml" enabled="true" />
        <add mimeType="application/xaml+xml" enabled="true" />
        <add mimeType="*/*" enabled="false" />
      </dynamicTypes>
      <staticTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/x-javascript" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="application/json" enabled="true" />
        <add mimeType="application/json; charset=utf-8" enabled="true" />
        <add mimeType="application/atom+xml" enabled="true" />
        <add mimeType="application/xaml+xml" enabled="true" />
        <add mimeType="*/*" enabled="false" />
      </staticTypes>
    </httpCompression>
    <urlCompression doStaticCompression="true" doDynamicCompression="true" />
  </system.webServer>
  <location path="Default Web Site">
    <system.webServer>
      <serverRuntime enabled="true"
         frequentHitThreshold="1"
         frequentHitTimePeriod="10:00:00" />
    </system.webServer>
  </location>


نظرات مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
- پویا هست و خروجی دسترسی هم گرفتید. زمانیکه تعریف می‌کنید:
new Field { Type = column.DataType }
یعنی در لیست نهایی، خاصیتی با نام ثابت Type و با مقدار متغیر column.DataType را تولید کن (نام خاصیت، مقدار ثابت نام خاصیت را در JSON نهایی تشکیل می‌دهد).
+ نیازی هم به این همه پیچیدگی نداشت. تمام کارهایی را که انجام دادید با تهیه خروجی ساده <List<Field از یک متد دلخواه، یکی هست و نیازی به anonymous type کار کردن نبود.
- به همان کلاس فیلد، خواص دیگر مورد نیاز را اضافه کنید (عنوان و سایر مشخصات یک فیلد) و در نهایت لیست ساده <List<Field را بازگشت دهید. هر خاصیت کلاس Field، یک ستون گرید را تشکیل می‌دهد.
- همچنین دقت داشته باشید اگر از روش مطلب جاری استفاده می‌کنید، اطلاعات ستون‌‌های نهایی باید در فیلد Data نهایی قرار گیرند (قسمت «پیشنیاز تامین داده مخصوص Kendo UI Grid» در بحث).
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش دوم

Curry and Partial methods

Curry – در ریاضیات و علوم کامپیوتر، currying روشی است برای ترجمه تابعی که آرگومان‌های متعددی می‌گیرد و به صورت ارزیابی دنباله‌ای‌است از توابع که هر کدام یک آرگومان دارند.
برای پیاده سازی آن در #C، از extension methods استفاده می‌کنیم.
public static class CurryMethodExtensions
{
    public static Func< A, Func< B, Func< C, R > > > Curry< A, B, C, R >( this Func< A, B, C, R > f )
    {
        return a => b => c => f( a, b, c );
    }
}
مثالی برای استفاده از متد بالا:
Func< int, int, int, int > addNumbers = ( x, y, z ) => x + y + z;
var f1 = addNumbers.Curry();
Func< int, Func< int, int > > f2 = f1( 3 );
Func< int, int > f3 = f2( 4 );
Console.WriteLine( f3( 5 ) );
بعد از فراخوانی متد Curry می‌توان از کلمه کلیدی var در دستورات بعدی بجای تعریف نوع متغیرها استفاده کرد.
نحوه اجرای دستورات بالا را در تصویر زیر می‌توانید مشاهده کنید:


Partial – در علوم کامپیوتر، قسمتی از یک برنامه (یا قسمتی از یک تابع برنامه) است که اشاره به روند تثبیت تعدادی از آرگومان‌ها به یک تابع و تولید تعداد آرگومان‌های کمتر تابع دیگری را می‌گویند.
public static class CurryMethodExtensions
{
    public static Func< C, R > Partial< A, B, C, R >( this Func< A, B, C, R > f, A a, B b )
    {
        return c => f( a, b, c );
    }
}
مثالی برای استفاده از تابع بالا:
Func< int, int, int, int > sumNumbers = ( x, y, z ) => x + y + z;
Func< int, int > f4 = sumNumbers.Partial( 3, 4 );
Console.WriteLine( f4( 5 ) );
بعد از فراخوانی متد Curry می‌توان از کلمه کلیدی var در دستورات بعدی بجای تعریف نوع متغیرها استفاده کرد.
نحوه اجرای دستورات بالا را در تصویر زیر می‌توانید مشاهده کنید:

WeakReference

یک ارجاع ضعیف به GC اجازه می‌دهد که یک شیء را جمع آوری کند، در عین حالی که برنامه امکان دسترسی به آن را خواهد داشت. در صورتیکه نیاز به شیء‌ای داشته باشید، می‌توانید یک ارجاع قوی را از آن داشته باشید و از جمع آوری آن توسط GC جلوگیری کنید.
var obj = new WeakReferenceTest
            {
                FirstName = "Vahid"
            };
var w = new WeakReference(obj);
obj = null;
GC.Collect();
var weakReferenceTest = w.Target as WeakReferenceTest;
if ( weakReferenceTest != null )
    Console.WriteLine( weakReferenceTest.FirstName );

Lazy<T>

برای ایجاد یک شیء بزرگ، پردازش زیاد منابع و یا اجرای یک وظیفه (task) با پردازش زیاد منابع، به خصوص در زمانیکه ایجاد و یا اجرای این فرآیند در طول عمر یک برنامه، ممکن است هرگز رخ ندهد، از Lazy استفاده می‌شود.
public abstract class ThreadSafeLazyBaseSingleton< T > where T : new()
{
    static readonly Lazy< T > lazy = new Lazy< T >( () => new T() );

    public static T Instance => lazy.Value;
}

BigInteger

نوع داده BigInteger یک نوع تغییر ناپذیر (immutable type) و نمایانگر یک عدد صحیح بزرگ دلخواه است که مقدار آن در تئوری در هیچ حد و مرز حداقل و حداکثری نیست. این نوع، از دیگر انواع جدایی ناپذیر (integral types) در NET.، که دارای خصوصیت MinValue و  MaxValue هستند، متفاوت است.
var positiveString = "91389681247993671255433422114345532000000";
var negativeString = "-9031583741089631207100208803453423537140000";
var posBigInt = BigInteger.Parse( positiveString );
Console.WriteLine( posBigInt );
var negBigInt = BigInteger.Parse( negativeString );
Console.WriteLine( negBigInt );
نکته: از آنجایی که BigInteger یک نوع تغییر ناپذیر و بدون حد و مرز حداقل و حداکثر است، برای بعضی از عملیات‌، اگر مقدار آن را بسیار زیاد افزایش دهید خطای OutOfMemoryException رخ می‌دهد (البته من با 1024 بار ضرب متغیر positiveString در خودش هم نتوانستم این پیام خطا را ببینم).
مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش سوم
تغییر الگوریتم پیش فرض هش کردن کلمه‌های عبور ASP.NET Identity

کلمه‌های عبور کاربران فعلی سیستم با الگوریتمی متفاوت از الگوریتم مورد استفاده Identity هش شده‌اند. برای اینکه کاربرانی که قبلا ثبت نام کرده بودند بتوانند با کلمه‌های عبور خود وارد سایت شوند، باید الگوریتم هش کردن Identity را با الگوریتم فعلی مورد استفاده Iris جایگزین کرد.

برای تغییر روش هش کردن کلمات عبور در Identity باید اینترفیس IPasswordHasher را پیاده سازی کنید:
    public class IrisPasswordHasher : IPasswordHasher
    {
        public string HashPassword(string password)
        {
            return Utilities.Security.Encryption.EncryptingPassword(password);
        }

        public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            return Utilities.Security.Encryption.VerifyPassword(providedPassword, hashedPassword) ?
                                                                PasswordVerificationResult.Success :
                                                                PasswordVerificationResult.Failed;
        }
    }

  سپس باید وارد کلاس ApplicationUserManager شده و در سازنده‌ی آن اینترفیس IPasswordHasher را به عنوان وابستگی تعریف کنید:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store,
            IUnitOfWork uow,
            IIdentity identity,
            IApplicationRoleManager roleManager,
            IDataProtectionProvider dataProtectionProvider,
            IIdentityMessageService smsService,
            IIdentityMessageService emailService, IPasswordHasher passwordHasher)
            : base(store)
        {
            _store = store;
            _uow = uow;
            _identity = identity;
            _users = _uow.Set<ApplicationUser>();
            _roleManager = roleManager;
            _dataProtectionProvider = dataProtectionProvider;
            this.SmsService = smsService;
            this.EmailService = emailService;
            PasswordHasher = passwordHasher;
            createApplicationUserManager();
        }

برای اینکه کلاس IrisPasswordHasher را به عنوان نمونه درخواستی IPasswordHasher معرفی کنیم، باید در تنظیمات StructureMap کد زیر را نیز اضافه کنید:
x.For<IPasswordHasher>().Use<IrisPasswordHasher>();

پیاده سازی اکشن متد ثبت نام کاربر با استفاده از Identity

در کنترلر UserController، اکشن متد Register را به شکل زیر بازنویسی کنید:
        [HttpPost]
        [ValidateAntiForgeryToken]
        [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")]
        public virtual async Task<ActionResult> Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser
                {
                    CreatedDate = DateAndTime.GetDateTime(),
                    Email = model.Email,
                    IP = Request.ServerVariables["REMOTE_ADDR"],
                    IsBaned = false,
                    UserName = model.UserName,
                    UserMetaData = new UserMetaData(),
                    LastLoginDate = DateAndTime.GetDateTime()
                };

                var result = await _userManager.CreateAsync(user, model.Password);

                if (result.Succeeded)
                {
                    var addToRoleResult = await _userManager.AddToRoleAsync(user.Id, "user");
                    if (addToRoleResult.Succeeded)
                    {
                        var code = await _userManager.GenerateEmailConfirmationTokenAsync(user.Id);
                        var callbackUrl = Url.Action("ConfirmEmail", "User",
                            new { userId = user.Id, code }, protocol: Request.Url.Scheme);

                        _emailService.SendAccountConfirmationEmail(user.Email, callbackUrl);

                        return Json(new { result = "success" });
                    }

                    addErrors(addToRoleResult);
                }

                addErrors(result);
            }

            return PartialView(MVC.User.Views._Register, model);
        }
نکته: در اینجا برای ارسال لینک فعال سازی حساب کاربری، از کلاس EmailService خود سیستم IRIS استفاده شده است؛ نه EmailService مربوط به ASP.NET Identity. همچنین در ادامه نیز از EmailService مربوط به خود سیستم Iris استفاده شده است.

برای این کار متد زیر را به کلاس EmailService  اضافه کنید: 
        public SendingMailResult SendAccountConfirmationEmail(string email, string link)
        {
            var model = new ConfirmEmailModel()
            {
                ActivationLink = link
            };

            var htmlText = _viewConvertor.RenderRazorViewToString(MVC.EmailTemplates.Views._ConfirmEmail, model);

            var result = Send(new MailDocument
            {
                Body = htmlText,
                Subject = "تایید حساب کاربری",
                ToEmail = email
            });

            return result;
        }
همچنین قالب ایمیل تایید حساب کاربری را در مسیر Views/EmailTemplates/_ConfirmEmail.cshtml با محتویات زیر ایجاد کنید:
@model Iris.Model.EmailModel.ConfirmEmailModel

<div style="direction: rtl; -ms-word-wrap: break-word; word-wrap: break-word;">
    <p>با سلام</p>
    <p>برای فعال سازی حساب کاربری خود لطفا بر روی لینک زیر کلیک کنید:</p>
    <p>@Model.ActivationLink</p>
    <div style=" color: #808080;">
        <p>با تشکر</p>
        <p>@Model.SiteTitle</p>
        <p>@Model.SiteDescription</p>
        <p><span style="direction: ltr !important; unicode-bidi: embed;">@Html.ConvertToPersianDateTime(DateTime.Now, "s,H")</span></p>
    </div>
</div>

اصلاح پیام موفقیت آمیز بودن ثبت نام  کاربر جدید


سیستم IRIS از ارسال ایمیل تایید حساب کاربری استفاده نمی‌کند و به محض اینکه عملیات ثبت نام تکمیل می‌شد، صفحه رفرش می‌شود. اما در سیستم Identity یک ایمیل حاوی لینک فعال سازی حساب کاربری به او ارسال می‌شود.
برای اصلاح پیغام پس از ثبت نام، باید به فایل myscript.js درون پوشه‌ی Scripts مراجعه کرده و رویداد onSuccess شیء RegisterUser را به شکل زیراصلاح کنید: 
RegisterUser.Form.onSuccess = function (data) {
    if (data.result == "success") {
        var message = '<div id="alert"><button type="button" data-dismiss="alert">×</button>ایمیلی حاوی لینک فعال سازی، به ایمیل شما ارسال شد؛ لطفا به ایمیل خود مراجعه کرده و بر روی لینک فعال سازی کلیک کنید.</div>';
        $('#registerResult').html(message);
    }
    else {
        $('#logOnModal').html(data);
    }
};
برای تایید ایمیل کاربری که ثبت نام کرده است نیز اکشن متد زیر را به کلاس UserController اضافه کنید:
        [AllowAnonymous]
        public virtual async Task<ActionResult> ConfirmEmail(int? userId, string code)
        {
            if (userId == null || code == null)
            {
                return View("Error");
            }
            var result = await _userManager.ConfirmEmailAsync(userId.Value, code);
            return View(result.Succeeded ? "ConfirmEmail" : "Error");
        }
این اکشن متد نیز احتیاج به View دارد؛ پس view متناظر آن را با محتویات زیر اضافه کنید:
@{
    ViewBag.Title = "حساب کاربری شما تایید شد";
}
<h2>@ViewBag.Title.</h2>
<div>
    <p>
        با تشکر از شما، حساب کاربری شما تایید شد.
    </p>
    <p>
        @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" })
    </p>
</div>

اصلاح اکشن متد ورود به سایت 

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async virtual Task<ActionResult> LogOn(LogOnModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                if (Request.IsAjaxRequest())
                    return PartialView(MVC.User.Views._LogOn, model);
                return View(model);
            }


            const string emailRegPattern =
                @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";

            string ip = Request.ServerVariables["REMOTE_ADDR"];

            SignInStatus result = SignInStatus.Failure;

            if (Regex.IsMatch(model.Identity, emailRegPattern))
            {

                var user = await _userManager.FindByEmailAsync(model.Identity);

                if (user != null)
                {
                    result = await _signInManager.PasswordSignInAsync
                   (user.UserName,
                   model.Password, model.RememberMe, shouldLockout: true);
                }
            }
            else
            {
                result = await _signInManager.PasswordSignInAsync(model.Identity, model.Password, model.RememberMe, shouldLockout: true);
            }


            switch (result)
            {
                case SignInStatus.Success:
                    if (Request.IsAjaxRequest())
                        return JavaScript(IsValidReturnUrl(returnUrl)
                            ? string.Format("window.location ='{0}';", returnUrl)
                            : "window.location.reload();");
                    return redirectToLocal(returnUrl);

                case SignInStatus.LockedOut:
                    ModelState.AddModelError("",
                        string.Format("حساب شما قفل شد، لطفا بعد از {0} دقیقه دوباره امتحان کنید.",
                            _userManager.DefaultAccountLockoutTimeSpan.Minutes));
                    break;
                case SignInStatus.Failure:
                    ModelState.AddModelError("", "نام کاربری یا کلمه عبور اشتباه است.");
                    break;
                default:
                    ModelState.AddModelError("", "در ورود شما خطایی رخ داده است.");
                    break;
            }


            if (Request.IsAjaxRequest())
                return PartialView(MVC.User.Views._LogOn, model);
            return View(model);
        }


اصلاح اکشن متد خروج کاربر از سایت

        [HttpPost]
        [ValidateAntiForgeryToken]
        [Authorize]
        public virtual ActionResult LogOut()
        {
            _authenticationManager.SignOut();

            if (Request.IsAjaxRequest())
                return Json(new { result = "true" });

            return RedirectToAction(MVC.User.ActionNames.LogOn, MVC.User.Name);
        }

پیاده سازی ریست کردن کلمه‌ی عبور با استفاده از ASP.NET Identity

مکانیزم سیستم IRIS برای ریست کردن کلمه‌ی عبور به هنگام فراموشی آن، ساخت GUID و ذخیره‌ی آن در دیتابیس است. سیستم Identity  با استفاده از یک توکن رمز نگاری شده و بدون استفاده از دیتابیس، این کار را انجام می‌دهد و با استفاده از قابلیت‌های تو کار سیستم Identity، تمهیدات امنیتی بهتری را نسبت به سیستم کنونی در نظر گرفته است.

برای این کار کدهای کنترلر ForgottenPasswordController را به شکل زیر ویرایش کنید:
using System.Threading.Tasks;
using System.Web.Mvc;
using CaptchaMvc.Attributes;
using Iris.Model;
using Iris.Servicelayer.Interfaces;
using Iris.Web.Email;
using Microsoft.AspNet.Identity;

namespace Iris.Web.Controllers
{
    public partial class ForgottenPasswordController : Controller
    {
        private readonly IEmailService _emailService;
        private readonly IApplicationUserManager _userManager;

        public ForgottenPasswordController(IEmailService emailService, IApplicationUserManager applicationUserManager)
        {
            _emailService = emailService;
            _userManager = applicationUserManager;
        }

        [HttpGet]
        public virtual ActionResult Index()
        {
            return PartialView(MVC.ForgottenPassword.Views._Index);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")]
        public async virtual Task<ActionResult> Index(ForgottenPasswordModel model)
        {
            if (!ModelState.IsValid)
            {
                return PartialView(MVC.ForgottenPassword.Views._Index, model);
            }

            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null || !(await _userManager.IsEmailConfirmedAsync(user.Id)))
            {
                // Don't reveal that the user does not exist or is not confirmed
                return Json(new
                {
                    result = "false",
                    message = "این ایمیل در سیستم ثبت نشده است"
                });
            }

            var code = await _userManager.GeneratePasswordResetTokenAsync(user.Id);

            _emailService.SendResetPasswordConfirmationEmail(user.UserName, user.Email, code);

            return Json(new
            {
                result = "true",
                message = "ایمیلی برای تایید بازنشانی کلمه عبور برای شما ارسال شد.اعتبارایمیل ارسالی 3 ساعت است."
            });
        }

        [AllowAnonymous]
        public virtual ActionResult ResetPassword(string code)
        {
            return code == null ? View("Error") : View();
        }


        [AllowAnonymous]
        public virtual ActionResult ResetPasswordConfirmation()
        {
            return View();
        }

        //
        // POST: /Account/ResetPassword
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null)
            {
                // Don't reveal that the user does not exist
                return RedirectToAction("Error");
            }
            var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "ForgottenPassword");
            }
            addErrors(result);
            return View();
        }

        private void addErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }

    }
}

همچنین برای اکشن متدهای اضافه شده، View‌های زیر را نیز باید اضافه کنید:

- View  با نام  ResetPasswordConfirmation.cshtml را اضافه کنید.
@{
    ViewBag.Title = "کلمه عبور شما تغییر کرد";
}

<hgroup>
    <h1>@ViewBag.Title.</h1>
</hgroup>
<div>
    <p>
        کلمه عبور شما با موفقیت تغییر کرد
    </p>
    <p>
        @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" })
    </p>
</div>

- View با نام ResetPassword.cshtml
@model Iris.Model.ResetPasswordViewModel
@{
    ViewBag.Title = "ریست کردن کلمه عبور";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("ResetPassword", "ForgottenPassword", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>ریست کردن کلمه عبور</h4>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Code)
    <div>
        @Html.LabelFor(m => m.Email, "ایمیل", new { @class = "control-label" })
        <div>
            @Html.TextBoxFor(m => m.Email)
        </div>
    </div>
    <div>
        @Html.LabelFor(m => m.Password, "کلمه عبور", new { @class = "control-label" })
        <div>
            @Html.PasswordFor(m => m.Password)
        </div>
    </div>
    <div>
        @Html.LabelFor(m => m.ConfirmPassword, "تکرار کلمه عبور", new { @class = "control-label" })
        <div>
            @Html.PasswordFor(m => m.ConfirmPassword)
        </div>
    </div>
    <div>
        <div>
            <input type="submit" value="تغییر کلمه عبور" />
        </div>
    </div>
}

همچنین این View و Controller متناظر آن، احتیاج به ViewModel زیر دارند که آن را به پروژه‌ی Iris.Models اضافه کنید.
using System.ComponentModel.DataAnnotations;
namespace Iris.Model
{
    public class ResetPasswordViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "ایمیل")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "کلمه عبور باید حداقل 6 حرف باشد", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "کلمه عبور")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "تکرار کلمه عبور")]
        [Compare("Password", ErrorMessage = "کلمه عبور و تکرارش یکسان نیستند")]
        public string ConfirmPassword { get; set; }

        public string Code { get; set; }
    }
}

حذف سیستم قدیمی احراز هویت

برای حذف کامل سیستم احراز هویت IRIS، وارد فایل Global.asax.cs شده و سپس از متد Application_AuthenticateRequest کدهای زیر را حذف کنید:

var principalService = ObjectFactory.GetInstance<IPrincipalService>();
var formsAuthenticationService = ObjectFactory.GetInstance<IFormsAuthenticationService>();
context.User = principalService.GetCurrent()

فارسی کردن خطاهای ASP.NET Identity

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

راه اول این است که از این پروژه استفاده کرد و کلاس‌های زیر را به پروژه اضافه کنید:

    public class CustomUserValidator<TUser, TKey> : IIdentityValidator<ApplicationUser>
        where TUser : class, IUser<int>
        where TKey : IEquatable<int>
    {

        public bool AllowOnlyAlphanumericUserNames { get; set; }
        public bool RequireUniqueEmail { get; set; }

        private ApplicationUserManager Manager { get; set; }
        public CustomUserValidator(ApplicationUserManager manager)
        {
            if (manager == null)
                throw new ArgumentNullException("manager");
            AllowOnlyAlphanumericUserNames = true;
            Manager = manager;
        }
        public virtual async Task<IdentityResult> ValidateAsync(ApplicationUser item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            var errors = new List<string>();
            await ValidateUserName(item, errors);
            if (RequireUniqueEmail)
                await ValidateEmailAsync(item, errors);
            return errors.Count <= 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
        }

        private async Task ValidateUserName(ApplicationUser user, ICollection<string> errors)
        {
            if (string.IsNullOrWhiteSpace(user.UserName))
                errors.Add("نام کاربری نباید خالی باشد");
            else if (AllowOnlyAlphanumericUserNames && !Regex.IsMatch(user.UserName, "^[A-Za-z0-9@_\\.]+$"))
            {
                errors.Add("برای نام کاربری فقط از کاراکتر‌های مجاز استفاده کنید ");
            }
            else
            {
                var owner = await Manager.FindByNameAsync(user.UserName);
                if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id))
                    errors.Add("این نام کاربری قبلا ثبت شده است");
            }
        }

        private async Task ValidateEmailAsync(ApplicationUser user, ICollection<string> errors)
        {
            var email = await Manager.GetEmailStore().GetEmailAsync(user).WithCurrentCulture();
            if (string.IsNullOrWhiteSpace(email))
            {
                errors.Add("وارد کردن ایمیل ضروریست");
            }
            else
            {
                try
                {
                    var m = new MailAddress(email);

                }
                catch (FormatException)
                {
                    errors.Add("ایمیل را به شکل صحیح وارد کنید");
                    return;
                }
                var owner = await Manager.FindByEmailAsync(email);
                if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id))
                    errors.Add("این ایمیل قبلا ثبت شده است");
            }
        }
    }

    public class CustomPasswordValidator : IIdentityValidator<string>
    {
        #region Properties
        public int RequiredLength { get; set; }
        public bool RequireNonLetterOrDigit { get; set; }
        public bool RequireLowercase { get; set; }
        public bool RequireUppercase { get; set; }
        public bool RequireDigit { get; set; }
        #endregion

        #region IIdentityValidator
        public virtual Task<IdentityResult> ValidateAsync(string item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            var list = new List<string>();

            if (string.IsNullOrWhiteSpace(item) || item.Length < RequiredLength)
                list.Add(string.Format("کلمه عبور نباید کمتر از 6 کاراکتر باشد"));

            if (RequireNonLetterOrDigit && item.All(IsLetterOrDigit))
                list.Add("برای امنیت بیشتر از حداقل از یک کارکتر غیر عددی و غیر حرف  برای کلمه عبور استفاده کنید");

            if (RequireDigit && item.All(c => !IsDigit(c)))
                list.Add("برای امنیت بیشتر از اعداد هم در کلمه عبور استفاده کنید");
            if (RequireLowercase && item.All(c => !IsLower(c)))
                list.Add("از حروف کوچک نیز برای کلمه عبور استفاده کنید");
            if (RequireUppercase && item.All(c => !IsUpper(c)))
                list.Add("از حروف بزرک نیز برای کلمه عبور استفاده کنید");
            return Task.FromResult(list.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(string.Join(" ", list)));
        }

        #endregion

        #region PrivateMethods
        public virtual bool IsDigit(char c)
        {
            if (c >= 48)
                return c <= 57;
            return false;
        }

        public virtual bool IsLower(char c)
        {
            if (c >= 97)
                return c <= 122;
            return false;
        }


        public virtual bool IsUpper(char c)
        {
            if (c >= 65)
                return c <= 90;
            return false;
        }

        public virtual bool IsLetterOrDigit(char c)
        {
            if (!IsUpper(c) && !IsLower(c))
                return IsDigit(c);
            return true;
        }
        #endregion

    }

سپس باید کلاس‌های فوق را به Identity معرفی کنید تا از این کلاس‌های سفارشی شده به جای کلاس‌های پیش فرض خودش استفاده کند. برای این کار وارد کلاس ApplicationUserManager شده و درون متد createApplicationUserManager کدهای زیر را اضافه کنید: 
            UserValidator = new CustomUserValidator< ApplicationUser, int>(this)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            PasswordValidator = new CustomPasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = false,
                RequireUppercase = false
            };
روش دیگر مراجعه به سورس ASP.NET Identity است. با مراجعه به مخزن کد آن، فایل Resources.resx آن را که حاوی متن‌های خطا به زبان انگلیسی است، درون پروژه‌ی خود کپی کنید. همچین کلاس‌های UserValidator و PasswordValidator را نیز درون پروژه کپی کنید تا این کلاس‌ها از فایل Resource موجود در پروژه‌ی خودتان استفاده کنند. در نهایت همانند روش قبلی درون متد createApplicationUserManager کلاس ApplicationUserManager، کلاس‌های UserValidator و PasswordValidator را به Identity معرفی کنید.


ایجاد SecurityStamp برای کاربران فعلی سایت

سیستم Identity برای لحاظ کردن یک سری موارد امنیتی، به ازای هر کاربر، فیلدی را به نام SecurityStamp درون دیتابیس ذخیره می‌کند و برای این که این سیستم عملکرد صحیحی داشته باشد، باید این مقدار را برای کاربران فعلی سایت ایجاد کرد تا کاربران فعلی بتوانند از امکانات Identity نظیر فراموشی کلمه عبور، ورود به سیستم و ... استفاده کنند.
برای این کار Identity، متدی به نام UpdateSecurityStamp را در اختیار قرار می‌دهد تا با استفاده از آن بتوان مقدار فیلد SecurityStamp را به روز رسانی کرد.
معمولا برای انجام این کارها می‌توانید یک کنترلر تعریف کنید و درون اکشن متد آن کلیه‌ی کاربران را واکشی کرده و سپس متد UpdateSecurityStamp را بر روی آن‌ها فراخوانی کنید.
        public virtual async Task<ActionResult> UpdateAllUsersSecurityStamp()
        {
            foreach (var user in await _userManager.GetAllUsersAsync())
            {
                await _userManager.UpdateSecurityStampAsync(user.Id);
            }
            return Content("ok");
        }
البته این روش برای تعداد زیاد کاربران کمی زمان بر است.


انتقال نقش‌های کاربران به جدول جدید و برقراری رابطه بین آن‌ها

در سیستم Iris رابطه‌ی بین کاربران و نقش‌ها یک به چند بود. در سیستم Identity این رابطه چند به چند است و من به عنوان یک حرکت خوب و رو به جلو، رابطه‌ی چند به چند را در سیستم جدید انتخاب کردم. اکنون با استفاده از دستورات زیر به راحتی می‌توان نقش‌های فعلی و رابطه‌ی بین آن‌ها را به جداول جدیدشان منتقل کرد:
        public virtual async Task<ActionResult> CopyRoleToNewTable()
        {
            var dbContext = new IrisDbContext();

            foreach (var role in await dbContext.Roles.ToListAsync())
            {
                await _roleManager.CreateAsync(new CustomRole(role.Name)
                {
                    Description = role.Description
                });
            }

            var users = await dbContext.Users.Include(u => u.Role).ToListAsync();

            foreach (var user in users)
            {
                await _userManager.AddToRoleAsync(user.Id, user.Role.Name);
            }
            return Content("ok");
        }
البته اجرای این کد نیز برای تعداد زیادی کاربر، زمانبر است؛ ولی روشی مطمئن و دقیق است.
مطالب
نوع‌های نال نپذیر در TypeScript
تا پیش از ارائه‌ی کامپایلر TypeScript 2.0، مقادیر null و undefined، به هر نوعی قابل انتساب بودند و امکان تفکیک آن‌ها وجود نداشت که این مورد می‌تواند منشاء بروز بسیاری از خطاهای در زمان اجرا شود.
let name: string;
name = "Vahid"; // OK
name = null; // OK
name = undefined;  // OK
let age: number;
age = 24; // OK
age = null; // OK
age = undefined;  // OK
برای نمونه در اینجا یک متغیر رشته‌ای و همچنین عددی تعریف شده‌اند که انتساب null و یا undefined نیز به آن‌ها مجاز است. این مورد جهت نوع‌های ورودی و خروجی متدها، اشیاء و آرایه‌ها نیز میسر است.


نوع null در TypeScript

همانند JavaScript، نوع null تنها یک مقدار معتبر نال را می‌تواند داشته باشد و نمی‌توان برای مثال یک رشته را به آن انتساب داد. اما انتساب این مقدار به هر نوع متغیر دیگری، سبب پاک شدن مقدار آن خواهد شد. با فعالسازی strictNullChecks، این نوع را تنها به نوع‌های نال‌پذیر می‌توان انتساب داد.


نوع undefined در TypeScript

هر متغیری که مقداری به آن انتساب داده نشده باشد، با undefined مقدار دهی می‌شود. این مورد حتی جهت خروجی متدها نیز صادق است و اگر return ایی در آن‌ها فراموش شود، این خروجی نیز به undefined تفسیر می‌شود.
در اینجا نیز اگر نوع متغیری به undefined تنظیم شد، این متغیر تنها مقدار undefined را می‌تواند بپذیرد. تنها با خاموش کردن پرچم strictNullChecks می‌توان آن‌را به اعداد، رشته‌ها و غیره نیز انتساب داد.


فعالسازی نوع‌های نال نپذیر در TypeScript

برای فعالسازی این قابلیت، نیاز است پرچم strictNullChecks را در فایل تنظیمات کامپایلر به true تنظیم کرد:
{
    "compilerOptions": {
        "strictNullChecks": true
    }
}
از این پس دیگر نمی‌توان null و undefined را به هر نوعی انتساب داد و این‌ها تنها به خودشان و یا نوع any، قابل انتساب هستند. برای مثال اکنون نوع number فقط یک عدد است و دیگر قابلیت پذیرش null و یا undefined را ندارد. البته در اینجا یک استثناء هم وجود دارد: undefined را می‌توان به نوع void نیز انتساب داد.
برای مثال اگر متدی، رشته‌ای را به عنوان پارامتر قبول کند، تا پیش از TypeScript 2.0 و فعالسازی strictNullChecks آن، مشخص نبود که رشته‌ی دریافتی از آن واقعا یک رشته‌است و یا شاید null. اما اکنون یک رشته، فقط یک رشته‌است و دیگر نال پذیر نیست.
 let foo: string = null; // Error! Type 'null' is not assignable to type 'string'.
به این ترتیب دیگر به خطاهای زمان اجرایی مانند خطاهای ذیل نخواهیم رسید:
Uncaught ReferenceError: foo is not defined
Uncaught TypeError: window.foo is not a function

این مورد برای آرایه‌ها نیز صادق است:
// With strictNullChecks set to false
let d: Array<number> = [null, undefined, 10, 15]; //OK
let e: Array<string> = ["pie", null, ""];  //OK
 
 
// With strictNullChecks set to true
let d: Array<number> = [null, undefined, 10, 15]; // Error
let e: Array<string> = ["pie", null, ""]; // Error
اگر strictNullChecks فعال شود، دیگر نمی‌توان به اعضای یک آرایه مقادیر null و یا undefined را نسبت داد.


ساده سازی تعریف بررسی‌های با پرچم strict، در TypeScript 2.3

تعداد گزینه‌های قابل تنظیم در فایل tsconfig روز به روز بیشتر می‌شوند. به همین جهت برای ساده سازی فعالسازی آن‌ها، از TypeScript 2.3 به بعد، پرچم strict نیز به این تنظیمات اضافه شده‌است. کار آن فعالسازی یکجای تمام بررسی‌های strict است؛ مانند noImplicitAny، strictNullChecks و غیره.
{ 
    "compilerOptions": { 
        "strict": true  /* Enable all strict type-checking options. */ 
    } 
}
در این حالت اگر نیاز به لغو یکی از گزینه‌ها بود، می‌توان به صورت ذیل عمل کرد:
{ 
    "compilerOptions": { 
        "strict": true, 
        "noImplicitThis": false 
    } 
}
گزینه‌ی strict تمام بررسی‌های متداول را فعال می‌کند؛ اما ذکر و تنظیم صریح noImplicitThis به false، تنها این یک مورد را لغو خواهد کرد.

یک نکته: اجرای دستور tsc --init ، سبب تولید یک فایل tsconfig.json از پیش تنظیم شده، بر اساس آخرین قابلیت‌های کامپایلر TypeScript می‌شود.


اما ... اکنون چگونه یک نوع را نال‌پذیر کنیم؟

TypeScript به همراه دو نوع ویژه‌ی null و undefined نیز شده‌است که تنها دارای مقادیر null و undefined می‌توانند باشند. به این معنا که در حین تعریف نوع یک متغیر، می‌توان این دو را نیز ذکر کرد و دیگر تنها به عنوان دو مقدار مطرح نیستند. به این ترتیب می‌توان از آن‌ها یک union type را ایجاد کرد:
 let foo: string | null = null; // Okay!
اکنون تنها در این حالت است که متغیر foo می‌تواند یک رشته و یا یک null را دریافت کند و یا اگر مثال ابتدای بحث را بخواهیم اصلاح کنیم، به نمونه‌ی ذیل خواهیم رسید:
let name: string | null;
name = "Vahid"; // OK
name = null; // OK
name = undefined;  // Error
یکی دیگر از مزایای این روش، وضوح بیشتر تعریف نوع متغیرها و به نوعی «خود مستند سازی» بهتر آن‌ها است. در این حالت یا به صورت صریح مشخص می‌کنیم که متدی فقط یک رشته را می‌پذیرد و یا با ذکر string | null، به استفاده کننده اعلام می‌کنیم که ارسال null نیز به آن پیش بینی شده‌است و به نتیجه‌ی نامشخصی منتهی نخواهد شد.

یک نکته:
تا پیش از این اگر متغیری را به این صورت تعریف می‌کردیم:
let z = null;
نوع آن any درنظر گرفته می‌شد. اما اکنون، نوع آن تنها null است و تنها مقداری را هم که می‌تواند بپذیرد نال خواهد بود.


بررسی انتساب، پیش از استفاده

با فعالسازی strictNullChecks، اکنون کامپایلر برای تمام نوع‌هایی که undefined نیستند، یک مقدار اولیه را پیش از استفاده‌ی از آن‌ها درخواست می‌کند:
testAssignedBeforeUseChecking() {
    let x: number;
    console.log(x);
}
در اینجا چون x از نوع عددی است، به علت عدم مقدار دهی اولیه، قابلیت استفاده‌ی از آن وجود ندارد و کامپایلر خطای ذیل را اعلام می‌کند:
 [ts] Variable 'x' is used before being assigned.

اما در حالت ذیل، عدد z می‌تواند عدد و یا undefined باشد؛ به همین جهت کامپایلر با استفاده‌ی از آن مشکلی نخواهد داشت:
let z: number | undefined;
console.log(z);

یک نکته: خواص و پارامترهای اختیاری، به صورت خودکار دارای نوع undefined نیز هستند. برای مثال امضای متد ذیل:
method1(x?: number) {
}
با متد زیر یکی است:
method1(x?: number | undefined) {
}


اجبار به بررسی نال نبودن مقادیر، پیش از استفاده‌ی از آن‌ها در متدهای نال نپذیر

اگر پارامتر متدی یا خاصیت شیءایی نال پذیر نباشند، با ارسال مقدار نوعی به آن‌ها که می‌تواند null و یا undefined را بپذیرد، یک خطای زمان کامپایل صادر خواهد شد. در اینجا محافظ‌های نوع‌ها توسعه یافته‌اند تا اگر بررسی نال یا undefined بودن مقداری انجام شد، مشکلی در جهت استفاده‌ی از آن‌ها نباشد:
  f(x: number): string {
    return x.toString();
  }

  testTypeGuards() {
    let x: number | null | undefined;
    if (x) {
      this.f(x);  // Ok, type of x is number here
    } else {
      this.f(x);  // Error, type of x is number? here
    }
  }
در این مثال، متد f فقط یک عدد را می‌پذیرد (و نه نال و یا undefined). اما در حین کاربرد آن در متد testTypeGuards، مقدار متغیر x می‌تواند یک عدد، نال و یا undefined باشد. چون پیش از اولین استفاده‌ی از متد f در اینجا، بررسی دارای مقدار بودن این متغیر صورت گرفته‌است، فراخوانی صورت گرفته، مجاز است. اما در قسمت else این شرط، کامپایلر خطای ذیل را صادر می‌کند:
 Argument of type 'number | null | undefined' is not assignable to parameter of type 'number'.
Type 'undefined' is not assignable to type 'number'.

امکان این بررسی در مورد عبارات شرطی نیز صادق است:
getLength(s: string | null) {
   return s ? s.length : 0;
}


توسعه‌ی محافظ‌های نوع‌ها جهت کار با نوع‌های نال نپذیر

در مثال ذیل، خروجی متد isNumber دارای امضایی به همراه is است:
isNumber(n: any): n is number { // type guard
   return typeof n === "number";
}
به یک چنین متدهایی type guard گفته می‌شود که امکان بررسی یک نوع را میسر می‌کنند. از این امکان می‌توان جهت بررسی بهتر پارامترها و یا خواص اختیاری استفاده کرد:
  usedMb(usedBytes?: number): number | undefined {
    return this.isNumber(usedBytes) ? (usedBytes / (1024 * 1024)) : undefined;
  }
یک چنین بررسی، بهتر است از بررسی ذیل:
  usedMb2(usedBytes?: number): number | undefined {
    return usedBytes ? (usedBytes / (1024 * 1024)) : undefined;
  }
از این جهت که عبارت شرطی بررسی شده، مقدار صفر را نیز به صورت undefined بازگشت خواهد داد (if(0) به false تعبیر می‌شود و قسمت else این شرط فراخوانی خواهد شد).
همچنین امضای متد نیز به number | undefined تغییر یافته‌است. در غیر اینصورت، خطای زمان کامپایل Type undefined is not assignable to type number صادر خواهد شد.
در حین استفاده‌ی از یک چنین متدی، دیگر نمی‌توان به خروجی آن به صورت ذیل دسترسی یافت:
  formatUsedMb(): string {
    //ERROR: TS2531: Object is possibly undefined
    return this.usedMb(123).toFixed(0).toString();
  }
چون مقدار usedMb می‌تواند undefined باشد، باید ابتدا آن‌را بررسی کرد:
  formatUsed(): string {
    const usedMb = this.usedMb(123);
    return usedMb ? usedMb.toFixed(0).toString() : "";
  }


لغو بررسی strictNullChecks به صورت موقت

با استفاده از اپراتور ! می‌توان به کامپایلر اطمینان داد که این متغیر یا خاصیت، دارای مقدار نال نیست و نخواهد بود:
export interface User {
  name: string;
  age?: number;
}
در این اینترفیس، خاصیت age به صورت اختیاری تعریف شده‌است. برای نمایش مقدار age با فعال بودن strictNullChecks، یا باید ابتدا null نبودن آن‌را به صورت صریحی بررسی کرد:
  printUserInfo(user: User) {
    if (user.age != null) {
      console.log(`${user.name}, ${user.age.toString()}`);
    }
  }
در غیراینصورت قطعه کد ذیل با خطای 'Object is possibly 'undefined کامپایل نخواهد شد:
  printUserInfo(user: User) {
    console.log(`${user.name}, ${user.age.toString()}`);
  }

و یا می‌توان توسط اپراتور ! این بررسی را به صورت موقت خاموش کرد:
  printUserInfo(user: User) {
    console.log(`${user.name}, ${user.age!.toString()}`);
  }
البته استفاده‌ی از این اپراتور توسط tslint توصیه نمی‌شود:
 [tslint] Forbidden non null assertion (no-non-null-assertion)
چون بهتر است به کامپایلر عنوان نکنیم «قسم می‌خورم که این مقدار نال نیست»!



یک نکته‌ی تکمیلی
پس از آزمایش موفقیت آمیز نوع‌های نال نپذیر در TypeScript، مایکروسافت قصد دارد این ویژگی را به C# 8.0 نیز در مورد نوع‌های ارجاعی که می‌توانند نال پذیر باشند، اضافه کند (امکان داشتن نوع‌های ارجاعی نال‌نپذیر).
مطالب
تهیه گزارشات Crosstab به کمک LINQ

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

مثال اول) تهیه گزارش Crosstab جمع هزینه‌های واحدهای مختلف به تفکیک ماه

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

using System;

namespace Pivot.Sample1
{
public class Expense
{
public DateTime Date { set; get; }
public string Department { set; get; }
public decimal Expenses { set; get; }
}
}

با توجه به این کلاس، یک منبع داده آزمایشی جهت تهیه گزارشات، می‌تواند به صورت زیر باشد:

using System;
using System.Collections.Generic;

namespace Pivot.Sample1
{
public class ExpenseDataSource
{
public static IList<Expense> ExpensesDataSource()
{
return new List<Expense>
{
new Expense { Date = new DateTime(2011,11,1), Department = "Computer", Expenses = 100 },
new Expense { Date = new DateTime(2011,11,1), Department = "Math", Expenses = 200 },
new Expense { Date = new DateTime(2011,11,1), Department = "Physics", Expenses = 150 },

new Expense { Date = new DateTime(2011,10,1), Department = "Computer", Expenses = 75 },
new Expense { Date = new DateTime(2011,10,1), Department = "Math", Expenses = 150 },
new Expense { Date = new DateTime(2011,10,1), Department = "Physics", Expenses = 130 },

new Expense { Date = new DateTime(2011,9,1), Department = "Computer", Expenses = 90 },
new Expense { Date = new DateTime(2011,9,1), Department = "Math", Expenses = 95 },
new Expense { Date = new DateTime(2011,9,1), Department = "Physics", Expenses = 100 }
};
}
}
}

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


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


یعنی اگر سه ماه را در نظر بگیریم با هر تعداد رکورد، فقط سه ردیف به ازای هر ماه باید حاصل شود و ستون‌های دیگر هم نام بخش‌ها یا واحدهای موجود باشند.
برای رسیدن به این خروجی Crosstab، می‌توان کوئری LINQ زیر را به کمک امکانات گروه بندی اطلاعات آن تهیه کرد:

using System.Collections;
using System.Linq;

namespace Pivot.Sample1
{
public class PivotTable
{
public static IList ExpensesCrossTab()
{
return ExpenseDataSource
.ExpensesDataSource()
.GroupBy(t =>
new
{
Year = t.Date.Year,
Month = t.Date.Month
})
.Select(myGroup =>
new
{
//Year = myGroup.Key.Year,
Month = myGroup.Key.Month,
ComputerDepartment = myGroup.Where(x => x.Department == "Computer").Sum(x => x.Expenses),
MathDepartment = myGroup.Where(x => x.Department == "Math").Sum(x => x.Expenses),
PhysicsDepartment = myGroup.Where(x => x.Department == "Physics").Sum(x => x.Expenses)
})
.ToList();
}
}
}

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


اگر علاقمند باشید که مثال فوق را در برنامه‌ی LINQPad آزمایش کنید، این فایل را دریافت نموده و در آن برنامه باز نمائید.


مثال دوم) تهیه لیست Crosstab حضور و غیاب افراد در طول یک هفته

کلاس StudentStat را جهت ثبت اطلاعات حضور یک دانشجو، می‌توان به شکل زیر تعریف کرد:

using System;

namespace Pivot.Sample2
{
public class StudentStat
{
public int Id { set; get; }
public string Name { set; get; }
public DateTime Date { set; get; }
public bool IsPresent { set; get; }
}
}

و بر همین اساس یک منبع داده فرضی جهت انجام گزارشات می‌تواند به نحو زیر تهیه شود:

using System;
using System.Collections.Generic;

namespace Pivot.Sample2
{
public class StudentsStatDataSource
{
public static IList<StudentStat> CreateMonthlyReportDataSource()
{
var result = new List<StudentStat>();
var rnd = new Random();

for (int day = 1; day < 6; day++)
{
for (int student = 1; student < 6; student++)
{
result.Add(new StudentStat
{
Id = student,
Date = new DateTime(2011, 11, day),
IsPresent = rnd.Next(-1, 1) == 0 ? true : false,
Name = "student " + student
});
}
}

return result;
}
}
}

خروجی این گزارش هم در این حالت ساده با 5 دانشجو و فقط 5 روز، 25 رکورد خواهد بود:


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


برای رسیدن به این خروجی Crosstab، مثلا می‌توان از کوئری LINQ زیر کمک گرفت که بر اساس شماره دانشجویی اطلاعات را گروه بندی کرده است:

using System.Collections;
using System.Linq;

namespace Pivot.Sample2
{
public class PivotTable
{
public static IList StudentsStatCrossTab()
{
return StudentsStatDataSource
.CreateWeeklyReportDataSource()
.GroupBy(x =>
new
{
x.Id
})
.Select(myGroup =>
new
{
myGroup.Key.Id,
Name = myGroup.First().Name,
Day1IsPresent = myGroup.Where(x => x.Date.Day == 1).First().IsPresent,
Day2IsPresent = myGroup.Where(x => x.Date.Day == 2).First().IsPresent,
Day3IsPresent = myGroup.Where(x => x.Date.Day == 3).First().IsPresent,
Day4IsPresent = myGroup.Where(x => x.Date.Day == 4).First().IsPresent,
Day5IsPresent = myGroup.Where(x => x.Date.Day == 5).First().IsPresent,
PresentsCount = myGroup.Where(x => x.IsPresent).Count(),
AbsentsCount = myGroup.Where(x => !x.IsPresent).Count()
})
.ToList();
}
}
}

و این کوئری خروجی زیر را تولید می‌کند که از هر لحاظ نسبت به لیست قبلی مفهوم‌تر است:


فایل LINQPad این مثال را می‌توانید از اینجا دریافت کنید.

مطالب
ایجاد لیستی از کلاسی جنریک
کلاس جنریک زیر را در نظر بگیرید:
public class Column<T>

{
public string Name { set; get; }
public T Data { set; get; }
}

مشکلی که با این نوع کلاس‌ها وجود دارد این است که نمی‌توان مثلا لیست زیر را در مورد آن‌ها تعریف کرد:

IList<Column<T>> myList = new List<Column<T>>();



به عبارتی می‌خواهیم یک لیست از کلاسی جنریک داشته باشیم. راه حل انجام آن به صورت زیر است:

using System.Collections;


namespace Tests
{
public interface IColumn
{
string Name { set; get; }
object Data { set; get; }
}

public class Column<T> : IColumn
{
public string Name { set; get; }

public T Data { set; get; }

object IColumn.Data
{
get { return this.Data; }
set { this.Data = (T)value; }
}
}
}

ابتدا یک اینترفیس عمومی را همانند اعضای کلاس Column تعریف می‌کنیم که در آن بجای T از object‌ استفاده شده است. سپس یک پیاده سازی جنریک از این اینترفیس را ارائه خواهیم داد؛ با این تفاوت که اینبار خاصیت Data مربوط به اینترفیس، به صورت خصوصی و صریح با استفاده از IColumn.Data تعریف می‌شود و نمونه‌ی جنریک هم نام آن، عمومی خواهد بود.
اکنون می‌توان نوشت:

var myList = new List<IColumn>();


برای مثال در این حالت تعریف لیست زیر که از تعدادی وهله‌ی کلاسی جنریک ایجاد شده، کاملا مجاز می‌باشد:

var myList = new List<IColumn>

{
new Column<int> { Data = 1, Name = "Col1"},
new Column<double> { Data = 1.2, Name = "Col2"}
};

خوب، تا اینجا یک مرحله پیشرفت است.اکنون اگر بخواهیم در این لیست، Data مثلا عنصری را که نامش Col1 است، دریافت کنیم چه باید کرد؟ آن هم نه به شکل object بلکه از نوع T مشخص:

static T GetColumnData<T>(IList<IColumn> list, string name)

{
var column = (Column<T>)Convert.ChangeType(list.Single(s => s.Name.Equals(name)), typeof(Column<T>), null);
return column.Data;
}

و نمونه‌ای از استفاده آن:

int data = GetColumnData<int>(myList, "Col1");


نظرات مطالب
SQL Antipattern #2
نیازی به استفاده از Id نیست. مسیر زیر را در نظر بگیرید:
/// Example: "00001.00042.00005".
مسیر بالا متناظر با نودی در درخت می‌باشد که در عمق 2 بوده و فرزند 5 ام مربوط به نود 00001.00042 می‌باشد. اگر نیاز باشد فرزند جدیدی به نود 00001.00042 اضافه شود، باید ابتدا مسیر آخرین فرزند آن یعنی الگوی بالایی واکشی شده و سپس مسیر جدیدی برای نود جدید به شکل زیر تشکیل شود:
/// Example: "00001.00042.00006".
دقیقا مشابه به کاری می‌باشد که نوع داده hierarchyid موجود در Sql Server انجام می‌دهد. با این روش دقیقا مشخص می‌باشد که نود x در چه مکانی قرار داد.

مدیریت واحدهای سازمانی
یکسری متد کمکی هم برای مدیریت فیلد Path در نظر گرفته شده است.
    public class OrganizationalUnit : TrackableEntity<User>, IHasRowVersion, IPassivable
    {
        #region Constants

        /// <summary>
        /// Maximum depth of an UO hierarchy.
        /// </summary>
        public const int MaxDepth = 16;

        /// <summary>
        /// Length of a code unit between dots.
        /// </summary>
        public const int PathUnitLength = 5;

        /// <summary>
        /// Maximum length of the <see cref="Path"/> property.
        /// </summary>
        public const int MaxPathLength = MaxDepth * (PathUnitLength + 1) - 1;

        public const char HierarchicalDisplayNameSeperator = '»';

        #endregion

        #region Properties

        public string Name { get; set; }
        public string NormalizedName { get; set; }
        public string HierarchicalDisplayName { get; set; }
        /// <summary>
        /// Hierarchical Path of this organization unit.
        /// Example: "00001.00042.00005".
        /// It's changeable if OU hierarch is changed.
        /// </summary>
        public string Path { get; set; }
        public bool IsActive { get; set; } = true;
        public byte[] RowVersion { get; set; }

        #endregion

        #region Navigation Properties

        public OrganizationalUnit Parent { get; set; }
        public long? ParentId { get; set; }
        public ICollection<OrganizationalUnit> Children { get; set; } = new HashSet<OrganizationalUnit>();
        public ICollection<UserOrganizationalUnit> UserOrganizationalUnits { get; set; } =
            new HashSet<UserOrganizationalUnit>();

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates path for given numbers.
        /// Example: if numbers are 4,2 then returns "00004.00002";
        /// </summary>
        /// <param name="numbers">Numbers</param>
        public static string CreatePath(params int[] numbers)
        {
            if (numbers.IsNullOrEmpty())
            {
                return null;
            }

            return numbers.Select(number => number.ToString(new string('0', PathUnitLength))).JoinAsString(".");
        }

        /// <summary>
        /// Appends a child path to a parent path. 
        /// Example: if parentPath = "00001", childPath = "00042" then returns "00001.00042".
        /// </summary>
        /// <param name="parentPath">Parent path. Can be null or empty if parent is a root.</param>
        /// <param name="childPath">Child path.</param>
        public static string AppendPath(string parentPath, string childPath)
        {
            if (childPath.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(childPath), "childPath can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return childPath;
            }

            return parentPath + "." + childPath;
        }

        /// <summary>
        /// Gets relative path to the parent.
        /// Example: if path = "00019.00055.00001" and parentPath = "00019" then returns "00055.00001".
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="parentPath">The parent path.</param>
        public static string GetRelativePath(string path, string parentPath)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            if (parentPath.IsNullOrEmpty())
            {
                return path;
            }

            if (path.Length == parentPath.Length)
            {
                return null;
            }

            return path.Substring(parentPath.Length + 1);
        }

        /// <summary>
        /// Calculates next path for given path.
        /// Example: if code = "00019.00055.00001" returns "00019.00055.00002".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string CalculateNextPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var parentPath = GetParentPath(path);
            var lastUnitPath = GetLastUnitPath(path);

            return AppendPath(parentPath, CreatePath(Convert.ToInt32(lastUnitPath) + 1));
        }

        /// <summary>
        /// Gets the last unit path.
        /// Example: if path = "00019.00055.00001" returns "00001".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetLastUnitPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            return splittedPath[splittedPath.Length - 1];
        }

        /// <summary>
        /// Gets parent path.
        /// Example: if path = "00019.00055.00001" returns "00019.00055".
        /// </summary>
        /// <param name="path">The path.</param>
        public static string GetParentPath(string path)
        {
            if (path.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(path), "Path can not be null or empty.");
            }

            var splittedPath = path.Split('.');
            if (splittedPath.Length == 1)
            {
                return null;
            }

            return splittedPath.Take(splittedPath.Length - 1).JoinAsString(".");
        }

        #endregion
    }

البته یک ویو نمایشی برای حالت درختی هم بهتر است داشته باشید.


یکسری متد DomainService

       public virtual async Task<string> GetNextChildPathAsync(long? parentId)
        {
            var lastChild = await GetLastChildOrNullAsync(parentId).ConfigureAwait(false);
            if (lastChild == null)
            {
                var parentPath = parentId != null ? await GetPathAsync(parentId.Value).ConfigureAwait(false) : null;
                return OrganizationalUnit.AppendPath(parentPath, OrganizationalUnit.CreatePath(1));
            }

            return OrganizationalUnit.CalculateNextPath(lastChild.Path);
        }

        public async Task<string> GetNextChildHierarchicalDisplayNameAsync(string name, long? parentId)
        {
            var parent = parentId != null
                ? await _organizationalUnits.SingleOrDefaultAsync(a => a.Id == parentId.Value).ConfigureAwait(false)
                : null;

            return parent == null
                ? name
                : $"{parent.HierarchicalDisplayName} {OrganizationalUnit.HierarchicalDisplayNameSeperator} {name}";
        }

        public virtual async Task<OrganizationalUnit> GetLastChildOrNullAsync(long? parentId)
        {
            return await _organizationalUnits.OrderByDescending(c => c.Path)
                .FirstOrDefaultAsync(ou => ou.ParentId == parentId).ConfigureAwait(false);
        }

        public virtual async Task<string> GetPathAsync(long id)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null)
            {
                throw new KeyNotFoundException();
            }
            return organizationalUnit.Path;
        }

        public async Task<List<OrganizationalUnit>> FindChildrenAsync(long? parentId, bool recursive = false)
        {
            if (!recursive)
            {
                return await _organizationalUnits.Where(ou => ou.ParentId == parentId).ToListAsync().ConfigureAwait(false);
            }

            if (!parentId.HasValue)
            {
                return await _organizationalUnits.ToListAsync().ConfigureAwait(false);
            }

            var path = await GetPathAsync(parentId.Value).ConfigureAwait(false);

            return await _organizationalUnits.Where(
                ou => ou.Path.StartsWith(path) && ou.Id != parentId.Value).ToListAsync().ConfigureAwait(false);
        }

        public virtual async Task MoveAsync(long id, long? parentId)
        {
            Guard.ArgumentNotZero(id, nameof(id));
            var organizationalUnit = await _organizationalUnits.SingleOrDefaultAsync(ou => ou.Id == id).ConfigureAwait(false);
            if (organizationalUnit == null || organizationalUnit.ParentId == parentId)
            {
                return;
            }

            //Should find children before Path change
            var children = await FindChildrenAsync(id, true).ConfigureAwait(false);

            //Store old Path of OU
            var oldPath = organizationalUnit.Path;

            //Move OU
            organizationalUnit.Path = await GetNextChildPathAsync(parentId).ConfigureAwait(false);
            organizationalUnit.ParentId = parentId;

            //Update Children Paths
            foreach (var child in children)
            {
                child.Path = OrganizationalUnit.AppendPath(organizationalUnit.Path, OrganizationalUnit.GetRelativePath(child.Path, oldPath));
            }
        }