مطالب
Soft Delete در Entity Framework 6
برای حذف نمودن یک رکورد از دیتابیس 2 راه وجود دارد : 1- حذف به صورت فیزیکی 2- حذف به صورت منطقی ( مورد بحث این مطلب )
در حذف رکورد به صورت منطقی، طراحان دیتابیس، فیلدی را با نام‌های متفاوتی همچون Flag , IsDeleted , IsActive , و غیره، در جداول ایجاد می‌نمایند. خوب، این روش مزایا و معایب خاص خودش را دارد. مثلا شما در هر پرس و جویی که ایجاد می‌نمایید، بایستی این مورد را چک نموده و رکوردهایی را فراخوانی نمایید که فیلد IsDeleted آن برابر با false باشد. و همچنین در زمان حذف رکورد، برنامه نویس بایستی از متد Update به جای حذف فیزیکی استفاده نماید که تمام این موارد حاکی از مشکلات خاص این روش است. 
در این مقاله سعی داریم که مشکلات ذکر شده در بالا را با ایجاد SoftDelete در EF 6 برطرف نماییم .*یکی از پیش نیاز‌های این پست مطالعه ( سری آموزشی EF CodeFirst ) در سایت جاری می‌باشد.
برای شروع، ما نیاز به داشتن یک Attribute برای مشخص ساختن موجودیت هایی داریم که بایستی بر روی آنها SoftDelete فعال گردد. پس برای اینکار کلاسی را به شکل زیر طراحی مینماییم:
using System.Data.Entity.Core.Metadata.Edm;
public class SoftDeleteAttribute : Attribute
    {
        public string ColumnName { get; set; }
        public SoftDeleteAttribute(string column)
        {
            ColumnName = column;
        }
        public static string GetSoftDeleteColumnName(EdmType type)
        {
            MetadataProperty column = type.MetadataProperties.Where(x => x.Name.EndsWith("customannotation:SoftDeleteColumnName")).SingleOrDefault();
            return column == null ? null : (string)column.Value;
        }
    }
توضیحات کد بالا: در متد سازنده، نام فیلدی را که قرار است بر روی آن SoftDelete به صورت اتوماتیک ایجاد شود، دریافت می‌نماییم و متد GetSoftDeleteColumnName در واقع با استفاده از متادیتاهایی که بر روی فیلد‌ها وجود دارد، فیلدی که انتهای نام آن متادیتای "customannotation:SoftDeleteColumnName" را دارد، انتخاب نموده و برگشت می‌دهد.
سؤال: متادیتای  "customannotation:SoftDeleteColumnName"  از کجا آمد؟ برای پاسخ به این سوال کافیست ادامه‌ی مطلب را کامل مطالعه نمایید.
حال این Attribute برای استفاده در موجودیت‌های ما آمده است. برای استفاده کافیست به روش زیر عمل نمایید .
    [SoftDelete("IsDeleted")]
    public class TblUser 
    {        
        [Key]
        public int TblUserID { get; set; }

        [MaxLength(30)]
        public string Name { get; set; }

        public bool IsDeleted { get; set; }
    }
برای معرفی این قابلیت جدید به EF 6 کافیست در DbContext برنامه در متد OnModelCreating به نحو زیر عمل نماییم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            var Conv = new AttributeToTableAnnotationConvention<SoftDeleteAttribute, string>(
                "SoftDeleteColumnName",
                (type, attribute) => attribute.Single().ColumnName);
            modelBuilder.Conventions.Add(Conv);

        }
در واقع ما در اینجا به Ef می‌گوییم که یک Annotation جدید، با نام SoftDeleteColumnName به Entity که توسط این Attribute مزین شده است، اضافه نماید و همچنین مقدار این Annotation را نام فیلدی که در متد سازنده SoftDeleteAttribute معرفی گردیده است قرار دهد.
برای اطمینان حاصل کردن از اینکه آیا Annotation جدید به مدل برنامه اضافه شده است یا نه کافیست بر روی فایل cs کانتکست DbContext، کلیک راست نموده و در منوی نمایش داده شده گزینه‌ی EntityFramework و سپس گزینه View Entity Data Model را انتخاب نمایید . مانند تصویر زیر:

در پنجره باز شده به قسمت سوم یعنی <StorageModels> مراجعه نمایید و بایستی گزینه زیر را مشاهده نمایید .

 <EntityType Name="TblUser" customannotation:SoftDeleteColumnName="IsDeleted">

تا اینجای کار ما توانستیم یک Annotation جدید را به Ef اضافه نماییم .

در مرحله بعد بایستی به Ef دستور دهیم که در تولید Query بر روی این Entity، این مورد را نیز لحاظ کند.

برای این کار کلاسی را ایجاد می‌نماییم که از اینترفیس IDbCommandTreeInterceptor ارث بری می‌نماید. مانند کد زیر :

public class SoftDeleteInterceptor : IDbCommandTreeInterceptor
    {
        public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
        {
            if (interceptionContext.OriginalResult.DataSpace == System.Data.Entity.Core.Metadata.Edm.DataSpace.SSpace)
            {
                var QueryCommand = interceptionContext.Result as DbQueryCommandTree;
                if (QueryCommand != null)
                {
                    var newQuery = QueryCommand.Query.Accept(new SoftDeleteQueryVisitor());
                    interceptionContext.Result = new DbQueryCommandTree(QueryCommand.MetadataWorkspace, QueryCommand.DataSpace, newQuery);
                }
            }
       }
}

در ابتدا تشخیص داده می‌شود که نوع خروجی Query آیا از نوع Storage Model است . ( برای توضیحات بیشتر ) سپس پرس و جوی تولید شده را با استفاده از الگوی visitor تغییر داده و Query جدید را تولید نموده و در انتها Query جدیدی را به جای Query قبلی جایگزین می‌نماییم.

در اینجا ما نیاز به داشتن کلاس  SoftDeleteQueryVisitor  برای تغییر دادن Query و اضافه نمودن IsDeleted <>1 به Query می‌باشیم.

یک کلاس دیگری با نام  SoftDeleteQueryVisitor  به شکل زیر  به برنامه اضافه می‌نماییم.

  public class SoftDeleteQueryVisitor : DefaultExpressionVisitor
    {
        public override DbExpression Visit(DbScanExpression expression)
        {
            var column = SoftDeleteAttribute.GetSoftDeleteColumnName(expression.Target.ElementType);
            if (column!=null)
            {
                var Binding = DbExpressionBuilder.Bind(expression);
                return DbExpressionBuilder.Filter(Binding, DbExpressionBuilder.NotEqual(DbExpressionBuilder.Property(DbExpressionBuilder.Variable(Binding.VariableType, Binding.VariableName), column), DbExpression.FromBoolean(true)));
            }
            else
            {
                return base.Visit(expression);
            }
        }
    }
در متد Visit تشخیص داده می‌شود که آیا Query ساخته شده دارای customannotation:SoftDeleteColumnName است؟ چنانچه این Annotation را دارا باشد، نام فیلدی را که بالای Entity ذکر شده است، بازگشت می‌دهد و در خط بعدی، نام این فیلد را با مقدار مخالف True به Query تولید شده اضافه می‌نماید.

در نهایت برای اینکه EF تشخیص دهد که یک‌چنین Interceptor ایی وجود دارد، بایستی در کلاس DbContextConfig، کلاس SoftDeleteInterceptor را اضافه نماییم؛ همانند کد زیر:

 public class DbContextConfig : DbConfiguration
    {
        public DbContextConfig()
        {
             AddInterceptor(new SoftDeleteInterceptor());
        }
    }

تا اینجا در تمام Query‌های تولید شده بر روی Entity که با خاصیت SoftDelete مزین شده است، مقدار IsDeleted <> 1 را به صورت اتوماتیک اعمال می‌نماید. حتی به صورت هوشمند چنانچه این موجودیت در یک Join استفاده شده باشد این شرط را قبل از Join به Query تولید شده اضافه می‌نماید.

در مقاله بعدی در مورد تغییر کد Remove به کد Update توضیح داده خواهد شد.


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

Entity Framework: Building Applications with Entity Framework 6

مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت چهارم
تا قسمت سوم توانستیم Xamarin را نصب و پروژه‌ی اولیه آن را بیلد کنیم. سپس کد مشترک بین سه پلتفرم را بر روی Windows اجرا و Edit & continue آن را هم تست کردیم که هم برای UI ای که با Xaml نوشته می‌شود و هم برای منطقی که با CSharp نوشته می‌شود، کار می‌کند.
همانطور که گفتیم، کد UI و Logic برای هر سه پلتفرم مشترک است؛ منتهی به علت امکانات دیباگ فوق العاده و سرعت بیشتر ویندوز، ابتدا آن را بر روی ویندوز تست کردیم و بعد برای تکمیل UI، آن را بر روی Android اجرا می‌کنیم. این بار می‌توانید دو پروژه UWP و iOS را Unload کنید و سپس پروژه Android ای را در صورت Unload بودن Load کنید. (با راست کلیک نمودن روی پروژه). این کار باعث می‌شود Visual Studio بیهوده کند نشود؛ مخصوصا اگر سیستم شما ضعیف است.
ابتدا با موبایل یا تبلت اندرویدی شروع می‌کنیم. اگر چه Xamarin از نسخه‌ی 4.0.3 اندروید به بالا را پشتیبانی می‌کند، ولی توصیه می‌کنم وقتتان را بر روی گوشی‌های اندرویدی کمتر از 4.4 تلف نکنید. دستگاه را می‌توانید، هم به صورت USB و هم به صورت Wifi استفاده کنید. ابتدا باید دستگاه اندرویدی خود را آماده‌ی برای دیباگ کنید. برای این منظور مقاله‌های فارسی و انگلیسی زیادی وجود دارند که می‌توانید از آن‌ها استفاده کنید. من عبارت "اندروید debug" را جستجو کردم و به این مقاله رسیدم. همچنین Android SDK شما باید USB debugging اش نصب شده باشد که البته حجم زیادی ندارد. برای بررسی این مورد ابتدا از وجود فولدر extras\google\usb\_driver درAndroid SDK خود مطمئن شوید. حال سؤال این است که ویژوال استودیو، Android SDK را کجا نصب کرده‌است که خیلی ساده در این لینک توضیح داده شده‌است.
اگر فولدر extras\google\usb\_driver وجود نداشت، باید آن را نصب کنید که خیلی ساده توسط Android SDK Manager امکان پذیر است؛ ولی نه! امکان پذیر نیست!
دلیل: در Xamarin شما همیشه بر روی آخرین SDK‌ها حرکت می‌کنید. این شامل Windows SDK 17134 و Android SDK 27 و iOS SDK 11 است. وقتی از نسخه‌ی فعلی ویژوال استودیو، یعنی 15.8 به نسخه‌ی بعدی ویژوال استودیو که الان Preview است بروید، یعنی 15.9، عملا به این معنا است که به Windows SDK 17763 و Android SDK 28 و iOS SDK 12 می‌روید. این بزرگترین مزیت Xamarin است و این یعنی شما همیشه به صد در صد امکانات هر پلتفرم در زبان CSharp دسترسی دارید و همیشه آخرین SDK هر سیستم عامل در اختیار شماست و اگر دوستی از طریق Swift توانست مثالی از ARKit 2.0 را در iOS 12 پیاده سازی کند، قطعا شما هم می‌توانید. همچنین تیم Xamarin نه تنها این امکانات را بلکه Documentation لازم را نیز در اختیار شما قرار می‌دهد. چون در همین مثال، مستندات Apple به زبان Swift / Objective-C بوده و مستندات Xamarin به زبان CSharp.
حال اگر سری به فولدر Android SDK نصب شده‌ی توسط Visual Studio بزنید، مشاهده می‌کنید که خبری از Android SDK Manager نیست! به صورت رسمی، مدتی است که گوگل در نسخه‌های اخیر Android SDK، دیگر Android SDK Manager را ارائه نمی‌کند و همانطور که گفتم شما الان بر روی آخرین نسخه‌ی Android SDK هستید. هر چند ترفندهایی وجود دارد که این Manager را باز می‌گردانند، ولی لزومی به انجام این کار در Xamarin نیست و شما می‌توانید از Android SDK Manager ای که تیم Xamarin ارائه داده‌است، استفاده کنید. همین مسئله در مورد Android Virtual Device Manager که برای مدیریت Emulator‌ها بود نیز صدق می‌کند.
برای استفاده از این دو، ضمن استفاده از ابزارهای دور زدن تحریم، در ویژوال استودیو، در منوی Tools، به قسمت Android رفته و Android SDK Manager را باز کنید. Android Emulator Manager نیز جایگزین Android Virtual Device Manager ای است که قبلا توسط گوگل ارائه می‌شد. حال بعد از باز کردن Android SDK Manager ارائه شده توسط Xamarin، به برگه‌ی Tools آن بروید و از  قسمت extras مطمئن شوید که Google USB driver تیک خورده باشد.
حال پس از وصل کردن گوشی یا تبلت اندرویدی به سیستم توسط کابل USB و Set as startup project نمودن پروژه‌ی XamApp.Android که در قسمت قبل آن را Clone کرده بودید، می‌توانید پروژه را بر روی گوشی خود اجرا کنید. اگر نام گوشی خود را در کنار دکمه‌ی سبز اجرای پروژه (F5) نمی‌بینید، بستن و باز کردن Visual Studio را امتحان کنید. 

پروژه را که اجرا کنید، اولین بیلد کمی طول می‌کشد (اولین بار دو برنامه بر روی گوشی شما نصب می‌شوند که برای کار دیباگ در Xamarin لازم هستند) و اساسا بیلد یک پروژه‌ی اندرویدی کند است. خوشبختانه به واسطه وجود Xaml edit and continue احتیاجی به Stop - Start کردن پروژه و بیلد کردن برای اعمال تغییرات UI نیست و به محض تغییر Xaml، می‌توانید تاثیر آن را در گوشی خود ببینید. ولی برای هر تغییر CSharp باید Stop - Start و Build کنید که زمان بر است و به همین علت تست بر روی پروژه ویندوزی را برای پیاده سازی منطق برنامه پیشنهاد می‌کنیم. البته در نسخه‌ی 15.9 ویژوال استودیو، سرعت بیلد تا 40% بهبود یافته است.
ممکن است شما گوشی اندرویدی یا تبلت نداشته باشید که بخواهید بر روی آن تست کنید و یا مثلا گوشی شما Android 7 هست و می‌خواهید بر روی Android 8 تست بگیرید. در این جا شما احتیاج به استفاده از Emulator را خواهید داشت.
توجه داشته باشید که Emulator شما ترجیحا نباید ARM باشد و بهتر است یا X86 یا X64 باشد، وگرنه ممکن است خیلی کند شود. همچنین بهتر است Google Play Services داشته باشد. همچنین ترجیحا دنبال گزینه‌ی اجرا کردن Emulator نروید؛ اگر خود ویندوز شما درون یک Virtual Machine در حال اجراست.

ابتدا ضمن جستجو کردن "فعال سازی intel virtualization"، اقدام به فعال سازی این امکان در سیستم خود کنید. این آموزش را مناسب دیدم.
گزینه‌های مطرح: [Google Android Emulator] - [Genymotion] - [Microsoft Hyper-V Android Emulator] که فقط یکی از آنها را لازم دارید.

Google Android Emulator توسط خود Google ارائه می‌شود و دارای Google Play Services نیز هست. بر اساس این آموزش به صفحه Workloads در Visual Studio Installer بروید و از قسمت Xamarin دو مورد "Google Android Emulator API Level 27" و "Intel Hardware Accelerated Execution Manager (HAXM) global install" را نصب کنید. توجه داشته باشید که بدین منظور احتیاج به ابزارهای دور زدن تحریم دارید؛ زیرا نیاز به دسترسی به سرورهای گوگل هست. این Emulator آماده برای دیباگ هست و نیازی به اقدام خاصی نیست.

Genymotion حجم کمتری دارد و برای دانلود احتیاج به ابزارهای دور زدن تحریم را ندارد و اساسا نسبت به بقیه بر روی سیستم‌های ضعیف‌تر، بهتر کار می‌کند. فقط Emulator ای که با آن می‌سازید، به صورت پیش فرض Google Play Services را ندارد که در آخرین نسخه‌های آن گزینه Open  GApps به toolbar اضافه شده که Google Play Services را اضافه می‌کند. (از انجام هر گونه عملیات پیچیده بر اساس آموزش‌هایی که برای نسخه‌های قدیمی‌تر Genymotion هستند، پرهیز کنید). مطابق با ابتدای همین آموزش برای دستگاه‌های اندرویدی، Emulator خود را آماده برای دیباگ کنید.

Microsoft Hyper-V android emulators. مایکروسافت قبلا اقدام به ارائه یک Android Emulator کرده بود که برای نسخه 4 و 5 اندروید بودند و بزرگ‌ترین ضعف آنها عدم پشتیبانی از Google Play Services بود که ادامه داده نشدند. ولی سری جدید ارائه شده توسط مایکروسافت چنین مشکلی را ندارد. اگر CPU شما AMD بوده و روش‌های قبلی برای شما کند هستند یا اساسا کار نمی‌کنند، یا در حال حاضر در حال استفاده از Docker for Windows هستید که از Hyper-V استفاده می‌کنید و قصد استفاده مجدد از منابع موجود را دارید، این نیز گزینه خوبی است که جزئیات آن را می‌توانید در  اینجا  دنبال کنید. این Emulator آماده برای دیباگ هست و نیازی به اقدام خاصی نیست. 

پس از اینکه Emulator خود را ساختید، آن را اجرا کنید. سپس برنامه را از درون ویژوال استدیو اجرا کنید. مطابق نسخه ویندوزی، دوباره یک دکمه دارید و یک Label، عدد بر روی Label، با هر بار کلیک کردن بر روی دکمه، افزایش می‌یابد.
سرعت اجرای این برنامه در Emulator یا گوشی شما برای دیباگ است و در حالت Release، سرعت چندین برابر بهتر خواهد شد و به هیچ وجه تست‌های Performance را بر روی Debug mode انجام ندهید.

حال نوبت به پابلیش پروژه می‌رسد. در این قسمت باید توجه کنید که حجم Apk شما برای پروژه‌ی XamApp مثال ما به 7 مگ می‌رسد که برای یک فرم ساده خیلی زیاد به نظر می‌رسد. ولی اگر شما بجای یک فرم ساده، صد فرم پیچیده نیز داشته باشید، باز هم این حجم به 8 مگ نخواهد رسید. حجم Apk خیلی متاثر از کدهای شما نیست، بلکه شامل موارد زیر است:
1- NET. که خود شامل CLR  & BCL است. (BCL (Base Class Library  مثل کلاس‌های string - Stream - List - File و (CLR (Common language runtime که شامل موارد لازم برای اجرای کدها است. این پیاده سازی بر اساس NET Standard 2.0. بوده که عملا اجازه استفاده از تعداد خیلی زیادی از کتابخانه‌های موجود را می‌دهد، حتی Entity framework core! البته هر کتابخانه حجم DLL‌های خودش را اضافه می‌کند.
2- Android Support libraries که به شما اجازه می‌دهد از تعداد زیادی (و البته نه همه) امکانات نسخه‌های جدید اندروید در پروژه‌تان استفاده کنید که بر روی نسخ قدیمی‌تر Android نیز کار کنند. همچنین با یکپارچگی با Google Play Services عملا خیلی از کارها ساده‌تر و با Performance بهتری انجام می‌شود، مانند گرفتن موقعیت کاربر جاری.
3-  Xamarin essentials . اگر چه در CSharp شما به صد در صد امکانات هر سیستم عامل دسترسی دارید و می‌توانید مثلا مقدار درصد شارژ باطری را بخوانید، ولی اینکار مستلزم نوشتن سه کد CSharp ای برای Android - iOS - Windows است که طبیعتا کار را سخت می‌کند. اما Xamarin Essentials به شما اجازه می‌دهد با یک کد CSharp واحد برای هر سه پلتفرم، با باطری، کلیپ‌بورد، قطب نما و خیلی موارد دیگر کار کنید.
4- Xamarin.Forms. اگر Button و Label ای که در مثال برنامه داشتیم، با یکبار نوشتن بر روی هر سه پلتفرم دارند کار می‌کنند، در حالی که هر پلتفرم، Button مخصوص به خود را دارد؛ این را Xamarin Forms مدیریت می‌کند. علاوه بر این، Binding نیز به عهده‌ی Xamarin Forms است.
5- Prism Autofac Bit Framework: درک آن‌ها نیاز به دنبال کردن آموزش‌های این دوره را دارد؛ ولی به صورت کلی معماری پروژه شما بسیار کارآمد و حرفه‌ای خواهد شد و به کدی با قابلیت نگهداری بالا خواهید رسید. 
6-  Rg Plugins Popup  و  Xamanimation  نیز دو کتابخانه‌ی UI بسیار کاربردی و جالب هستند که در طول این آموزش از آنها استفاده خواهد شد.
حجم 7 مگ برای این تعداد کتابخانه و امکان، خیلی زیاد نیست و شما عملا تعداد زیادی از پروژه‌های خود را می‌توانید با همین حجم ببندید و اگر مثلا به پروژه‌ی Humanizer خیلی علاقه داشته باشید (که در این صورت حق هم دارید!) می‌توانید با اضافه شدن چند کیلوبایت (!) به پروژه آن را داشته باشید. اکثر کتابخانه‌های NET. ای سبک هستند. همچنین موقع قرار گرفتن در پروژه، فشرده سازی نیز می‌شوند و قسمت‌های استفاده نشده‌ی آن‌ها نیز توسط Linker حذف می‌شوند.
علاوه بر این، اجرای برنامه بر روی گوشی‌های ضعیف و قدیمی کمی طول می‌کشد. این مربوط به اجرای برنامه است؛ نه باز شدن فرم مثال ما که دارای Button و Label بود و اگر مثال ما دو فرم داشته باشد (که در آموزش‌های بعدی به آن می‌رسیم) می‌بینید که چرخش بین فرم‌ها بسیار سریع است.

مواردی مهم در زمینه‌ی بهبود عملکرد پروژه‌های Xamarin در Android
در ابتدا باید بدانید Apk شما شامل دو قسمت است؛ یکی کدهای CSharp ای شما که DotNet ای بوده و در کنار کدهای کتابخانه‌هایی چون Json.NET بر روی DotNet اجرا می‌شوند. دیگری کتابخانه‌ای است که مثلا با Java نوشته شده و بعد برای استفاده در CSharp بر روی آن یک Wrapper یا پوشاننده توسعه داده شده‌است. عموما توسعه دهندگان چنین پروژه‌هایی، ابتدا پروژه را به Java می‌نویسند و بعد برای JavaScript - CSharp و ... Wrapper ارائه می‌دهند.
برای بهبود اینها ابزارهایی چون AOT-NDK-LLVM-ProGurad-Linker و ... وجود دارند که سعی می‌کنم به صورت ساده آنها را توضیح دهم.

وظیفه ProGurad این است که از قسمتی از پروژه‌ی شما که بخاطر کتابخانه‌های Java ای، عملا DotNet ای نیست، کدهای اضافه و استفاده نشده را حذف کند.
ممکن است استفاده از ProGurad باعث شود کلاسی که داینامیک استفاده شده است، به اشتباه حذف شود. پروژه XamApp دارای یک ProGuard configuration file است که جلوی چنین اشتباهاتی را حتی الامکان می‌گیرد.
همچنین ProGurad که در داخل Android SDK قرار دارد، به Space در طول مسیر حساس است (!) و با توجه به اینکه مسیر پیش فرض Android SDK نصب شده‌ی توسط ویژوال استودیو دارای Space است (C:\Program Files (x86)\Android\android-sdk)  شما در همان ابتدا دچار مشکل می‌شوید! برای حل این مشکل ابدا فولدر Android SDK را جا به جا نکنید؛ بلکه از امکانی در ویندوز به نام Junction folder یا فولدر جانشین استفاده کنید. بدین منظور دستور زیر را وارد کنید:
mklink /j C:\android-sdk "C:\Program Files (x86)\Android\android-sdk"
این مورد باعث می‌شود که مسیر C:\android-sdk نیز به همان مسیر پیش فرض اشاره کند و این دو مسیر در واقع یکی هستند. امیدوارم این امکان را با قابلیت Shortcut سازی در ویندوز اشتباه نگیرید! حال از منوی Tools > Options > Xamarin > Android Settings مسیر Android SDK را به C:\android-sdk تغییر دهید که فاقد Space است و ویژوال استودیو را ببندید و باز کنید.

NDK که در ادامه SDK برای Android قرار می‌گیرد، Native Development Kit است و باعث می‌شود هم DLL‌های DotNet ای و هم Jar‌های Java ای به فایل‌های so تبدیل شوند. so همان DLL ویندوز است، البته برای Linux و همانطور که احتمالا می‌دانید، پایه Android بر روی Linux است. طبیعتا کامپایل شدن کدها به so، بر روی بهبود سرعت برنامه تاثیر گذار است.

Linker نیز مشابه با ProGuard کمک می‌کند، ولی اینبار حجم DLL‌های DotNet ای مانند Json.NET را کم می‌کند. بالاخره شما از صد در صد کلاس‌های یک DLL استفاده نمی‌کنید و موارد اضافی نیز باید حذف شوند. البته این وسط، امکان حذف اشتباه کلاس‌هایی که به صورت داینامیک فراخوانی شده باشند وجود دارد که LinkerConfig موجود در پروژه XamApp حتی الامکان جلوی این مشکل را می‌گیرد.

Release mode  مثل هر پروژه CSharp ای دیگری، بهتر است پروژه در حالت Release mode پابلیش شود. در پروژه XamApp در حالت Release mode، موارد بالا یعنی Linker-NDK-ProGuard نیز درخواست می‌شوند.

جزئیات این موارد در مستندات Xamarin وجود دارد و در پایان این دوره یک Project Builder سورس باز نیز به شما ارائه می‌شود که ساختار اولیه پروژه‌ها را بر اساس نیازهای شما و با بهترین تنظیمات ممکن می‌سازد.

در پروژه XamApp علاوه بر موارد فوق، دو مورد دیگر نیز آماده به استفاده هستند، ولی غیر فعال شده اند؛ AOT و LLVM. اگر به تازگی برنامه نویس شده‌اید، موارد زیر ممکن است خیلی برایتان پیچیده باشند، از آن‌ها عبور کنید و به عنوان "نحوه انجام دادن پابلیش" بروید.

کدهای‌های DotNet ای به سه شکل می‌توانند اجرا شوند:
JIT - AOT - Interpreter
یک برنامه DotNet ای برای اجرا می‌تواند از ترکیب اینها استفاده کند. حالت Interpreter که خیلی جدید معرفی شده و الآن موضوع بحث نیست؛ می‌ماند JIT و AOT
کد CSharp در هنگام کامپایل به IL تبدیل و سپس در زمان اجرا توسط Just in time compiler به زبان ماشین تبدیل می‌شود. اگر قبلا پروژه‌ی ASP.NET یا ASP.NET Core نوشته باشید، چنین رفتاری را در پشت صحنه خواهد داشت. خود JIT که در هر بار اجرای برنامه انجام می‌شود، عملا زمان بر هست. ولی کد زبان ماشین حاصل از آن خیلی Optimize شده برای دقیقا همان ماشین هست؛ با در نظر گرفتن خیلی فاکتورها. در پروژه‌های سمت سرور مثل ASP.NET که پروژه وقتی یک بار اجرا می‌شود، مثلا روی IIS، ممکن است صدها هزار دستور را اجرا کند، در طول چندین روز یا ماه، این عمل JIT خیلی مفید هست. البته همان سربار اولیه‌ی JIT هم توسط چیزی به عنوان Tiered JIT می‌تواند کمتر شود.
اما در پروژه‌ی موبایل که برنامه ممکن است بعد از باز شدن، مثلا ده دقیقه باز باشد و بعد بسته شود، انجام شدن JIT با هر بار باز شدن برنامه خیلی مفید به فایده نیست. بنا به برخی مسائل که واقعا سطح این آموزش را خیلی پیچیده می‌کند، نتیجه کار JIT قابلیت Cache شدن آن چنانی ندارد و عملا باید هر بار اجرا شود.
در پروژه‌های موبایل، گزینه دیگری بر روی میز هست به نام Ahead of time یا AOT که کار تبدیل IL به زبان ماشین را در زمان کامپایل و پابلیش پروژه انجام دهد. طبیعتا این باعث می‌شود سرعت برنامه موبایل در عمل خیلی بالاتر رود، چون سربار JIT در هر بار اجرای برنامه حذف می‌شود. همچنین روال AOT می‌تواند از LLVM یا Low level virtual machine استفاده کند که منجر به تبدیل شدن کد زبان ماشینی می‌شود که بر روی LLVM کار می‌کند. LLVM خودش یک Runtime با سرعت خیلی بالاست که بر روی تمامی سیستم عامل‌ها کار می‌کند.
بر روی Android - iOS - Windows می‌شود از AOT استفاده کرد. در iOS و ویندوز، استفاده‌ی از AOT منجر به افزایش سایز برنامه نمی‌شود، چون قبلا برنامه یک سری کد IL بوده که زمان اجرا توسط JIT به کد ماشین تبدیل می‌شده و الان بجای آن IL، یک سری کد زبان ماشین مبتنی بر LLVM هست. اما بر روی Android، پیاده سازی AOT ناقص هست و البته که با فعال کردن‌اش، سرعت برنامه بسیار بیشتر می‌شود، ولی کماکان نیاز به JIT و IL هم برای برخی از سناریوها هست. این مورد یعنی اینکه فعال سازی AOT+LLVM بر روی اندروید تا مادامی که AOT در Android به صورت آزمایشی هست، باعث افزایش حجم Apk ما از 7 به 13 مگ می‌شود. البته این مورد در نسخه‌های بعدی رفع خواهد شد و رفتار Android مشابه با iOS-Windows خواهد بود؛ یعنی حجم نسبتا کم و سرعت خیلی بالا.
برای فعال سازی AOT+LLVM در csproj پروژه اندرویدی، دو مقدار AotAssemblies و EnableLLVM را از false به true تغییر دهید:
 <AotAssemblies>true</AotAssemblies> 
 <EnableLLVM>true</EnableLLVM>
با این تنظیمات، بیلد شما طولانی‌تر و در عوض سرعت اجرای برنامه بیشتر خواهد شد.

نحوه انجام دادن پابلیش 
برای انجام دادن پابلیش، بر روی پروژه XamApp.Android در هنگامیکه بر روی Release mode هستید، راست کلیک کنید و Archive را بزنید. سپس فایل Archive شده را انتخاب و Distribute را بزنید که به شما Apk مناسب برای انتشار توسط خودتان یا Google Play می‌دهد.
نکات مهم:
1- فایل Apk حاصل از Archive را بدون Distribute کردن، در اختیار کسی قرار ندهید. فقط پیام Corrupt و خراب بودن فایل، حاصل کارتان خواهد بود!
2- اولین باری که Distribute می‌کنید، Wizard مربوطه کمک می‌کند تا یک فایل Certificate را برای Apk اتان بسازید. آن فایل را گم نکنید! در پابلیش‌های بعدی دیگر نباید Certificate جدیدی بسازید؛ بلکه فایل قبلی را باید به آن معرفی کنید و فقط رمز آن Certificate را دوباره بزنید.
3- به برنامه آیکون بدهید. برای آن Splash Screen خوبی بگذارید. در هر بار پابلیش، ورژن برنامه را افزایش دهید. اینها همگی توضیحات اش بر روی بستر وب موجود است. سؤالی بود، همینجا هم می‌توانید بپرسید.
فایل‌های Apk این مثال را می‌توانید از اینجا دانلود کنید.

در قسمت بعدی آموزش، دیباگ و پابلیش گرفتن پروژه بر روی iOS را خواهیم داشت که البته مقداری از مطالب اش با مطالب این آموزش مشترک هست. بعد دست به کد شده و آموزش CSharp و Xaml را خواهیم داشت تا پروژه‌ای با کیفیت، کارآمد و عالی از هر جهت بنویسید.
همچنین تعدادی از نکات مربوط به Performance که مربوط به ظاهر برنامه و نحوه چیدمان صفحات و کنترل‌ها هستند و بر روی Performance هر سه پلتفرم تاثیر گذار هستند (و نه فقط Android‌) نیز در ادامه بحث خواهند شد.
مطالب
پیاده سازی UnitOfWork برای BrightStarDb
 در این پست با BrightStarDb و مفاهیم اولیه آن آشنا شدید. همان طور که پیش‌تر ذکر شد BrightStarDb از تراکنش‌ها جهت ذخیره اطلاعات پشتیبانی می‌کند. قصد داریم روش شرح داده شده در اینجا را بر روی BrightStarDb فعال کنیم. ابتدا بهتر است با روش ساخت مدل در B*Db آشنا شویم.
*یکی از پیش نیاز‌های این پست مطالعه این دو مطلب (^ )  و (^ ) می‌باشد.
فرض می‌کنیم در دیتابیس مورد نظر یک Store به همراه یک جدول به صورت زیر داریم:
[Entity]
    public interface IBook
    {
        [Identifier]
        string Id { get; }

        string Title { get; set; }

        string Isbn { get; set; }
    }
بر روی پروژه مورد نظر کلیک راست کرده و گزینه Add new Item را انتخاب نمایید. از برگه Data  گزینه BrightStar Entity Context را انتخاب کنید

بعد از انخاب گزینه بالا یک فایل با پسوند tt به پروژه اضافه خواهد شد که وظیفه آن جستجو در اسمبلی مورد نظر و پیدا کردن تمام اینترفیس هایی که دارای  EntityAttribute هستند و همچنین ایجاد کلاس‌های متناظر جهت پیاده سازی اینترفیس‌های بالا است. در نتیجه ساختار پروژه تا این جا به صورت زیر خواهد شد.

واضح است که فایلی به نام Book به عنوان پیاده سازی مدل IBook  به عنوان زیر مجموعه فایل DatabaseContext.tt به پروژه اضافه شده است.

تا اینجا برای استفاده از Context مورد نظر باید به صورت زیر عمل نمود:

DatabaseContext context = new DatabaseContext();    
  context.Books.Add(new Book());
Context پیش فرض ساخته شده توسط B*Db از Generic DbSet‌های معادل EF پشتیبانی نمی‌کند و از طرفی IUnitOfWork مورد نظر به صورت زیر است
public interface IUnitOfWork
    {
        BrightstarEntitySet<T> Set<T>() where TEntity : class;
        void DeleteObject(object obj); 
         void SaveChanges();
    }
در اینجا فقط به جای  IDbSet از BrightStarDbSet استفاده شده است. همان طور که در این مقاله توضیح داده شده است، برای پیاده سازی مفهوم UnitOfWork؛ نیاز است تا کلاس DatabaseContext که نماینده BrightStarDbContext پروژه است، از اینترفیس IUnitOfWork طراحی شده ارث بری کند. جهت انجام این مهم  و همچنین جهت اضافه کردن قابلیت ایجاد Generic DbSet‌ها نیز باید کمی در فایل Template Generator تغییر ایجاد نماییم. این تغییرات را قبلا در طی یک پروژه ایجاد کرده‌ام و شما می‌توانید آن را از اینجا دریافت کنید. بعد از دانلود کافیست فایل DatabaseContext.tt مورد نظر را در پروژه خود کپی کرده و گزینه Run Custom Tools را فراخوانی نمایید.

نکته: برای حذف یک آبجکت از Store، باید از متد DeleteObject تعبیه شده در Context استفاده نماییم. در نتیجه متد مورد نظر نیز در اینترفیس بالا در نظر گرفته شده است.

استفاده از IOC Container جهت رجیستر کردن IUnitOfWrok
در این قدم باید IUnitOfWork را در یک IOC container رجیستر کرده تا در جای مناسب عملیات وهله سازی از آن میسر باشد. من در اینجا از Castle Windsor Container استفاده کردم. کلاس زیر این کار را برای ما انجام خواهد داد:
 public class DependencyResolver
    {
        public static void Resolve(IWindsorContainer container)
        {
            var context = new DatabaseContext("type=embedded;storesdirectory=c:\brightstar;storename=test ");
            container.Register(Component.For<IUnitOfWork>().Instance(context).LifestyleTransient());
        }
    }
حال کافیست در کلاس‌های سرویس برنامه UnitOfWork رجیستر شده را به سازنده آن‌ها تزریق نماییم.
public class BookService
    {
        public BookService(IUnitOfWork unitOfWork)
        {
            UnitOfWork = unitOfWork;
        }

        public IUnitOfWork UnitOfWork
        {
            get;
            private set;
        }

        public IList<IBook> GetAll()
        {
            return UnitOfWork.Set<IBook>().ToList();
        }

        public void Add()
        {
            UnitOfWork.Set<IBook>().Add(new Book());
        }

        public void Remove(IBook entity)
        {
            UnitOfWork.DeleteObject(entity);
        }
    }
سایر موارد دقیقا معادل مدل EF آن است.
نکته: در حال حاضر امکان جداسازی مدل‌های برنامه (تعاریف اینترفیس) در قالب یک پروژه دیگر(نظیر مدل CodeFirst در EF) در B*Db امکان پذیر نیست.
نکته : برای اضافه کردن آیتم جدید به Store نیاز به وهله سازی از اینترفیس IBook داریم. کلاس Book ساخته شده توسط DatabaseContext.tt در عملیات Insert و update کاربرد خواهد داشت.

مطالب
MVC Scaffolding #2
از آنجائیکه اصل کار با MVC Scaffolding از طریق خط فرمان پاورشل انجام می‌شود، بنابراین بهتر است در ادامه با گزینه‌ها و سوئیچ‌های مرتبط با آن بیشتر آشنا شویم.
دو نوع پارامتر حین کار با MVC Scaffolding مهیا هستند:

الف) سوئیچ‌ها
مانند پارامترهای boolean عمل کرده و شامل موارد ذیل می‌باشند. تمام این پارامترها به صورت پیش فرض دارای مقدار false بوده و ذکر هرکدام در دستور نهایی سبب true شدن مقدار آن‌ها می‌گردد:
Repository: برای تولید کدها بر اساس الگوی مخزن
Force: برای بازنویسی فایل‌های موجود.
ReferenceScriptLibraries: ارجاعاتی را به اسکریپت‌های موجود در پوشه Scripts، اضافه می‌کند.
NoChildItems: در این حالت فقط کلاس کنترلر تولید می‌شود و از سایر ملحقات مانند تولید Viewها، DbContext و غیره صرفنظر خواهد شد.

ب) رشته‌ها
این نوع پارامترها، رشته‌ای را به عنوان ورودی خود دریافت می‌کنند و شامل موارد ذیل هستند:
ControllerName: جهت مشخص سازی نام کنترلر مورد نظر
ModelType: برای ذکر صریح کلاس مورد استفاده در تشکیل کنترلر بکار می‌رود. اگر ذکر نشود، از نام کنترلر حدس زده خواهد شد.
DbContext: نام کلاس DbContext تولیدی را مشخص می‌کند. اگر ذکر نشود از نامی مانند ProjectNameContex استفاده خواهد کرد.
Project: پیش فرض آن پروژه جاری است یا اینکه می‌توان پروژه دیگری را برای قرار دادن فایل‌های تولیدی مشخص کرد. (برای مثال هربار یک سری کد مقدماتی را در یک پروژه جانبی تولید کرد و سپس موارد مورد نیاز را از آن به پروژه اصلی افزود)
CodeLanguage: می‌تواند cs یا vb باشد. پیش فرض آن زبان جاری پروژه است.
Area: اگر می‌خواهید کدهای تولیدی در یک ASP.NET MVC area مشخص قرار گیرند، نام Area مشخصی را در اینجا ذکر کنید.
Layout: در حالت پیش فرض از فایل layout اصلی استفاده خواهد شد. اما اگر نیاز است از layout دیگری استفاده شود، مسیر نسبی کامل آن‌را در اینجا قید نمائید.

یک نکته:
نیازی به حفظ کردن هیچکدام از موارد فوق نیست. برای مثال در خط فرمان پاورشل، دستور Scaffold را نوشته و پس از یک فاصله، دکمه Tab را فشار دهید. لیست پارامترهای قابل اجرای در این حالت ظاهر خواهند شد. اگر در اینجا برای نمونه Controller انتخاب شود، مجددا با ورود یک فاصله و خط تیره و سپس فشردن دکمه Tab، لیست پارامترهای مجاز و همراه با سوئیچ کنترلر ظاهر می‌گردند.


MVC Scaffolding و مدیریت روابط بین کلاس‌ها

مثال قسمت قبلی بسیار ساده و شامل یک کلاس بود. اگر آن‌را کمی پیچیده‌تر کرده و برای مثال روابط one-to-many و many-to-many را اضافه کنیم چطور؟
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcApplication1.Models
{
    public class Task
    {
        public int Id { set; get; }

        [Required]
        public string Name { set; get; }

        [DisplayName("Due Date")]
        public DateTime? DueDate { set; get; }

        [ForeignKey("StatusId")]
        public virtual Status Status { set; get; } // one-to-many
        public int StatusId { set; get; }

        [StringLength(450)]
        public string Description { set; get; }

        public virtual ICollection<Tag> Tags { set; get; } // many-to-many
    }

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

        [Required]
        public string Name { set; get; }

        public virtual ICollection<Task> Tasks { set; get; } // many-to-many
    }

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

        [Required]
        public string Name { set; get; }
    }
}
کلاس Task تعریف شده اینبار دارای رابطه many-to-many با برچسب‌های مرتبط با آن است. همچنین یک رابطه one-to-many با کلاس وضعیت هر Task نیز تعریف شده است. به علاوه نکته تعریف «کار با کلیدهای اصلی و خارجی در EF Code first» نیز در اینجا لحاظ گردیده است.
در ادامه دستور تولید کنترلر‌های Task، Tag و Status ساخته شده با الگوی مخزن را در خط فرمان پاورشل ویژوال استودیو صادر می‌کنیم:
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
PM> Scaffold Controller -ModelType Tag -ControllerName TagsController -DbContextType TasksDbContext -Repository -Force
PM> Scaffold Controller -ModelType Status -ControllerName StatusController -DbContextType TasksDbContext -Repository -Force
اگر به کارهایی که در اینجا انجام می‌شود دقت کنیم، می‌توان صرفه جویی زمانی قابل توجهی را شاهد بود؛ خصوصا در برنامه‌هایی که از ده‌ها فرم ورود اطلاعات تشکیل شده‌اند. فرض کنید قصد استفاده از ابزار فوق را نداشته باشیم. باید به ازای هر عملیات CRUD دو متد را ایجاد کنیم. یکی برای نمایش و دیگری برای ثبت. بعد بر روی هر متد کلیک راست کرده و Viewهای متناظری را ایجاد کنیم. سپس مجددا یک سری پیاده سازی «مقدماتی» تکراری را به ازای هر متد جهت ثبت یا ذخیره اطلاعات تدارک ببینیم. اما در اینجا پس از طراحی کلاس‌های برنامه، با یک دستور، حجم قابل توجهی از کدهای «مقدماتی» که بعدها مطابق نیاز ما سفارشی سازی و غنی‌تر خواهند شد، تولید می‌گردند.

چند نکته:
- با توجه به اینکه مدل‌ها تغییر کرده‌اند، نیاز است بانک اطلاعاتی متناظر نیز به روز گردد. مطالب مرتبط با آن‌را در مباحث Migrations می‌توانید مطالعه نمائید.
- View تولیدی رابطه many-to-many را پشتیبانی نمی‌کند. این مورد را باید دستی اضافه و طراحی کنید: (^ و ^)
- رابطه one-to-many به خوبی با View متناظری دارای یک drop down list تولید خواهد شد. در اینجا لیست تولیدی به صورت خودکار با مقادیر خاصیت Name کلاس Status پر می‌شود. اگر این نام دقیقا Name نباشد نیاز است توسط ویژگی به نام DisplayColumn که بر روی نام کلاس قرار می‌گیرد، مشخص کنید از کدام خاصیت باید استفاده شود.
@Html.DropDownListFor(model => model.StatusId,
((IEnumerable<Status>)ViewBag.PossibleStatus).Select(option => new SelectListItem {
  Text = (option == null ? "None" : option.Name),
  Value = option.Id.ToString(),
  Selected = (Model != null) && (option.Id == Model.StatusId)
}), "Choose...")
@Html.ValidationMessageFor(model => model.StatusId)


تولید آزمون‌های واحد به کمک MVC Scaffolding

MVC Scaffolding امکان تولید خودکار کلاس‌ها و متدهای آزمون واحد را نیز دارد. برای این منظور دستور زیر را در خط فرمان پاورشل وارد نمائید:
 PM> Scaffold MvcScaffolding.ActionWithUnitTest -Controller TasksController -Action ArchiveTask -ViewModel Task
دستوری که در اینجا صادر شده است نسبت به حالت‌های کلی قبلی، اندکی اختصاصی‌تر است. این دستور بر روی کنترلری به نام TasksController، جهت ایجاد اکشن متدی به نام ArchiveTask با استفاده از کلاس ViewModel ایی به نام Task اجرا می‌شود. حاصل آن ایجاد اکشن متد یاد شده به همراه کلاس TasksControllerTest است؛ البته اگر حین ایجاد پروژه جدید در ابتدای کار، گزینه ایجاد پروژه آزمون‌های واحد را نیز انتخاب کرده باشید. نام پروژه پیش فرضی که جستجوی می‌شود YourMvcProjectName.Test/Tests است.
 نکته مهم آن، عدم حذف یا بازنویسی کامل کنترلر یاد شده است. کاری هم که در تولید متد آزمون واحد متناظر انجام می‌شود، تولید بدنه متد آزمون واحد به همراه تولید کدهای اولیه الگوی Arrange/Act/Assert است. پر کردن جزئیات بیشتر آن با برنامه نویس است.
و یا به صورت خلاصه‌تر:
 PM> Scaffold UnitTest Tasks Delete
در اینجا متد آزمون واحد کنترلر Tasks و اکشن متد Delete آن، تولید می‌شود.

کار مقدماتی با MVC Scaffolding و امکانات مهیای در آن همینجا به پایان می‌رسد. در قسمت‌های بعد به سفارشی سازی این مجموعه خواهیم پرداخت.
مطالب
اجرای برنامه‌های ASP.NET توسط Mono در Ubuntu
در ادامه مباحث بررسی اجرای برنامه‌های دات نت بر روی لینوکس، قصد داریم برنامه‌های ASP.NET را به کمک Mono 3.0 و یک وب سرور لینوکسی، بر روی Ubuntu اجرا کنیم.


پیشنیازها
دو پروژه خالی ASP.NET Web forms و ASP.NET MVC را در VS.NET تحت ویندوز ایجاد نمائید. آن‌ها را یکبار کامپایل کرده و اجرا کنید. سپس فایل‌‌های آن‌ها را به ubuntu منتقل کنید (پوشه‌های bin پروژه‌ها فراموش نشوند؛ خصوصا نگارش MVC که به همراه یک سری کتابخانه جانبی است).
برای انتقال فایل‌ها به لینوکس، اگر از VMWare workstation برای اجرا و آزمایش Ubuntu استفاده می‌کنید، کپی و paste مستقیم فایل‌ها از ویندوز به درون ماشین مجازی لینوکس پشتیبانی می‌شود.


نصب وب سرور آزمایشی مونو یا XSP
اگر نیاز به یک وب سرور آزمایشی، چیزی شبیه به وب سرور توکار VS.NET داشتید، پروژه XSP جهت این نوع آزمایشات ایجاد شده است.
پس از نصب آن (که به همراه همان بسته PPA قسمت قبل، هم اکنون بر روی سیستم شما نصب است)، در ترمینال لینوکس، با استفاده از دستور cd به ریشه وب سایت خود وارد شوید، سپس دستور xsp4 را اجرا کنید تا وب سرور xsp4 مشغول هاست سایت شما شود (برای اجرا در مسیر  /opt/bin/xsp4 نصب شده است).


اجرای برنامه ASP.NET Web forms 4 توسط XSP
بدون هیچ مشکل خاصی در همان ابتدای کار اجرا شد (البته باید دقت داشت که لینوکس به کوچکی و بزرگی حروف حساس است. یعنی حتما باید Default.aspx وارد شود و نه default.aspx):



اجرای برنامه ASP.NET MVC 4 توسط XSP
اجرا نشد! پیام می‌دهد که
 "Missing method System.Web.Security.FormsAuthentication::get_IsEnabled() in assembly System.Web.dll
و یا
Compiler Error Message: CS1703: An assembly with the same identity `mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' 
has already been imported. Consider removing one of the references
علت اینجا است که XSP4 همراه با نسخه PPA، قدیمی است. بنابراین باید نسخه اصلی را از مخزن کد آن دریافت و کامپایل کنیم. پیشنیازهای اینکار مانند Git و Mono، در قسمت قبل دریافت شدند. سپس فرامین ذیل را در خط فرمان لینوکس اجرا کنید:
 git clone git://github.com/mono/xsp.git
cd xsp
./autogen.sh --prefix=/opt
make
sudo make install
پس از کامپایل، اگر این نگارش جدید را اجرا کنید، به خطای ذیل خواهید رسید:
 System.IO.FileNotFoundException: Could not load file or assembly XSP, Version=3.0.0.0
برای رفع این مشکل باید اینبار وب سرور جدید را با فرمان sudo یا دسترسی مدیریتی اجرا کنید تا مشکل برطرف شود.
البته من سورس دریافت شده را در خود monodevelop کامپایل کردم (فایل sln آن‌را در monodevelop باز کرده و پروژه را build کنید). در این حالت دو فایل Mono.WebServer.dll و Mono.WebServer.XSP.exe در پوشه  xsp/src/Mono.WebServer.XSP/bin/Debug ظاهر می‌شوند.
یکی دیگر از دلایل ظاهر شدن خطای فوق، نیاز به نصب این دو فایل در GAC است که به نحو زیر قابل انجام می‌باشد:
 cd xsp/src/Mono.WebServer.XSP/bin/Debug
sudo gacutil -i Mono.WebServer.XSP.exe
sudo gacutil -i Mono.WebServer.dll
بعد این دو فایل dll و exe را در پوشه برنامه MVC خود کپی کنید و سپس دستور ذیل را اجرا نمائید:
 cd myMvcAppPath
sudo mono Mono.WebServer.XSP.exe
اینبار وب سرور جدید، روی پورت 9000 شروع به کار می‌کند. اکنون اگر در فایرفاکس آدرس http://localhost:9000 را باز کنید، برنامه اجرا شده اما به خطای ذیل خواهید رسید:
 CS0234: The type or namespace name `Helpers' does not exist in the namespace `System.Web'.
Are you missing an assembly reference?
برای رفع این مشکل باید اندکی فایل web.config برنامه را ویرایش کرد:
  <system.web> 
    <compilation debug="true" targetFramework="4.0">
      <assemblies>
        <add assembly="System.Web.Helpers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />        
        <add assembly="System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />        
      </assemblies>
    </compilation>
سعی بعدی ... اجرا نشد! با هر بار refresh صفحه یک خطای جدید نمایش می‌داد که ... Type خاصی را نمی‌تواند بارگذاری کند (به همراه نام اسمبلی مربوطه). برای رفع این مشکل dllهای ذیل را از پوشه bin پروژه MVC خود که از ویندوز به لینوکس کپی کرده‌اید، حذف کنید:
Microsoft.Web.Infrastructure.dll
System.Net.Http.dll
System.Net.Http.Formatting.dll
System.Web.Http.dll
System.Web.Http.WebHost.dll
این فایل‌ها توسط تیم Mono به صورت مستقل پیاده سازی شده‌اند و نیازی نیست تا از ویندوز به لینوکس کپی شوند.
بعد از حذف این فایل‌های اضافی، برنامه ASP.NET MVC نیز اجرا شد:



چند نکته تکمیلی
- نحوه تشخیص موجود بودن یک DLL خاص، در نگارش جاری Mono نصب شده:
 $ gacutil -l Microsoft.Web.Infrastructure
The following assemblies are installed into the GAC:
Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
Number of items = 1
- اگر می‌خواهید مطمئن شوید که تمام اسمبلی‌های موجود در GAC درست نصب شده‌اند یا خیر، فرمان ذیل را اجرا کنید:
cd /opt/lib/mono/gac # assuming this is your main gac
sudo find . */*/*.dll -exec gacutil -i '{}' \;
- در نسخه لینوکسی System.Web ممکن است یک سری از فضاهای نام هنوز موجود نباشند. لیست آن‌ها را در این آدرس می‌توانید مشاهده کنید:
مطالب
Blazor 5x - قسمت 20 - کار با فرم‌ها - بخش 8 - استفاده از یک کامپوننت ثالث HTML Editor
در این قسمت می‌خواهیم بجای دریافت اطلاعات توضیحات یک اتاق، توسط یک text area متداول، برای مثال از Quill rich text editor استفاده کنیم. برای این منظور می‌توان از کامپوننت Blazor محصور کننده‌ی آن به نام Blazored TextEditor کمک گرفت.


نصب کامپوننت Blazored TextEditor

ابتدا نیاز است بسته‌ی نیوگت آن‌را با اجرای دستور زیر، به پروژه‌ی Blazor خود اضافه کرد:
dotnet add package Blazored.TextEditor
و همچنین کتابخانه‌ی اصلی quill را نیز در مسیر wwwroot/lib/quill نصب می‌کنیم:
libman install quill --provider unpkg --destination wwwroot/lib/quill
سپس به فایل Pages\_Host.cshtml مراجعه کرده و ابتدا مداخل تعریف فایل‌های CSS آن‌را اضافه می‌کنیم:
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorServer.App</title>
    <base href="~/" />
    <link href="lib/quill/dist/quill.snow.css" rel="stylesheet" />
    <link href="lib/quill/dist/quill.bubble.css" rel="stylesheet" />
و در ادامه سه مدخل اسکریپتی زیر را نیز به قسمت پیش از بسته شدن تگ body، اضافه می‌کنیم:
 <script src="lib/quill/dist/quill.min.js"></script>
<script src="_content/Blazored.TextEditor/quill-blot-formatter.min.js"></script>
<script src="_content/Blazored.TextEditor/Blazored-BlazorQuill.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
اگر برنامه‌ی مورد نظر از نوع Blazor WASM است، این تنظیمات به فایل wwwroot\index.html منتقل می‌شوند.

و در آخر جهت سهولت کار با این کامپوننت می‌توان فضای نام آن‌را به فایل BlazorServer.App\_Imports.razor به صورت زیر اضافه کرد:
@using Blazored.TextEditor


استفاده از کامپوننت Blazored.TextEditor در کامپوننت HotelRoomUpsert.razor

می‌خواهیم در کامپوننت HotelRoomUpsert.razor مثال این سری، بجای کامپوننت InputTextArea مورد استفاده، از یک HTML Editor استفاده کنیم:
<div class="form-group">
    <label>Details</label>
    @*<InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea>*@
    <BlazoredTextEditor @ref="@QuillHtml">
        <ToolbarContent>
            <select class="ql-header">
                <option selected=""></option>
                <option value="1"></option>
                <option value="2"></option>
                <option value="3"></option>
                <option value="4"></option>
                <option value="5"></option>
            </select>
            <span class="ql-formats">
                <button class="ql-bold"></button>
                <button class="ql-italic"></button>
                <button class="ql-underline"></button>
                <button class="ql-strike"></button>
            </span>
            <span class="ql-formats">
                <select class="ql-color"></select>
                <select class="ql-background"></select>
            </span>
            <span class="ql-formats">
                <button class="ql-list" value="ordered"></button>
                <button class="ql-list" value="bullet"></button>
            </span>
            <span class="ql-formats">
                <button class="ql-link"></button>
            </span>
        </ToolbarContent>
        <EditorContent>
        </EditorContent>
    </BlazoredTextEditor>
</div>
- در اینجا قسمت محتوای EditorContent مثال آن‌را خالی کرده‌ایم.
- همانطور که ملاحظه می‌کنید، این تعریف به همراه یک ارجاع به وهله‌ای از آن نیز هست:
<BlazoredTextEditor @ref="@QuillHtml">
به همین جهت نیاز است فیلد متناظر با آن‌را در قسمت کدهای کامپوننت، به صورت زیر تعریف کرد:
@code
{
   private BlazoredTextEditor QuillHtml;
تا اینجا اگر برنامه را اجرا کنیم، به خروجی زیر می‌رسیم:


برای تغییر اندازه و مقدار placeholder پیش‌فرض آن، می‌توان به صورت زیر عمل کرد:
<div class="form-group pb-4" style="height:250px;">
    <label>Details</label>
    <BlazoredTextEditor @ref="@QuillHtml" Placeholder="Please enter the room's detail">


تنظیم و دریافت متن نمایشی HTML Editor

مطابق مستندات این کامپوننت، روش تنظیم متن نمایشی آن، به کمک متد LoadHTMLContent است. به همین جهت متد زیر را به کدهای کامپوننت جاری اضافه می‌کنیم:
    private async Task SetHTMLAsync()
    {
        if(!string.IsNullOrEmpty(HotelRoomModel.Details))
        {
            await QuillHtml.LoadHTMLContent(HotelRoomModel.Details);
        }
    }
بنابراین روش متداول two-way binding در اینجا کار نمی‌کند و باید متن این ادیتور را به نحو فوق تنظیم کرد و برای مثال در زمان بارگذاری اولیه‌ی این کامپوننت و در حالت ویرایش، متن دریافتی از بانک اطلاعاتی را به ادیتور فوق ارسال نمود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
            await SetHTMLAsync();
        }

        // ... 
    }
و یا در زمان ثبت اولیه و یا حتی در حالت ویرایش اطلاعات در متد HandleHotelRoomUpsert، با استفاده از متد GetHTML آن، خاصیت HotelRoomModel.Details را مقدار دهی اولیه کرد:
    private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       HotelRoomModel.Details = await QuillHtml.GetHTML();

       // ...
    }

مشکل! ادیتور در زمان ویرایش یک رکورد، اطلاعات پیشین را نمایش نمی‌دهد!

پس از اعمال تغییرات فوق، برنامه را اجرا می‌کنیم. سپس یک اتاق جدید را اضافه کرده و در لیست نمایش اتاق‌ها، گزینه‌ی ویرایش آن‌را انتخاب می‌کنیم. در این حالت هرچند کار مقدار دهی HotelRoomModel.Details در زمان ثبت اطلاعات انجام شده، اما ... در زمان ویرایش چیزی نمایش داده نمی‌شود و تغییراتی را که به متد رویدادگردان OnInitializedAsync اضافه کرده‌ایم، عمل نمی‌کنند.
در این مورد در قسمت بررسی چرخه‌ی حیات کامپوننت‌ها توضیحاتی ابتدایی ارائه شد:
«رویدادهای OnAfterRender و OnAfterRenderAsync

پس از هر بار رندر کامپوننت، این متدها فراخوانی می‌شوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آن‌ها به پایان رسیده‌است. یکی از کاربردهای آن، آغاز کامپوننت‌های جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.»

بنابراین در این حالت خاص که ادیتور جاوا اسکریپتی مورد استفاده، پس از رندر کامل UI نمایش داده می‌شود، قرار دادن متد SetHTML در روال رویدادگردان OnInitializedAsync کار نخواهد کرد و باید آن‌را به روال رویدادگردان OnAfterRender انتقال دهیم:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
   await SetHTMLAsync();
}
پس از این تغییرات هم باز متن وارد شده‌ی در قسمت توضیحات، در حالت ویرایش نمایش داده نمی‌شود! علت آن‌را نیز در مطلب بررسی چرخه‌ی حیات کامپوننت‌ها بررسی کردیم: «یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمی‌شوند؛ چون در مرحله‌ی آخر رندر UI قرار دارند.» به همین جهت نیاز به فراخوانی دستی StateHasChanged وجود دارد:
    private async Task SetHTMLAsync()
    {
        if(!string.IsNullOrEmpty(HotelRoomModel.Details))
        {
            await QuillHtml.LoadHTMLContent(HotelRoomModel.Details);
            StateHasChanged();
        }
    }


مشکل! اگر در این حالت سعی کنیم متنی را در ادیتور وارد کنیم، میسر نیست و همچنین CPU Usage سیستم به 100 درصد رسیده‌است!

علت اینجا است که فراخوانی StateHasChanged، هر چند سبب رندر مجدد UI می‌شود، اما چون در پایان کار رندر قرار داریم، یک حلقه‌ی بی‌نهایت را سبب خواهد شد. به همین جهت باید در متد OnAfterRenderAsync، بر اساس پارامتر firstRender، از رندرهای بعدی جلوگیری کرد:
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        while (true)
        {
            try
            {
                await SetHTMLAsync();
                break;
            }
            catch
            {
                await Task.Delay(100); // Quill needs some time to load
            }
        }
    }
در اینجا هم مدیریت firstRender را مشاهده می‌کنید، تا دیگر یک حلقه‌ی بی‌نهایت رخ ندهد و هم حلقه‌ای را جهت منتظر ماندن تا بارگذاری کامل Quill در این مثال. این افزونه‌ی جاوا اسکریپتی، حتی پس از پایان رندر کامپوننت هم نیاز به مدت زمانی دارد تا بتواند کامل بارگذاری شده و قابل استفاده شود.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-20.zip
مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
ASP.NET Core 2.0 به همراه یک AuthenticationMiddleware است که یکی از قابلیت‌های این میان‌افزار، افزودن اطلاعات کاربر و یا همان HttpContext.User به یک کوکی رمزنگاری شده و سپس اعتبارسنجی این کوکی در درخواست‌های بعدی کاربر است. این مورد سبب خواهد شد تا بتوان بدون نیاز به پیاده سازی سیستم کامل ASP.NET Core Identity، یک سیستم اعتبارسنجی سبک، ساده و سفارشی را تدارک دید.



تعریف موجودیت‌های مورد نیاز جهت طراحی یک سیستم اعتبارسنجی

در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت می‌توان طراحی تمام قسمت‌ها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی ساده‌ی ما، شامل جدول کاربران و نقش‌های آن‌ها خواهد بود و این دو با هم رابطه‌ی many-to-many دارند. به همین جهت جدول UserRole نیز در اینجا پیش بینی شده‌است.

جدول کاربران

    public class User
    {
        public User()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }

        public string Username { get; set; }

        public string Password { get; set; }

        public string DisplayName { get; set; }

        public bool IsActive { get; set; }

        public DateTimeOffset? LastLoggedIn { get; set; }

        /// <summary>
        /// every time the user changes his Password,
        /// or an admin changes his Roles or stat/IsActive,
        /// create a new `SerialNumber` GUID and store it in the DB.
        /// </summary>
        public string SerialNumber { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
در اینجا SerialNumber فیلدی است که با هر بار ویرایش اطلاعات کاربران باید از طرف برنامه به روز رسانی شود. از آن جهت غیرمعتبر سازی کوکی کاربر استفاده خواهیم کرد. برای مثال اگر خاصیت فعال بودن او تغییر کرد و یا نقش‌های او را تغییر دادیم، کاربر در همان لحظه باید logout شود. به همین جهت چنین فیلدی در اینجا در نظر گرفته شده‌است تا با بررسی آن بتوان وضعیت معتبر بودن کوکی او را تشخیص داد.

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

    public class Role
    {
        public Role()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
البته این سیستم ساده دارای یک سری نقش ثابت و مشخص است.
    public static class CustomRoles
    {
        public const string Admin = nameof(Admin);
        public const string User = nameof(User);
    }
که این نقش‌ها را در ابتدای کار برنامه به بانک اطلاعات اضافه خواهیم کرد.


جدول ارتباط نقش‌ها با کاربران و برعکس

    public class UserRole
    {
        public int UserId { get; set; }
        public int RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }
وجود این جدول در EF Core جهت تعریف یک رابطه‌ی many-to-many ضروری است.


تعریف Context برنامه و فعالسازی Migrations در EF Core 2.0

DbContext برنامه را به صورت ذیل در یک اسمبلی دیگر اضافه خواهیم کرد:
    public interface IUnitOfWork : IDisposable
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;

        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    }

    public class ApplicationDbContext : DbContext, IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<User> Users { set; get; }
        public virtual DbSet<Role> Roles { set; get; }
        public virtual DbSet<UserRole> UserRoles { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<User>(entity =>
            {
                entity.Property(e => e.Username).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Username).IsUnique();
                entity.Property(e => e.Password).IsRequired();
                entity.Property(e => e.SerialNumber).HasMaxLength(450);
            });

            builder.Entity<Role>(entity =>
            {
                entity.Property(e => e.Name).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Name).IsUnique();
            });

            builder.Entity<UserRole>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.RoleId });
                entity.HasIndex(e => e.UserId);
                entity.HasIndex(e => e.RoleId);
                entity.Property(e => e.UserId);
                entity.Property(e => e.RoleId);
                entity.HasOne(d => d.Role).WithMany(p => p.UserRoles).HasForeignKey(d => d.RoleId);
                entity.HasOne(d => d.User).WithMany(p => p.UserRoles).HasForeignKey(d => d.UserId);
            });
        }
    }
در اینجا موجودیت‌های برنامه به صورت DbSet در معرض دید EF Core 2.0 قرار گرفته‌اند. همچنین رابطه‌ی Many-to-Many بین نقش‌ها و کاربران نیز تنظیم شده‌است.
سازنده‌ی کلاس به همراه پارامتر DbContextOptions است تا بتوان آن‌را در آغاز برنامه تغییر داد.


فعالسازی مهاجرت‌ها در EF Core 2.0

EF Core 2.0 برخلاف نگارش‌های قبلی آن به دنبال کلاسی مشتق شده‌ی از IDesignTimeDbContextFactory می‌گردد تا بتواند نحوه‌ی وهله سازی ApplicationDbContext را دریافت کند. در اینجا چون DbContext تعریف شده دارای یک سازنده‌ی با پارامتر است، EF Core 2.0 نمی‌داند که چگونه باید آن‌را در حین ساخت مهاجرت‌ها و اعمال آن‌ها، وهله سازی کند. کار کلاس ApplicationDbContextFactory ذیل دقیقا مشخص سازی همین مساله است:
    /// <summary>
    /// Only used by EF Tooling
    /// </summary>
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var basePath = Directory.GetCurrentDirectory();
            Console.WriteLine($"Using `{basePath}` as the BasePath");
            var configuration = new ConfigurationBuilder()
                                    .SetBasePath(basePath)
                                    .AddJsonFile("appsettings.json")
                                    .Build();
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            builder.UseSqlServer(connectionString);
            return new ApplicationDbContext(builder.Options);
        }
    }
کاری که در اینجا انجام شده، خواندن DefaultConnection از قسمت ConnectionStrings فایل appsettings.json است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;"
  },
  "LoginCookieExpirationDays": 30
}
و سپس استفاده‌ی از آن جهت تنظیم رشته‌ی اتصالی متد UseSqlServer و در آخر وهله سازی ApplicationDbContext.
کار یافتن این کلاس در حین تدارک و اعمال مهاجرت‌ها توسط EF Core 2.0 خودکار بوده و باید محل قرارگیری آن دقیقا در اسمبلی باشد که DbContext برنامه در آن تعریف شده‌است.


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

پس از مشخص شدن ساختار موجودیت‌ها و همچنین Context برنامه، اکنون می‌توان لایه سرویس برنامه را به صورت ذیل تکمیل کرد:

سرویس کاربران
    public interface IUsersService
    {
        Task<string> GetSerialNumberAsync(int userId);
        Task<User> FindUserAsync(string username, string password);
        Task<User> FindUserAsync(int userId);
        Task UpdateUserLastActivityDateAsync(int userId);
    }
کار این سرویس ابتدایی کاربران، یافتن یک یک کاربر بر اساس Id او و یا کلمه‌ی عبور و نام کاربری او است. از این امکانات در حین لاگین و یا اعتبارسنجی کوکی کاربر استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس نقش‌های کاربران
    public interface IRolesService
    {
        Task<List<Role>> FindUserRolesAsync(int userId);
        Task<bool> IsUserInRole(int userId, string roleName);
        Task<List<User>> FindUsersInRoleAsync(string roleName);
    }
از این سرویس برای یافتن نقش‌های کاربر لاگین شده‌ی به سیستم و افزودن آن‌ها به کوکی رمزنگاری شده‌ی اعتبارسنجی او استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس آغاز بانک اطلاعاتی

    public interface IDbInitializerService
    {
        void Initialize();
        void SeedData();
    }
از این سرویس در آغاز کار برنامه برای اعمال خودکار مهاجرت‌های تولیدی و همچنین ثبت نقش‌های آغازین سیستم به همراه افزودن کاربر Admin استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس اعتبارسنجی کوکی‌های کاربران

یکی از قابلیت‌های میان‌افزار اعتبارسنجی ASP.NET Core 2.0، رخ‌دادی است که در آن اطلاعات کوکی دریافتی از کاربر، رمزگشایی شده و در اختیار برنامه جهت تعیین اعتبار قرار می‌گیرد:
    public interface ICookieValidatorService
    {
        Task ValidateAsync(CookieValidatePrincipalContext context);
    }

    public class CookieValidatorService : ICookieValidatorService
    {
        private readonly IUsersService _usersService;
        public CookieValidatorService(IUsersService usersService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));
        }

        public async Task ValidateAsync(CookieValidatePrincipalContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                await handleUnauthorizedRequest(context);
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }

        private Task handleUnauthorizedRequest(CookieValidatePrincipalContext context)
        {
            context.RejectPrincipal();
            return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
کار این سرویس، تعیین اعتبار موارد ذیل است:
- آیا کوکی دریافت شده دارای اطلاعات HttpContext.User است؟
- آیا این کوکی به همراه اطلاعات فیلد SerialNumber است؟
- آیا این کوکی به همراه Id کاربر است؟
- آیا کاربری که بر اساس این Id یافت می‌شود غیرفعال شده‌است؟
- آیا کاربری که بر اساس این Id یافت می‌شود دارای SerialNumber یکسانی با نمونه‌ی موجود در بانک اطلاعاتی است؟

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


تنظیمات ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0

تنظیمات کامل ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0 را در فایل Startup.cs می‌توانید مشاهده کنید.

ابتدا سرویس‌های برنامه معرفی شده‌اند:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IUsersService, UsersService>();
            services.AddScoped<IRolesService, RolesService>();
            services.AddScoped<ISecurityService, SecurityService>();
            services.AddScoped<ICookieValidatorService, CookieValidatorService>();
            services.AddScoped<IDbInitializerService, DbInitializerService>();

سپس تنظیمات مرتبط با ترزیق وابستگی‌های ApplicationDbContext برنامه انجام شده‌است. در اینجا رشته‌ی اتصالی، از فایل appsettings.json خوانده شده و سپس در اختیار متد UseSqlServer قرار می‌گیرد:
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                    serverDbContextOptionsBuilder =>
                        {
                            var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                            serverDbContextOptionsBuilder.CommandTimeout(minutes);
                            serverDbContextOptionsBuilder.EnableRetryOnFailure();
                        });
            });

در ادامه تعدادی Policy مبتنی بر نقش‌های ثابت سیستم را تعریف کرده‌ایم. این کار اختیاری است اما روش توصیه شده‌ی در ASP.NET Core، کار با Policyها است تا کار مستقیم با نقش‌ها. Policy‌ها انعطاف پذیری بیشتری را نسبت به نقش‌ها ارائه می‌دهند و در اینجا به سادگی می‌توان چندین نقش و یا حتی Claim را با هم ترکیب کرد و به صورت یک Policy ارائه داد:
            // Only needed for custom roles.
            services.AddAuthorization(options =>
                    {
                        options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
                        options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                    });

قسمت اصلی تنظیمات میان افزار اعتبارسنجی مبتنی بر کوکی‌ها در اینجا قید شده‌است:
            // Needed for cookie auth.
            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(options =>
                {
                    options.SlidingExpiration = false;
                    options.LoginPath = "/api/account/login";
                    options.LogoutPath = "/api/account/logout";
                    //options.AccessDeniedPath = new PathString("/Home/Forbidden/");
                    options.Cookie.Name = ".my.app1.cookie";
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = context =>
                        {
                            var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
                            return cookieValidatorService.ValidateAsync(context);
                        }
                    };
                });
ابتدا مشخص شده‌است که روش مدنظر ما، اعتبارسنجی مبتنی بر کوکی‌ها است و سپس تنظیمات مرتبط با کوکی رمزنگاری شده‌ی برنامه مشخص شده‌اند. تنها قسمت مهم آن CookieAuthenticationEvents است که نحوه‌ی پیاده سازی آن‌را با معرفی سرویس ICookieValidatorService پیشتر بررسی کردیم. این قسمت جائی است که پس از هر درخواست به سرور اجرا شده و کوکی رمزگشایی شده، در اختیار برنامه جهت اعتبارسنجی قرار می‌گیرد.

کار نهایی تنظیمات میان افزار اعتبارسنجی در متد Configure با فراخوانی UseAuthentication صورت می‌گیرد. اینجا است که میان افزار، به برنامه معرفی خواهد شد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseAuthentication();

همچنین پس از آن، کار اجرای سرویس آغاز بانک اطلاعاتی نیز انجام شده‌است تا نقش‌ها و کاربر Admin را به سیستم اضافه کند:
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var dbInitializer = scope.ServiceProvider.GetService<IDbInitializerService>();
                dbInitializer.Initialize();
                dbInitializer.SeedData();
            }


پیاده سازی ورود و خروج به سیستم

پس از این مقدمات به مرحله‌ی آخر پیاده سازی این سیستم اعتبارسنجی می‌رسیم.

پیاده سازی Login
در اینجا از سرویس کاربران استفاده شده و بر اساس نام کاربری و کلمه‌ی عبور ارسالی به سمت سرور، این کاربر یافت خواهد شد.
در صورت وجود این کاربر، مرحله‌ی نهایی کار، فراخوانی متد الحاقی HttpContext.SignInAsync است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                return Unauthorized();
            }

            var loginCookieExpirationDays = _configuration.GetValue<int>("LoginCookieExpirationDays", defaultValue: 30);
            var cookieClaims = await createCookieClaimsAsync(user).ConfigureAwait(false);
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                cookieClaims,
                new AuthenticationProperties
                {
                    IsPersistent = true, // "Remember Me"
                    IssuedUtc = DateTimeOffset.UtcNow,
                    ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
                });

            await _usersService.UpdateUserLastActivityDateAsync(user.Id).ConfigureAwait(false);

            return Ok();
        }
مهم‌ترین کار متد HttpContext.SignInAsync علاوه بر تنظیم طول عمر کوکی کاربر، قسمت createCookieClaimsAsync است که به صورت ذیل پیاده سازی شده‌است:
        private async Task<ClaimsPrincipal> createCookieClaimsAsync(User user)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
            identity.AddClaim(new Claim("DisplayName", user.DisplayName));

            // to invalidate the cookie
            identity.AddClaim(new Claim(ClaimTypes.SerialNumber, user.SerialNumber));

            // custom data
            identity.AddClaim(new Claim(ClaimTypes.UserData, user.Id.ToString()));

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
            }

            return new ClaimsPrincipal(identity);
        }
در اینجا است که اطلاعات اضافی کاربر مانند Id او یا نقش‌های او به کوکی رمزنگاری شده‌ی تولیدی توسط HttpContext.SignInAsync اضافه شده و در دفعات بعدی و درخواست‌های بعدی او، به صورت خودکار توسط مرورگر به سمت سرور ارسال خواهند شد. این کوکی است که امکان کار با MyProtectedApiController و یا MyProtectedAdminApiController را فراهم می‌کند. اگر شخص لاگین کرده باشد، بلافاصله قابلیت دسترسی به امکانات محدود شده‌ی توسط فیلتر Authorize را خواهد یافت. همچنین در این مثال چون کاربر Admin لاگین می‌شود، امکان دسترسی به Policy مرتبطی را نیز خواهد یافت:
[Route("api/[controller]")]
[Authorize(Policy = CustomRoles.Admin)]
public class MyProtectedAdminApiController : Controller

پیاده سازی Logout

متد الحاقی HttpContext.SignOutAsync کار Logout کاربر را تکمیل می‌کند.
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return true;
        }


آزمایش نهایی برنامه

در فایل index.html ، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
آشنایی با TransactionScope
زمانی که از تراکنش‌های مربوط به DbConnection استفاده می‌کنید اتمام آن توسط متد Commit موجود در این کلاس مشخص خواهد شد نه متد SaveChanges . در نتیجه اگر تمامی SaveChanges‌ها فراخوانی شده  به درستی انجام شوند کل عملیات Commit در غیر این صورت عملیات RollBack می‌شود. در مورد TransactionScope‌ها نیز به همین صورت است. اگر قبل از رسیدن اجرای برنامه به دستور transcation.Complete هر گونه خطایی صادر شود عملیات RollBack خواهد شد.
مطالب
Angular CLI - قسمت هفتم - اجرای آزمون‌ها
پروژه‌های Angular CLI در حالت پیش فرض آن‌ها به همراه دو نوع آزمون واحد و آزمون end to end ایجاد می‌شوند. Angular CLI از Karma برای اجرای آزمون‌های واحد استفاده می‌کند و از Protractor برای اجرای آزمون‌های end to end. برای شروع می‌توان از راهنمای آن کمک گرفت:
 > ng test --help
زمانیکه دستور ng test را اجرا می‌کنیم، به صورت خودکار تمام فایل‌های spec.ts.* را یافته و آزمون‌های واحد موجود در آن‌ها را اجرا می‌کند. این نوع فایل‌های ویژه نیز به صورت خودکار، زمانیکه اجزای مختلف Angular را توسط Angular CLI ایجاد می‌کنیم، تولید می‌شوند. به علاوه دستور ng test تغییرات این فایل‌ها را تحت نظر قرار داده و در صورت نیاز، آزمون‌های واحد را مجددا و به صورت خودکار اجرا می‌کند.


یک مثال: بررسی اجرای دستور ng test

یکی از مثال‌های بررسی شده‌ی در این سری را انتخاب و یا حتی یک برنامه‌ی جدید را توسط Angular CLI ایجاد کرده و سپس دستور ng test را در ریشه‌ی این پروژه اجرا کنید. به این ترتیب برنامه به صورت خودکار کامپایل شده و سپس به صورت خودکار آزمون‌های واحد آن‌را که در فایل‌های spec.ts‌.* قرار دارند، اجرا می‌کند. در آخر نتیجه را در مرورگر گزارش می‌دهد:


همانطور که مشخص است، 3 specs, 3 failures داریم. در اینجا می‌توان بر روی لینک Spec List کلیک کرد و لیست آزمون‌های واحد موجود را مشاهده نمود:


هر کدام از عناوین ذکر شده نیز به جزئیات مشکلات آن‌ها، لینک شده‌اند. برای مثال اگر بر روی اولین مورد کلیک کنیم، خطایی مانند «'alert' is not a known element» قابل مشاهده‌است. به این معنا که برای نمونه در قسمت قبل کامپوننت alert را به صفحه اضافه کردیم:
 <alert type="success">Alert success!</alert>
اما اجرا کننده‌ی آزمون‌های واحد اطلاعاتی در مورد آن ندارد؛ از این جهت که آزمون‌های واحد به صورت ایزوله فقط همان کامپوننت خاص برنامه را آزمایش می‌کنند و کاری به وابستگی‌های آن ندارد. به همین جهت فایل src\app\app.component.spec.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { NO_ERRORS_SCHEMA } from '@angular/core';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
  schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
  }));
در اینجا ابتدا ماژول NO_ERRORS_SCHEMA معرفی شده و سپس به قسمت schemas معرفی گشته‌است.
پس از این تغییر، بلافاصله مجدد برنامه کامپایل شده و آزمون‌های واحد آن با موفقیت اجرا می‌شوند (با این فرض که هنوز پنجره‌ی اجرا کننده‌ی دستور ng test را باز نگه داشته‌اید):


تغییر افزودن schemas: [NO_ERRORS_SCHEMA] را باید در مورد تمام فایل‌های spec موجود تکرار کرد.


گزینه‌های مختلف دستور ng test

دستور ng test به همراه گزینه‌های متعددی است که شرح آن‌ها را در جدول ذیل مشاهده می‌کنید:

گزینه
 مخفف  توضیح
 code-coverage--  cc-   تولید گزارش code coverage که به صورت پیش فرض خاموش است. 
 colors--     به صورت پیش فرض فعال است و سبب نمایش رنگ‌های قرمز و سبز، برای آزمون‌های شکست خورده و یا موفق می‌شود. 
 single-run--  sr-   اجرای یکباره‌ی آزمون‌های واحد، بدون فعال سازی گزینه‌ی مشاهده‌ی مداوم تغییرات که به صورت پیش فرض خاموش است. 
 progress--     نمایش جزئیات کامپایل و اجرای آزمون‌های واحد که به صورت پیش فرض فعال است. 
 sourcemaps--  sm-   تولید فایل‌های سورس‌مپ که به صورت پیش فرض فعال است. 
 watch--
 w-   بررسی مداوم تغییرات فایل‌ها و اجرای آزمون‌های واحد به صورت خودکار که به صورت پیش فرض فعال است. 

بنابراین اجرا دستور ng test بدون ذکر هیچ گزینه‌ای به معنای اجرای مداوم آزمون‌های واحد، در صورت مشاهده‌ی تغییراتی در آن‌ها، به کمک Karma است.
همچنین دو دستور ذیل نیز به یک معنا هستند و هر دو سبب یکبار اجرای آزمون‌های واحد می‌شوند:
> ng test -sr
> ng test -w false


اجرای بررسی میزان پوشش آزمون‌های واحد

یکی از گزینه‌های ng test روشن کردن پرچم code-coverage است:
 > ng test --code-coverage
برای آزمایش آن دستور ذیل را در ریشه‌ی پروژه اجرا کنید (که سبب اجرای یکبار برررسی میزان پوشش آزمون‌های واحد می‌شود):
 > ng test -sr -cc
پس از اجرای این آزمون ویژه، پوشه‌ی جدیدی به نام coverage در ریشه‌ی پروژه‌ی جاری تشکیل می‌شود. فایل index.html آن‌را در مرورگر باز کنید تا بتوان گزارش تولید شده را مشاهده کرد:


کار این آزمون بررسی قسمت‌های مختلف برنامه و ارائه گزارشی است که مشخص می‌کند آیا آزمون‌های واحد نوشته شده تمام انشعابات برنامه را پوشش می‌دهند یا خیر؟ برای مثال اگر در متدی if/else دارید، آیا فقط قسمت if را پوشش داده‌اید و یا آیا قسمت else هم در آزمون‌های واحد، بررسی شده‌است.


اجرای آزمون‌های end to end

هدف از ساخت یک برنامه ... استفاده‌ی از آن توسط دیگران است؛ اینجا است که آزمون‌های end to end مفهوم پیدا می‌کنند. در آزمون‌های e2e رفتار برنامه همانند حالتی که یک کاربر از آن استفاده می‌کند، بررسی می‌شود (برای مثال باز کردن مرورگر، لاگین و مرور صفحات). برای این منظور، Angular CLI  در پشت صحنه از Protractor برای این نوع آزمون‌ها استفاده می‌کند.  
برای مشاهده‌ی راهنما و گزینه‌های مختلف مرتبط با آزمون‌های e2e، می‌توان دستور ذیل را صادر کرد:
 >ng e2e --help
البته با توجه به اینکه این دستور کار توزیع برنامه را نیز انجام می‌دهد، تمام گزینه‌های ng serve نیز در اینجا صادق هستند، به علاوه‌ی موارد ذیل:

 گزینه  مخفف توضیح
 config--  c-   به فایل کانفیگ آزمون‌های e2e اشاره می‌کند که به صورت پیش‌فرض همان protractor.conf.js واقع در ریشه‌ی پروژه‌است. 
 element-explorer--  ee-   بررسی و دیباگ protractor از طریق خط فرمان 
 serve--  s-   کامپایل و توزیع برنامه بر روی پورتی اتفاقی (حالت پیش فرض آن true است) 
 specs--  sp-   پیش فرض آن بررسی تمام specهای موجود در پروژ‌ه‌است. اگر نیاز به لغو آن باشد می‌توان از این گزینه استفاده کرد. 
 webdriver-update--  wu- به روز رسانی web driver که به صورت پیش فرض فعال است. 

بنابراین زمانیکه دستور ng e2e صادر می‌شود، به معنای کامپایل، توزیع برنامه بر روی پورتی اتفاقی و اجرای آزمون‌ها است.

از این جهت که این نوع آزمون‌ها، وابسته‌ی به جزئی خاص از برنامه نیستند، حالت عمومی داشته و فایل‌های spec آن‌ها را می‌توان در پوشه‌ی e2e واقع در ریشه‌ی پروژه، یافت. برای مثال در قسمتی از آن کار یافتن متن نمایش داده شده‌ی در صفحه‌ی اول سایت انجام می‌شود
getParagraphText() {
    return element(by.css('app-root h1')).getText();
}
و سپس در فایل spec آن بررسی می‌کند که آیا مساوی app works هست یا خیر؟
 expect(page.getParagraphText()).toEqual('app works!');

برای آزمایش آن دستور ng e2e را در ریشه‌ی پروژه صادر کنید. همچنین دقت داشته باشید که در این حالت نیاز است به اینترنت نیز متصل باشد؛ چون از chromedriver api گوگل نیز استفاده می‌کند. در غیراینصورت خطای ذیل را دریافت خواهید کرد:
 Error: getaddrinfo ENOTFOUND chromedriver.storage.googleapis.com chromedriver.storage.googleapis.com:443
نظرات مطالب
آشنایی با TransactionScope
برای اجرای تراکنش در سیستم‌های با کاربر و حجم داده زیاد بهتر ه از امکانات تراکنش موجود در ORM‌ها استفاده کنید. برای مثال در Entity Framework می‌تونید از DBTransaction‌ها استفاده کنید یا در NHibernate از تراکنش موجود در Session  استفاده کنید. برای مثال در CodeFirst
  public void Save( TEntity entity )
        {
            DbTransaction transaction = null;
            try
            {
                transaction = this.Database.Connection.BeginTransaction();
                //عملیات مورد نظر
                transaction.Commit();
            }
            catch
            {
                transaction.Rollback();
            }
            finally
            {
                transaction.Dispose();
            }          
        }
البته در زمان مناسب در صورت نیاز یک پست رو به این مورد اختصاص خواهم داد.